MVVM同步集合

55

是否有一种标准化的方法在C#和WPF中同步一组Model对象与一组匹配的ModelView对象?我正在寻找某种类,它可以保持以下两个集合同步,假设我只有几个苹果并且我可以将它们全部保存在内存中。

换句话说,如果我向Apples集合添加一个Apple,则希望将一个AppleModelView添加到AppleModelViews集合中。我可以通过侦听每个集合的CollectionChanged事件来编写自己的代码。这似乎是一个常见的场景,有人比我更聪明地定义了“正确”的方法来处理它。

public class BasketModel
{
    public ObservableCollection<Apple> Apples { get; }
}

public class BasketModelView
{
    public ObservableCollection<AppleModelView> AppleModelViews { get; }
}

我不完全理解你的问题。今天可能有点慢,你可能需要重新陈述一下。 - Shaun Bowe
我在上方添加了一个新的段落,希望有所帮助。 - Jake Pearson
为什么要有单独的集合?Apple 可以是 AppleModelView 的子集,然后根据你获得 Apple 的方式只填充 AppleModelView 中相关的部分。一般来说,我会将模型完全从 WPF 中分离出来,仅保留 ViewModel。模型是数据库或其他实体。 - markmnl
在绑定过程中,您还可以使用值转换器将 Apple 转换为 AppleModelView,具体取决于您是否想在其他地方重用 AppleModelView。 - MikeT
11个回答

-1
这是 Sam Harwell 答案 的轻微变化,实现了 IReadOnlyCollection<>INotifyCollectionChanged,而不是直接继承自 ObservableCollection<>。 这样可以防止使用者修改集合,在这种情况下通常是不必要的。
此实现还使用 CollectionChangedEventManager 将事件处理程序附加到源集合,以避免在源集合没有同时释放时出现内存泄漏的情况。
/// <summary>
/// A collection that mirrors an <see cref="ObservableCollection{T}"/> source collection 
/// with a transform function to create it's own elements.
/// </summary>
/// <typeparam name="TSource">The type of elements in the source collection.</typeparam>
/// <typeparam name="TDest">The type of elements in this collection.</typeparam>
public class MappedObservableCollection<TSource, TDest>
    : IReadOnlyCollection<TDest>, INotifyCollectionChanged
{
    /// <inheritdoc/>
    public int Count => _mappedCollection.Count;

    /// <inheritdoc/>
    public event NotifyCollectionChangedEventHandler CollectionChanged {
        add { _mappedCollection.CollectionChanged += value; }
        remove { _mappedCollection.CollectionChanged -= value; }
    }

    private readonly Func<TSource, TDest> _elementMapper;
    private readonly ObservableCollection<TDest> _mappedCollection;

    /// <summary>
    /// Initializes a new instance of the <see cref="MappedObservableCollection{TSource, TDest}"/> class.
    /// </summary>
    /// <param name="sourceCollection">The source collection whose elements should be mapped into this collection.</param>
    /// <param name="elementMapper">Function to map elements from the source collection to this collection.</param>
    public MappedObservableCollection(ObservableCollection<TSource> sourceCollection, Func<TSource, TDest> elementMapper)
    {
        if (sourceCollection == null) throw new ArgumentNullException(nameof(sourceCollection));
        _mappedCollection = new ObservableCollection<TDest>(sourceCollection.Select(elementMapper));

        _elementMapper = elementMapper ?? throw new ArgumentNullException(nameof(elementMapper));

        // Update the mapped collection whenever the source collection changes
        // NOTE: Use the weak event pattern here to avoid a memory leak
        // See: https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/weak-event-patterns
        CollectionChangedEventManager.AddHandler(sourceCollection, OnSourceCollectionChanged);
    }

    /// <inheritdoc/>
    IEnumerator<TDest> IEnumerable<TDest>.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <summary>
    /// Mirror a change event in the source collection into the internal mapped collection.
    /// </summary>
    private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action) {
            case NotifyCollectionChangedAction.Add:
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                break;
            case NotifyCollectionChangedAction.Replace:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Reset:
                _mappedCollection.Clear();
                InsertItems(e.NewItems, 0);
                break;
            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1) {
                    _mappedCollection.Move(e.OldStartingIndex, e.NewStartingIndex);
                } else {
                    RemoveItems(e.OldItems, e.OldStartingIndex);

                    var movedItems = _mappedCollection.Skip(e.OldStartingIndex).Take(e.OldItems.Count).GetEnumerator();
                    for (int i = 0; i < e.OldItems.Count; i++) {
                        _mappedCollection.Insert(e.NewStartingIndex + i, movedItems.Current);
                        movedItems.MoveNext();
                    }
                }

                break;
        }
    }

    private void InsertItems(IList newItems, int newStartingIndex)
    {
        for (int i = 0; i < newItems.Count; i++)
            _mappedCollection.Insert(newStartingIndex + i, _elementMapper((TSource)newItems[i]));
    }

    private void RemoveItems(IList oldItems, int oldStartingIndex)
    {
        for (int i = 0; i < oldItems.Count; i++)
            _mappedCollection.RemoveAt(oldStartingIndex);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接