2012-02-22 94 views
11

我正試圖在集合中的對象上掛鉤事件INotifyPropertyChanged觀察PropertyChanged集合中的項目

,我見過這個問題每一個答案,說按如下方式處理:

void NotifyingItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
{ 
    if(e.NewItems != null) 
    { 
     foreach(INotifyPropertyChanged item in e.NewItems) 
     { 
      item.PropertyChanged += new PropertyChangedEventHandler(CollectionItemChanged); 
     } 
    } 
    if(e.OldItems != null) 
    { 
     foreach(ValidationMessageCollection item in e.OldItems) 
     { 
      item.PropertyChanged -= CollectionItemChanged; 
     } 
    } 
} 

我的問題是當一個開發商呼籲NotifyingItems收集Clear(),這完全失敗。當發生這種情況時,這個事件處理程序調用e.Action == Resete.NewItemse.OldItems等於null(我期望後者包含所有項目)。

問題是那些物品不會消失,它們不會被破壞,它們不再被當前課程監控 - 但是因爲我從來沒有機會取消它們的PropertyChangedEventHandler - 他們保留即使它們已從我的NotifyingItems列表中清除,也會調用我的CollectionItemChanged處理程序。這種情況應該如何用這種「完善的」模式來處理?

+1

[清除ObservableCollection時,e.OldItems中沒有項目]的可能重複(http://stackoverflow.com/questions/224155/when-clearing-an-observablecollection-there-are-no-items- in-e-olditems) – Rachel 2012-02-22 16:58:32

回答

2

終極解決方案發現

我已經找到了一個解決方案,讓用戶既利用增加或一次刪除多個項目,而只發射一個事件的效率 - 並滿足UI元素的需求獲取Action.Reset事件參數,而所有其他用戶都希望添加和刪除元素列表。

此解決方案涉及重寫CollectionChanged事件。當我們開始討論這個事件時,我們實際上可以查看每個註冊處理程序的目標並確定它們的類型。由於只有ICollectionView類需要NotifyCollectionChangedAction.Reset參數,當多個項目發生更改時,我們可以將它們單獨出來,並向其他人提供適當的事件參數,其中包含已刪除或添加的項目的完整列表。以下是實施。

public class BaseObservableCollection<T> : ObservableCollection<T> 
{ 
    //Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear() 
    private bool _SuppressCollectionChanged = false; 

    /// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args. 
    public override event NotifyCollectionChangedEventHandler CollectionChanged; 

    public BaseObservableCollection() : base(){} 
    public BaseObservableCollection(IEnumerable<T> data) : base(data){} 

    #region Event Handlers 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
    { 
     if(!_SuppressCollectionChanged) 
     { 
      base.OnCollectionChanged(e); 
      if(CollectionChanged != null) 
       CollectionChanged.Invoke(this, e); 
     } 
    } 

    //CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than 
    //one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable 
    //for applications in code, so we actually check the type we're notifying on and pass a customized event args. 
    protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e) 
    { 
     NotifyCollectionChangedEventHandler handlers = this.CollectionChanged; 
     if(handlers != null) 
      foreach(NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList()) 
       handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
    } 
    #endregion 

    #region Extended Collection Methods 
    protected override void ClearItems() 
    { 
     if(this.Count == 0) return; 

     List<T> removed = new List<T>(this); 
     _SuppressCollectionChanged = true; 
     base.ClearItems(); 
     _SuppressCollectionChanged = false; 
     OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
    } 

    public void Add(IEnumerable<T> toAdd) 
    { 
     if(this == toAdd) 
      throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified."); 

     _SuppressCollectionChanged = true; 
     foreach(T item in toAdd) 
      Add(item); 
     _SuppressCollectionChanged = false; 
     OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd))); 
    } 

    public void Remove(IEnumerable<T> toRemove) 
    { 
     if(this == toRemove) 
      throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified."); 

     _SuppressCollectionChanged = true; 
     foreach(T item in toRemove) 
      Remove(item); 
     _SuppressCollectionChanged = false; 
     OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove))); 
    } 
    #endregion 
} 

感謝大家的建議和鏈接。如果沒有看到其他人想出的所有漸進式更好的解決方案,我都不會想到這一點。

+0

感謝您的解決方案阿蘭。但是我發現了一個小錯誤。在「添加」和「刪除」的方法中,您在參數中迭代兩次IEnumerable。因此,例如如果該IEnumerable會創建對象,它們將被創建兩次。要簡單地緩存它之前會做的伎倆,像這樣:var toAddList = toAdd as IList ?? toAdd.ToList();無論如何,你最後在可枚舉中創建一個列表。 – FrankyB 2014-03-11 12:53:36

+0

@FrankyB你是對的。這是我在ReSharper向我展示我的方式錯誤之前的早期日子:) – Alain 2014-03-11 14:19:48

5

也許看看this answer

它建議不使用.Clear()和實施.RemoveAll()擴展方法,將刪除的項目一個接一個

public static void RemoveAll(this IList list) 
{ 
    while (list.Count > 0) 
    { 
     list.RemoveAt(list.Count - 1); 
    } 
} 

