INotifyPropertyChanged causes cross-thread error

后端 未结 4 1786
走了就别回头了
走了就别回头了 2020-12-16 06:21

Here is my scenarion:

I have a GridControl bound to a BindingList. At first what I was doing was creating a worker thread and access the BindingList directly, but th

相关标签:
4条回答
  • 2020-12-16 06:29

    Just in case someone has run into the same problem... I managed to fix it after some hours. Here is what I did:

    Basically the problem was that the object implementing INotifyPropertyChanged was living in a worker thread, and this causes problems when accessing the UI thread.

    So what I did was pass a reference to the object that needs to be updated to the INotifyPropertyChanged object, and then use invoke on it.

    Here is what it looks like:

        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; }
        }
    

    From the thread, all I did was:

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

    Hope this helps someone!!

    0 讨论(0)
  • 2020-12-16 06:35

    I subclassed BindingList so I could check for a required Invoke. This way my business objects do not have a reference to the UI.

    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);
      }
    }
    
    0 讨论(0)
  • 2020-12-16 06:39

    I took a similar approach to TheGateKeeper's eventual solution. However I was binding to many different objects. So I needed something a bit more generic. The solution was to create a wrapper that implemented also ICustomTypeDescriptor. In this way, I do not need to create wrapper properties for everything that can be displayed in the UI.

    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
    }
    

    Then in the Ui, I bind to this wrapper using something like:

        private void CreateBindings()
        {
            if (_model == null) return;
    
            var threadSafeModel = new SynchronizedNotifyPropertyChanged<MyViewModel>(_model, this);
    
            directiveLabel.DataBindings.Add("Text", threadSafeModel, "DirectiveText", false, DataSourceUpdateMode.OnPropertyChanged);
        }
    

    MyViewModel has a "DirectiveText" property and implements INotifyPropertyChanged with no special consideration to threading or to the view classes.

    0 讨论(0)
  • 2020-12-16 06:44

    If you are manipulating the UI from outside of the UI thread (such as from a worker thread), then you need to rejoin the UI thread. You can do this by calling Invoke on the UI control. You can test if this is required by using InvokeRequired.

    The pattern typically used is this:

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

    In your case the UI is being manipulated as a result of INotifyPropertyChanged, so you need to make sure that either you always modify your entity on the UI thread (using the above technique), or use a generic asynchronous INotifyPropertyChanged helper. This is a wrapper around the item being bound. It uses the above technique to ensure the ChangeProperty event fires on the UI thread.

    Here's a very crude example of a proxy for an Entity class. This ensures that the property change event rejoins the UI thread, and keeps the entity itself unmodified. Obviously you'll probably want to implement this more generically using DynamicObject for instance.

    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);
            }
         }
     }
    
    0 讨论(0)
提交回复
热议问题