WPF - 可观察排序字典

WPF - Observable Sorted Dictionary

我需要将大量数据推送到 DataGrid(我更喜欢 DataGrid,因为它适合我的要求)。为了使 UI 在加载数据时以及在初始加载完成后数据进入时响应更快,需要相当高的性能。最重要的是,数据需要排序(基于日期的降序)。因为它只是从一个线程并发更新(and/or 不可变)并不是真正需要的(据我所知,并发 and/or 不可变在任何情况下都可能会减慢负载)。因此,出于这个原因,我想实现一个 Observable 性能集合,例如 SortedDictionary

从我所看到的情况来看,上面的内容并不容易获得 - 我看到的选项是 http://www.codeproject.com/Articles/208361/Concurrent-Observable-Collection-Dictionary-and-So but this isn't bindable to a DataGrid (more toward a ListView). The other is http://drwpf.com/blog/2007/09/16/can-i-bind-my-itemscontrol-to-a-dictionary/,它基于字典和手动排序(这似乎违反直觉,因为已经有一个 SortedDictionary - 而且似乎也不容易绑定到 DataGrid。

下面是我的

public class ObservableSortedDictionary<TKey, TValue> : IObservableSortedDictionary<TKey, TValue>
{
    private const string CountString = "Count";
    private const string IndexerName = "Item[]";
    private const string KeysName = "Keys";
    private const string ValuesName = "Values";

    private int _capacity = 0;

    private SortedDictionary<TKey, TValue> _dictionary;
    protected SortedDictionary<TKey, TValue> Dictionary
    {
        get { return _dictionary; }
        private set { _dictionary = value; }
    }

    #region Fields
    private readonly SimpleMonitor _monitor;
    #endregion

    #region Constructors
    public ObservableSortedDictionary(IComparer<TKey> comparer)
    {
        this._monitor = new SimpleMonitor();
        CollectionChanged += new NotifyCollectionChangedEventHandler(ObservableSortedDictionary_CollectionChanged);
        _dictionary = new SortedDictionary<TKey, TValue>(comparer);
    }
    public ObservableSortedDictionary(int capacity, IComparer<TKey> comparer)
    {
        this._monitor = new SimpleMonitor();
        _capacity = capacity;
        CollectionChanged += new NotifyCollectionChangedEventHandler(ObservableSortedDictionary_CollectionChanged);
        _dictionary = (new SortedDictionary<TKey, TValue>(comparer));         }
    public ObservableSortedDictionary(IDictionary<TKey, TValue> dictionary, IComparer<TKey> comparer)
    {
        if (dictionary == null)
        {
            throw new ArgumentNullException("dictionary");
        }
        CollectionChanged += new NotifyCollectionChangedEventHandler(ObservableSortedDictionary_CollectionChanged);
        this._monitor = new SimpleMonitor();
        _dictionary = new SortedDictionary<TKey, TValue>(dictionary, comparer);
    }
    public ObservableSortedDictionary(IDictionary<TKey, TValue> dictionary, IComparer<TKey> comparer, int capacity)
    {
        if (dictionary == null)
        {
            throw new ArgumentNullException("dictionary");
        }
        CollectionChanged += new NotifyCollectionChangedEventHandler(ObservableSortedDictionary_CollectionChanged); 
        this._monitor = new SimpleMonitor();
        _capacity = capacity;
        try
        {
            _dictionary = new SortedDictionary<TKey, TValue>(dictionary, comparer);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
    #endregion

    #region IDictionary<TKey,TValue> Members

    public void Add(TKey key, TValue value)
    {
        Insert(key, value, true);
    }

    public bool ContainsKey(TKey key)
    {
        return Dictionary.ContainsKey(key);
    }

    public ICollection<TKey> Keys
    {
        get { return Dictionary.Keys; }
    }

    public bool Remove(TKey key)
    {
        if (key == null) throw new ArgumentNullException("key");

        CheckReentrancy(); 
        TValue value;
        Dictionary.TryGetValue(key, out value);
        var removed = Dictionary.Remove(key);
        if (removed)
            OnCollectionChanged();

        return removed;
    }


    public bool TryGetValue(TKey key, out TValue value)
    {
        return Dictionary.TryGetValue(key, out value);
    }


    public ICollection<TValue> Values
    {
        get { return Dictionary.Values; }
    }


    public TValue this[TKey key]
    {
        get
        {
            return Dictionary[key];
        }
        set
        {
            Insert(key, value, false);
        }
    }
    #endregion

    #region ICollection<KeyValuePair<TKey,TValue>> Members
    public void Add(KeyValuePair<TKey, TValue> item)
    {
        Insert(item.Key, item.Value, true);
    }

    public void Clear()
    {
        if (Dictionary.Count > 0)
        {
            CheckReentrancy(); 
            Dictionary.Clear();
            OnCollectionChanged();
        }
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return Dictionary.Contains(item);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        Dictionary.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return Dictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return ((IDictionary<TKey, TValue>)Dictionary).IsReadOnly; }
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        return Remove(item.Key);
    }
    #endregion


    #region IEnumerable<KeyValuePair<TKey,TValue>> Members
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return Dictionary.GetEnumerator();
    }
    #endregion