如果不爲你工作,在鏈接中還有其他很好的解決方案。

+0

謝謝,這實際上看起來像這個問題的確切副本,只是更好的措辭。 – Alain 2012-02-22 16:55:23

+0

我看到你自己實際上遇到了這個問題。 [鏈接](http://stackoverflow.com/questions/7449196/how-can-i-raise-a-collectionchanged-event-on-an-observablecollection-and-pass-i)你有沒有找到一種方式來處理如果沒有必要發射數百個屬性改變的事件,那麼這個散點清晰? (IE,爲UIElements提出一個「清除」事件,併爲其他事件提出刪除事件?) – Alain 2012-02-22 20:05:57

+0

@Alain我從未做過。相反,當'.AddRange()'或'.RemoveRange()'的執行時間過長時,我完全重新創建了該集合。通常我在集合的'set'方法中有一些東西去鉤住舊集合的所有事件處理程序,以及連接新集合的所有事件處理程序。這絕對不是一個理想的解決方案,但它的工作。 – Rachel 2012-02-22 20:39:07

0

重置不提供更改的項目。如果您繼續使用「清除」,則需要維護單獨的收集以清除事件。

更簡單,更有效的解決方案是創建自己的清除功能並刪除每個項目而不是調用集合的清除。

void ClearCollection() 
    { 
     while(collection.Count > 0) 
     { 
      // Could handle the event here... 
      // collection[0].PropertyChanged -= CollectionItemChanged; 
      collection.RemoveAt(collection.Count -1); 
     } 
    } 
+0

這個解決方案的唯一問題是,正如問題中所暗示的那樣,這個類和集合被其他開發人員使用,並且沒有任何關於此代碼的內容允許我強制其他開發人員不要在集合上使用「Clear()」 - 該方法在那裏,他們喜歡它。如果有的話,它會表現爲一個非常難以診斷運行時錯誤。 – Alain 2012-02-22 17:21:31

+0

創建一個新的繼承類並覆蓋這些函數實際上是您唯一的解決方案。但是你已經得出結論,祝你好運。 – JeremyK 2012-02-22 18:54:38

1

我通過使我自己的ObservableCollection<T>子類覆蓋ClearItems方法解決了這個問題。在調用基礎實現之前,它會引發一個CollectionChanging事件,該事件是我在我的類中定義的。

CollectionChanging集合實際上被清除之前觸發,因此您有機會訂閱事件並取消訂閱事件。

例子:

public event NotifyCollectionChangedEventHandler CollectionChanging; 

protected override void ClearItems() 
{ 
    if (this.Items.Count > 0) 
    { 
     this.OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
    } 

    base.ClearItems(); 
} 

protected virtual void OnCollectionChanging(NotifyCollectionChangedEventArgs eventArgs) 
{ 
    if (this.CollectionChanging != null) 
    { 
     this.CollectionChanging(this, eventArgs); 
    } 
} 
+0

這是一個有效的解決方案,儘管我正在努力尋求一種不需要其他開發人員「永遠記得處理我發明的這個新事件或者它不起作用的事件。」這些不能在編譯時強制執行的規則在具有多個開發人員的項目中實際上並不適用。 – Alain 2012-02-22 17:23:01

+0

那麼,你總是可以根據我上面提供的內容創建自己的集合類型,這樣當元素被移除或集合被清除時,內部負責取消訂閱 – RobSiklos 2012-02-22 18:00:49

1

編輯:此解決方案不雷切爾鏈接到問題的工作

This solution看起來是絢麗:

如果我代替我NotifyingItems帶有覆蓋可覆蓋集合的繼承類的ObservableCollection。ClearItems()方法,那麼我可以攔截NotifyCollectionChangedEventArgs,並代替復位操作刪除替換它,並通過刪除的項目清單:

//Makes sure on a clear, the list of removed items is actually included. 
protected override void ClearItems() 
{ 
    if(this.Count == 0) return; 

    List<T> removed = new List<T>(this); 
    base.ClearItems(); 
    base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
} 

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
{ 
    //If the action is a reset (from calling base.Clear()) our overriding Clear() will call OnCollectionChanged, but properly. 
    if(e.Action != NotifyCollectionChangedAction.Reset) 
     base.OnCollectionChanged(e); 
} 

輝煌,並且需要除了在任何地方什麼都沒有改變我自己的班級。


*編輯*

我喜歡這個解決方案,但它不起作用 ...你不能提出一個NotifyCollectionChangedEventArgs有多個項目改變除非該行動是「重置」。您將得到以下運行時異常:Range actions are not supported。我不知道爲什麼它必須對此過於挑剔,但現在除了每次刪除每個項目之外別無選擇,每個項目都有一個新的CollectionChanged事件。真是一個該死的麻煩。

+0

所以我的答案是? :P – JeremyK 2012-02-23 01:31:34

+0

我想出了一個解決上述運行時異常的方法,該異常由CollectionView類(所有項目列表UIElements使用)引發。解決方法發佈如下:http://stackoverflow.com/a/9416568/529618 – Alain 2012-02-23 16:00:38