WPF视图模型列表绑定到模型对象列表

3
在这个模型中,我有:
public ObservableCollection<Item> Items { get; private set; }

在ViewModel中,我有一个对应的ItemViewModels列表。我希望这个列表能够双向绑定到模型的列表上:

public ObservableCollection<ItemViewModel> ItemViewModels ...

在XAML中,我将绑定(在这种情况下是TreeView)到ItemViewModels属性。我的问题是,在上面显示的ViewModel中,“…”中应该放什么?我希望有一两行代码来绑定这两个ObservableCollections(为每个模型对象提供要构造的ViewModel类型)。然而,我担心必须编写大量代码来处理Items.CollectionChanged事件,并通过必要的构造ViewModel手动更新ItemViewModels列表,以及相应的反向操作,以根据ItemViewModels的更改更新Items集合。谢谢!Eric
2个回答

5
您可以使用以下类:
public class BoundObservableCollection<T, TSource> : ObservableCollection<T>
{
    private ObservableCollection<TSource> _source;
    private Func<TSource, T> _converter;
    private Func<T, TSource, bool> _isSameSource;

    public BoundObservableCollection(
        ObservableCollection<TSource> source,
        Func<TSource, T> converter,
        Func<T, TSource, bool> isSameSource)
        : base()
    {
        _source = source;
        _converter = converter;
        _isSameSource = isSameSource;

        // Copy items
        AddItems(_source);

        // Subscribe to the source's CollectionChanged event
        _source.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_source_CollectionChanged);
    }

    private void AddItems(IEnumerable<TSource> items)
    {
        foreach (var sourceItem in items)
        {
            Add(_converter(sourceItem));
        }
    }

    void _source_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                AddItems(e.NewItems.Cast<TSource>());
                break;
            case NotifyCollectionChangedAction.Move:
                // Not sure what to do here...
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (var sourceItem in e.OldItems.Cast<TSource>())
                {
                    var toRemove = this.First(item => _isSameSource(item, sourceItem));
                    this.Remove(toRemove);
                }
                break;
            case NotifyCollectionChangedAction.Replace:
                for (int i = e.NewStartingIndex; i < e.NewItems.Count; i++)
                {
                    this[i] = _converter((TSource)e.NewItems[i]);
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                this.Clear();
                this.AddItems(_source);
                break;
            default:
                break;
        }
    }
}

使用方法如下:

var models = new ObservableCollection<Model>();
var viewModels =
    new BoundObservableCollection<ViewModel, Model>(
        models,
        m => new ViewModel(m), // creates a ViewModel from a Model
        (vm, m) => vm.Model.Equals(m)); // checks if the ViewModel corresponds to the specified model
BoundObservableCollection将在ObservableCollection更改时更新,但反过来不会(您需要覆盖一些方法才能实现)。

1
谢谢!我知道有人已经封装了这个功能。一个问题:当更改操作是Remove时,我看到你使用isSameSource。为什么不直接使用e.OldStartingIndex和e.OldItems.Count: for (int i = 0; i < e.OldItems.Count; i++) this.RemoveAt(e.OldStartingIndex); - Eric
1
@Eric,是的,它可能起作用,而且如果您确信两个集合的顺序相同,实际上会更好。由于我没有处理NotifyCollectionChangedAction.Move情况,这就不能保证了... - Thomas Levesque
为什么不直接使用 this.Move(e.OldStartingIndex,e.NewStartingIndex) 来移动呢?这样两个集合就会以相同的方式排序。即使通常情况下不需要,但如果两个列表都保持相同的顺序,调试起来会更容易。 - Ignacio Soler Garcia

2

是的,你所担心的是真的,你需要将所有的 ObservableCollection 功能进行封装。

不过我的问题是,为什么你想要在已经很好的模型周围再加一个视图模型包装层呢?如果你的数据模型基于一些不可绑定的业务逻辑,则视图模型非常有用。通常这个业务/数据层有一种或两种获取数据并通知外部观察者其更改的方式,这些方式可以轻松地由视图模型处理,并转换为对 ObservableCollection 的更改。事实上,在 .NET 3.5 中,ObservableCollectionWindowsBase.dll 的一部分,因此通常不应该首先在数据模型中使用它。

我的建议是,要么将填充/修改 ObservableCollection 的逻辑从数据模型移动到视图模型中,要么直接将当前称为数据模型的层绑定到它,并仅仅称之为一个视图模型。

你显然可以编写一个帮助类,使用一些转换器 lambda 来同步两个集合(从 ItemItemViewModel 和反向转换),并在像这样的地方使用它(但一定要确保正确处理项目的唯一性),但在我看来,这种方法会产生冗余的包装类,并且每一层都会降低功能并增加复杂性。这正好与 MVVM 的目标相反。


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