    #region IEnumerable Members
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    #endregion

    #region INotifyCollectionChanged Members
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    #endregion


    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    public void AddRange(IDictionary<TKey, TValue> items)
    {
        if (items == null) throw new ArgumentNullException("items");

        if (items.Count > 0)
        {
            if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
                throw new ArgumentException("An item with the same key has already been added.");
            else
            {
                foreach (var item in items)
                {
                    Dictionary.Add(item.Key, item.Value);
                    OnPropertyChanged();
                    OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(item.Key, item.Value));
                }
            }
        }
    }

    private void Insert(TKey key, TValue value, bool add)
    {
        if (key == null) throw new ArgumentNullException("key");

        CheckReentrancy(); 
        TValue item;
        if (Dictionary.TryGetValue(key, out item))
        {
            if (add) throw new ArgumentException("An item with the same key has already been added.");
            if (Equals(item, value)) return;
            Dictionary[key] = value;

            OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
        }
        else
        {
            Dictionary[key] = value;
            OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
            if (_capacity > 0 && Dictionary.Count > _capacity)
            {
                Dictionary.Remove(Dictionary.Keys.Last());
                OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
            }
        }
    }

    #region SimpleMonitor
    protected IDisposable BlockReentrancy()
    {
        this._monitor.Enter();
        return this._monitor;
    }

    protected void CheckReentrancy()
    {
        if ((this._monitor.Busy && (CollectionChanged != null)) && (CollectionChanged.GetInvocationList().Length > 1))
        {
            throw new InvalidOperationException("Collection Reentrancy Not Allowed");
        }
    }

    [Serializable]
    private class SimpleMonitor : IDisposable
    {
        private int _busyCount;

        public bool Busy
        {
            get { return this._busyCount > 0; }
        }

        public void Enter()
        {
            this._busyCount++;
        }

        #region Implementation of IDisposable

        public void Dispose()
        {
            this._busyCount--;
        }

        #endregion
    }
    #endregion

    private void OnPropertyChanged()
    {
        OnPropertyChanged(CountString);
        OnPropertyChanged(IndexerName);
        OnPropertyChanged(KeysName);
        OnPropertyChanged(ValuesName);
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private void OnCollectionChanged()
    {
        OnPropertyChanged();
        if (CollectionChanged != null) using (BlockReentrancy()) { CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));}
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
    {
        OnPropertyChanged();
        if (CollectionChanged != null) using (BlockReentrancy()) { CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem));}
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
    {
        OnPropertyChanged();
        if (CollectionChanged != null) using (BlockReentrancy()) {CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));}
    }


    private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems)
    {
        OnPropertyChanged();
        if (CollectionChanged != null) using (BlockReentrancy()) {CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems));}
    }

    void ObservableSortedDictionary_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (Object item in e.NewItems)
            {
                ((KeyValuePair<TickerKey, TickerViewModel>)(item)).Value.PropertyChanged += ObservableSortedDictionary_PropertyChanged;                 }
        }
        if (e.OldItems != null)
        {
            foreach (Object item in e.OldItems)
            {
                ((KeyValuePair<TickerKey, TickerViewModel>)(item)).Value.PropertyChanged -= ObservableSortedDictionary_PropertyChanged;                 }
        }
    }

    void ObservableSortedDictionary_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //if (e.PropertyName == "Dictionary")
            OnPropertyChanged("Dictionary");
    }
}

