2012-04-14 18 views
5

Oto moja scenariusz:INotifyPropertyChanged powoduje błąd wątku krzyżowego

Mam kontrolkę GridControl powiązaną z opcją BindingList. Początkowo co robiłem było stworzenie wątku roboczego i uzyskania dostępu do BindingList bezpośrednio, ale to było rzucanie „operację Cross-wątek wykryty”, więc po instrukcji tutaj:

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

przez klonowanie oryginał BindingList do wątku roboczego i zmiana tego, uzyskałem pożądany efekt. Jednak niedawno wdrożyłem INotifyPropertyChanged do obiektu, który jest trzymany w BindingList i zacząłem otrzymywać błąd ponownie.

Domyślam się, że GridView wciąż nasłuchuje INotifyPropertyChanged od obiektu.

Jak mogę to naprawić?

Moja klasa:

public class Proxy : INotifyPropertyChanged 
{ 
    public event PropertyChangedEventHandler PropertyChanged; 

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

Odpowiedz

10

Jeśli manipulując UI z zewnątrz wątku UI (takie jak z wątku pracownika), to trzeba wrócić do wątku UI. Możesz to zrobić, dzwoniąc pod numer Invoke w formancie interfejsu użytkownika. Możesz sprawdzić, czy jest to wymagane przy użyciu InvokeRequired.

Wzór zazwyczaj stosowany jest taka:

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

W twoim przypadku UI jest manipulowany w wyniku INotifyPropertyChanged, więc trzeba się upewnić, że albo ty zawsze modyfikować swoją jednostkę na wątku UI (przy użyciu powyższej techniki) lub użyj generic asynchronous INotifyPropertyChanged helper. To jest opakowanie wokół przedmiotu, który jest związany. Używa powyższej techniki, aby upewnić się, że zdarzenie ChangeProperty zostanie wywołane w wątku interfejsu użytkownika.

Oto bardzo prosty przykład proxy dla klasy Entity. Gwarantuje to, że zdarzenie zmiany właściwości zostanie ponownie dołączone do wątku interfejsu użytkownika i spowoduje, że sama jednostka nie będzie modyfikowana. Oczywiście prawdopodobnie będziesz chciał zaimplementować to bardziej ogólnie używając DynamicObject na przykład.

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

hmm .... Więc muszę umieścić to w zdarzeniu INotifyPropertyChanged? Zaktualizowałem pytanie za pomocą kodu zajęć. – TheGateKeeper

+0

Zaktualizowałem, aby wyjaśnić, albo zmień tylko związany obiekt w wątku interfejsu użytkownika, albo zawiń go w klasę pomocnika podczas wiązania. – TheCodeKing

+0

Nie używał tego, ale oznaczył go jako odpowiedź, ponieważ zapewnia wiele szczegółów. – TheGateKeeper

1

Tylko w przypadku, gdy ktoś prowadzi do tego samego problemu ... udało mi się go naprawić po kilku godzinach. Oto co zrobiłem:

Zasadniczo problem polegał na tym, że obiekt implementujący INotifyPropertyChanged żył w wątku roboczym, a to powoduje problemy podczas uzyskiwania dostępu do wątku interfejsu użytkownika.

Więc to, co zrobiłem, było przekazanie odwołania do obiektu, który należy zaktualizować do obiektu INotifyPropertyChanged, a następnie użyć wywołania na nim.

Oto jak to wygląda:

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

z wątku, wszystko zrobiłem było:

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

Nadzieja to pomaga kogoś !!

+2

To jest trochę brudne, ponieważ oznacza, że ​​obiekt biznesowy zawiera odniesienia do interfejsu użytkownika. Lepiej używać klasy opakowania, zobacz link w drugiej odpowiedzi. – TheCodeKing

+0

Dzięki za aktualizację, ale myślę, że nadal trzeba przekazać odniesienie do obiektu w ten czy inny sposób. 'this.Invoke (new Action (() => ChangeText (text)));' na przykład, potrzebowałoby 'this' do faktycznego sterowania. – TheGateKeeper

+0

Jestem bardzo nowy w tworzeniu wątków, więc to wszystko jest dla mnie bardzo nowe, nie mogę nawet zrozumieć kodu, który łączysz. Myślę, że na razie będę trzymać się swojej drogi. – TheGateKeeper

2

Podobałam się do ostatecznego rozwiązania TheGateKeeper. Jednak wiązałem się z wieloma różnymi przedmiotami. Potrzebowałem czegoś bardziej ogólnego.Rozwiązaniem było stworzenie opakowania, które zaimplementowało również ICustomTypeDescriptor. W ten sposób nie muszę tworzyć właściwości opakowania dla wszystkiego, co może być wyświetlane w interfejsie użytkownika.

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 
} 

Następnie w interfejsie użytkownika, wiążę z tym owijki użyciu coś jak:

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

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

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

MyViewModel ma „DirectiveText” własność i implementuje INotifyPropertyChanged bez szczególną uwagę do gwintowania lub do klas widzenia.

0

Podklasowałem BindingList, więc mogłem sprawdzić, czy wymagany jest Invoke. W ten sposób moje obiekty biznesowe nie mają odniesienia do interfejsu użytkownika.

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); 
    } 
} 
Powiązane problemy