INotifyPropertyChanged вызывает межпотоковую ошибку

Вот мой сценарий:

У меня есть GridControl, связанный с BindingList. Сначала я создавал рабочий поток и получал прямой доступ к BindingList, но это вызывало "Обнаружена операция с несколькими потоками", поэтому я следовал приведенному здесь руководству:

http://www.devexpress.com/Support/Center/p/AK2981.aspx

Клонировав оригинальный BindingList в рабочий поток и изменив его, я получил желаемый эффект. Однако недавно я реализовал INotifyPropertyChanged в объект, который содержится в BindingList, и я снова начал получать ошибку.

Я предполагаю, что GridView все еще прослушивает INotifyPropertyChanged от объекта.

Как я могу это исправить?

Мои занятия:

public class Proxy : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

4 ответа

Решение

Если вы манипулируете пользовательским интерфейсом извне потока пользовательского интерфейса (например, из рабочего потока), вам необходимо присоединиться к потоку пользовательского интерфейса. Вы можете сделать это, позвонив Invoke на контроле пользовательского интерфейса. Вы можете проверить, требуется ли это, используя InvokeRequired,

Обычно используется шаблон:

public void ChangeText(string text)
{
   if(this.InvokeRequired)
   {
      this.Invoke(new Action(() => ChangeText(text)));
   }
   else
   {
      label.Text = text;  
   }
}

В вашем случае пользовательский интерфейс манипулирует в результате INotifyPropertyChanged, поэтому вам нужно убедиться, что вы либо всегда изменяете свою сущность в потоке пользовательского интерфейса (используя описанную выше технику), либо используете универсальный асинхронный помощник INotifyPropertyChanged. Это обертка вокруг привязываемого предмета. Он использует вышеуказанную технику, чтобы обеспечить ChangeProperty событие запускается в потоке пользовательского интерфейса.

Вот очень грубый пример прокси для Entity учебный класс. Это гарантирует, что событие изменения свойства присоединяется к потоку пользовательского интерфейса, и сохраняет саму сущность неизмененной. Очевидно, вы, вероятно, захотите реализовать это более широко, например, с помощью DynamicObject.

public class NotificationHelper : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly ISynchronizeInvoke invokeDelegate;
    private readonly Entity entity;

    public NotificationHelper(ISynchronizeInvoke invokeDelegate, Entity entity)
    {
       this.invokeDelegate = invokeDelegate;
       this.entity = entity;

       entity.PropertyChanged += OnPropertyChanged;
    }

    public string Name
    {
       get { return entity.Name; }
    }

    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
        {
           if (invokeDelegate.InvokeRequired)
           {
               invokeDelegate.Invoke(new PropertyChangedEventHandler(OnPropertyChanged),
                                     new[] { sender, e });
               return;
           }
           PropertyChanged(this, e);
        }
     }
 }

Я применил аналогичный подход к возможному решению TheGateKeeper. Однако я был привязан ко многим различным объектам. Поэтому мне нужно что-то более общее. Решение состояло в том, чтобы создать оболочку, которая также реализовала ICustomTypeDescriptor. Таким образом, мне не нужно создавать свойства оболочки для всего, что может отображаться в пользовательском интерфейсе.

public class SynchronizedNotifyPropertyChanged<T> : INotifyPropertyChanged, ICustomTypeDescriptor
    where T : INotifyPropertyChanged
{
    private readonly T _source;
    private readonly ISynchronizeInvoke _syncObject;

    public SynchronizedNotifyPropertyChanged(T source, ISynchronizeInvoke syncObject)
    {
        _source = source;
        _syncObject = syncObject;

        _source.PropertyChanged += (sender, args) => OnPropertyChanged(args.PropertyName);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged == null) return;

        var handler = PropertyChanged;
        _syncObject.BeginInvoke(handler, new object[] { this, new PropertyChangedEventArgs(propertyName) });
    }

    public T Source { get { return _source; }}

    #region ICustomTypeDescriptor
    public AttributeCollection GetAttributes()
    {
        return new AttributeCollection(null);
    }

    public string GetClassName()
    {
        return TypeDescriptor.GetClassName(typeof(T));
    }

    public string GetComponentName()
    {
        return TypeDescriptor.GetComponentName(typeof (T));
    }

    public TypeConverter GetConverter()
    {
        return TypeDescriptor.GetConverter(typeof (T));
    }

    public EventDescriptor GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(typeof (T));
    }

    public PropertyDescriptor GetDefaultProperty()
    {
        return TypeDescriptor.GetDefaultProperty(typeof(T));
    }

    public object GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(typeof (T), editorBaseType);
    }

    public EventDescriptorCollection GetEvents()
    {
        return TypeDescriptor.GetEvents(typeof(T));
    }

    public EventDescriptorCollection GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(typeof (T), attributes);
    }

    public PropertyDescriptorCollection GetProperties()
    {
        return TypeDescriptor.GetProperties(typeof (T));
    }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return TypeDescriptor.GetProperties(typeof(T), attributes);
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return _source;
    }
    #endregion ICustomTypeDescriptor
}

Затем в пользовательском интерфейсе я связываюсь с этой оболочкой, используя что-то вроде:

    private void CreateBindings()
    {
        if (_model == null) return;

        var threadSafeModel = new SynchronizedNotifyPropertyChanged<MyViewModel>(_model, this);

        directiveLabel.DataBindings.Add("Text", threadSafeModel, "DirectiveText", false, DataSourceUpdateMode.OnPropertyChanged);
    }

MyViewModel имеет свойство "DirectiveText" и реализует INotifyPropertyChanged без особого учета потоков или классов представлений.

Я подкласс BindingList так что я мог проверить на необходимый Invoke, Таким образом, мои бизнес-объекты не имеют ссылки на пользовательский интерфейс.

public class InvokingBindingList<T> : BindingList<T>
{
  public InvokingBindingList(IList<T> list, Control control = null) : base(list)
  {
    this.Control = control;
  }

  public InvokingBindingList(Control control = null)
  {
    this.Control = control;
  }

  public Control Control { get; set; }

  protected override void OnListChanged(ListChangedEventArgs e)
  {
    if (Control?.InvokeRequired == true)
      Control.Invoke(new Action(() => base.OnListChanged(e)));
    else
      base.OnListChanged(e);
  }
}

На всякий случай, если кто-то столкнулся с той же проблемой... Мне удалось исправить это через несколько часов. Вот что я сделал:

В основном проблема заключалась в том, что объект, реализующий INotifyPropertyChanged, жил в рабочем потоке, и это вызывает проблемы при доступе к потоку пользовательского интерфейса.

Поэтому я передал ссылку на объект, который необходимо обновить, на объект INotifyPropertyChanged, а затем использовал invoke для него.

Вот как это выглядит:

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            //If the Proxy object is living in a non-UI thread, use invoke
            if (c != null)
            {
                c.BeginInvoke(new Action(() => handler(this, new PropertyChangedEventArgs(name))));
            }
            //Otherwise update directly
            else
            {
                handler(this, new PropertyChangedEventArgs(name));
            }

        }
    }

    //Use this to reference the object on the UI thread when there is need to
    public Control C
    {
        set { c = value; }
    }

Из потока, все, что я сделал, было:

                    prox.c = this;
                    //Logic here
                    prox.c = null;

Надеюсь, это поможет кому-то!!

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