public interface IObservableSortedDictionary<TKey, TValue> : IDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged, IEnumerable<KeyValuePair<TKey, TValue>>
{
}

视图模型

    //...
    private ObservableSortedDictionary<TickerKey, TickerViewModel> _tickersData;
    public ObservableSortedDictionary<TickerKey, TickerViewModel> TickersData
    {
        get
        {
            return _tickersData;
        }
        set
        {
            if (value != _tickersData)
            {
                _tickersData = value;
                OnPropertyChanged("TickersData");
            }
        }
    }
    //...
    if (TickersData == null)
    {
        TickerComparer comparer = new TickerComparer();
        TickersData = new ObservableSortedDictionary<TickerKey, TickerViewModel>(_tickersInsert, comparer, 50);
    }
    else
    {
        TickersData.AddRange(_tickersInsert);
        foreach (var item in _tickersInsert) TickersData.Add(item.Key, item.Value);
    }
    //...

景观 (XAML)

//...
<DataGrid FontSize="9" x:Name="Ticker1Tickers" IsReadOnly="True" ItemsSource="{Binding TickersData.Values}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="Auto" Width="Auto">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding TickerPrice}" Header="Price" Width="50"/>
        <DataGridTextColumn Binding="{Binding TickerVolume}" Header="Volume" Width="50" />
        <DataGridTextColumn Binding="{Binding TickerTimeMilliSecondsSinceMidnight, Converter={StaticResource mmSsFormatConverter}, StringFormat=\{0:hh:mm:ss tt\}}" Header="Time" Width="70" />
    </DataGrid.Columns>
</DataGrid>
//...

一些注意事项

有没有人尝试过类似上述的事情and/or可以看出通知问题可能在哪里?

更新 根据 Pieter 的建议,我将 XAML DataGrid 定义更改为

<DataGrid FontSize="9" x:Name="Ticker1Tickers" IsReadOnly="True" ItemsSource="{Binding TickersData}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="Auto" Width="Auto">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Value.TickerPrice}" Header="Price" Width="50"/>
        <DataGridTextColumn Binding="{Binding Value.TickerVolume}" Header="Volume" Width="50" />
        <DataGridTextColumn Binding="{Binding Value.TickerTimeMilliSecondsSinceMidnight, Converter={StaticResource mmSsFormatConverter}, StringFormat=\{0:hh:mm:ss tt\}}" Header="Time" Width="70" />
    </DataGrid.Columns>
</DataGrid>

感谢您在此 Pieter 上的帮助。 refresh/sort 我从 ObservableSortedDictionary 中找到的 DataGrid 解决方案(阅读 http://programmer.wrighton.org/2009/01/wpf-datagrid-items-refresh.html 中的一条评论后)是

CollectionViewSource.GetDefaultView(TickersData).Refresh();

希望以上声明不会造成太多资源开销

我还有一个与 OnCollectionChanged 相关的问题 - NotifyCollectionChangedEventArgs 需要 SortedDictionary 的索引(而不是项目),它不是开箱即用的(使用 SortedList 更容易,但无论如何).

但现在一切正常 - 工作正常