C# DataBinding / Обновить свойство DynamicObject из другого потока

Ранее я работал над реализацией привязки данных DynamicObject (см. Двусторонняя привязка данных к динамическим объектам), и решение отлично работает. С тех пор я столкнулся с необходимостью иметь возможность обновлять значения из разных потоков, что, кажется, нарушает привязки. Я могу обновить экземпляр DynamicObject из нескольких потоков, но не могу обновлять привязки.

Я попытался реализовать решение SynchronizedNotifyPropertyChanged, предоставленное Коди Барнсом в SO: INotifyPropertyChanged, приводит к межпотоковой ошибке, но безуспешно.

Будем весьма благодарны за любую помощь в связывании реализации DynamicObject, которая обновляется через не-пользовательский поток. Решение и сущность приложения работают отлично (значения обновляются в любом потоке (пользовательский или не-пользовательский интерфейс), нет проблем), это просто привязка данных к элементу управления формой, что не так.

Edit # 1 - (Спасибо Реза Агаи)

Я вижу, где мой вопрос немного расплывчатый, вот реализация и цель, которую я пытаюсь достичь.

Во-первых, у меня есть форма, которая обрабатывает создание логического "движка", который может выполнять задачи (на основе внутренних методов класса, а также внешних вызовов методов или событий). Итак, где-то внутри класса Form, я генерирую этот логический объект 'engine' (в этом примере назовем его GameEngine).

// GameEngine initializes and provides the DynamicData (MyCustomObject)
// --> engine is defined via public or private field of (this) Form class
engine = new GameEngine();
// Create the binding
LabelTestCounter.Databindings.Add("Text", engine.Data, "TestValue", true, DataSourceUpdateMode.OnPropertyChanged);

Теперь в классе GameEngine я создаю экземпляр DynamicData (MyCustomObject)

public class GameEngine
{
    public dynamic Data;

    // Constructor
    public GameEngine()
    {
        Data = new MyCustomObject();
        // I would like to initialize the data here as ownership really
        // belongs to the 'GameEngine' object, not the Form
        engine.Data.TestValue = "Initial test value";
    }

    public void StartBusinessLogic()
    {
        // In reality, this would be a long-running loop that updates 
        // multiple values and performs business logic.
        Task.Run(() => {
            data.TestValue = "Another Text";
        });
    }
}

Обратите внимание, что в этом примере выше MyCustomObject (в настоящее время) является точной копией того, что Реза предоставил в своей сути в своем ответе ниже. В сообщении блога и в его сути я склоняюсь к предоставленному "Варианту 1", так как я хотел бы, чтобы объект DynamicData (MyCustomObject) сам содержал логику для синхронизации в целях переносимости.

Еще одно замечание: GameEngine, вне удержания объекта DynamicData в свойстве, не должен заботиться о том, использует ли его поток пользовательского интерфейса, ответственность за синхронизацию должна (на мой взгляд) оставаться за потоком пользовательского интерфейса. При этом объект DynamicData должен знать о многопоточных вызовах и обрабатывать их соответствующим образом по мере необходимости.

Правка № 2 - Обновление оригинальной ссылки на вопрос

В отношении:

Я попытался реализовать решение SynchronizedNotifyPropertyChanged, предоставленное Коди Барнсом в SO: INotifyPropertyChanged, приводит к межпотоковой ошибке, но безуспешно.

При работе с исходным объектом SO Dynamic, двусторонней привязкой данных и оригинальным решением (еще раз спасибо Reza), я попытался реализовать ссылочную "оболочку", чтобы преодолеть проблему с многопоточными обновлениями. Однако единственный способ, с помощью которого я мог заставить работать вышеупомянутое решение, заключался в жестком кодировании свойств в классе. Когда делались какие-либо попытки использовать динамический объект, привязки либо обновлялись бы один раз и теряли привязку, либо генерировали какое-то исключение привязки.

Я надеюсь, что это проясняет исходный вопрос немного лучше.

Правка № 3 - Символ не разрешен

Не уверен, что это проблема, так как компиляция работает (и это может быть Resharper, не уверен, так как я не вспомнил проблему ранее).

В "MyCustomObject":

public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
    var properties = new List<MyCustomPropertyDescriptor>();
    foreach (var e in dictionary)
        properties.Add(new MyCustomPropertyDescriptor(e.Key, (e.Value?.GetType()) ?? typeof(object)));
    return new PropertyDescriptorCollection(properties.ToArray());
}

(e.Value?.GetType()) <-- GetType is showing 'Cannot resolve symbol 'GetType'.

однако код компилируется без ошибок, поэтому я не уверен, почему / когда именно я начал это видеть.

Правка № 4 - Решено Правка № 3

Понятия не имею, что вызвало проблему в Edit 3, но она стала показывать другие ошибки (таинственно), такие как Cannot apply indexing with to an expression of type 'System.Collections.Generic.Dictionary' Однако я сделал Build -> Clean Solution а затем закрыл решение и снова открыл его в Visual Studio, и проблема, казалось, исчезла с подсветкой этой ошибки в редакторе (что я видел странное поведение, так как я начал играть с Resharper (EAP), так что, возможно, ранние ошибки доступа - но эта проблема не связана с этим SO, исходный SO был решен, и странное поведение в edit 3 будет лучше обрабатываться командой JetBrains/Resharper, а не здесь, на данном этапе.

Редактировать № 5 - Особая благодарность

Я надеюсь, что это не является неуместным (если это так, администраторы могут свободно удалять / редактировать это), однако, я хотел бы отправить отдельное спасибо Резе Агаи за всю вашу помощь как здесь, так и в фоновом режиме. Реза, ваш блог, специалисты и другая помощь в решении этой постоянной проблемы действительно помогли как решить проблему, так и помочь мне понять причины, лежащие в основе решения.

1 ответ

Решение

Поднимать OnPropertyChanged из потока, не являющегося пользовательским интерфейсом, таким образом, который обновляет пользовательский интерфейс, вам необходимо передать экземпляр ISynchronizeInvoke к вашей реализации ICustomTypeDescriptor:

ISynchronizeInvoke syncronzeInvoke;
public MyCustomObject(ISynchronizeInvoke value = null)
{
    syncronzeInvoke = value;
}

Затем на OnPropertyChanged использовать это ISynchronizeInvoke в Invoke когда InvokeRequired:

private void OnPropertyChanged(string name)
{
    var handler = PropertyChanged;
    if (handler != null)
    {
        if (syncronzeInvoke != null && syncronzeInvoke.InvokeRequired)
            syncronzeInvoke.Invoke(handler, new object[] 
                { this, new PropertyChangedEventArgs(name) });
        else
            handler(this, new PropertyChangedEventArgs(name));
    }
}

Таким образом, событие будет вызываться в потоке пользовательского интерфейса при необходимости.

Другие вопросы по тегам