当底层模型数据更改时,通知ViewModel中定义的属性更改

本文关键字:ViewModel 定义 属性 通知 模型 数据 | 更新日期: 2023-09-27 18:11:46

问题:在列表中单个项目的属性值发生变化后,如何更新有界UI元素以显示仅在ViewModel中定义的属性值?

当在一个类中实现INotifyPropertyChanged时,它将成为列表中的项,它只更新特定数据块绑定到的UI元素。比如ListView项或DataGrid单元格。这很好,这就是我们想要的。但如果我们需要一个总计行,就像在Excel表格中一样。当然,有几种方法可以解决这个特定的问题,但这里的潜在问题是在ViewModel中根据来自Model的数据定义和计算属性的时间。例如:

public class ViewModel
{
   public double OrderTotal => _model.order.OrderItems.Sum(item => item.Quantity * item.Product.Price);
}

何时以及如何得到通知/更新/调用?

让我们用一个更完整的例子来试试。

这里是XAML
<Grid>
    <DataGrid x:Name="GrdItems" ... ItemsSource="{Binding Items}"/>
    <TextBox x:Name="TxtTotal" ... Text="{Binding ItemsTotal, Mode=OneWay}"/>
</Grid>

这是模型:

public class Item : INotifyPropertyChanged
{
    private string _name;
    private int _value;
    public string Name
    {
        get { return _name; }
        set
        {
            if (value == _name) return;
            _name = value;
            OnPropertyChanged();
        }
    }
    public int Value
    {
        get { return _value; }
        set
        {
            if (value.Equals(_value)) return;
            _value = value;
            OnPropertyChanged();
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new propertyChangedEventArgs(propertyName));
    }
}
public class Model
{
    public List<Item> Items { get; set; } = new List<Item>();
    public Model()
    {
        Items.Add(new Item() { Name = "Item A", Value = 100 });
        Items.Add(new Item() { Name = "Item b", Value = 150 });
        Items.Add(new Item() { Name = "Item C", Value = 75 });
    }
}

和ViewModel:

public class ViewModel
{
    private readonly Model _model = new Model();
    public List<Item> Items => _model.Items;
    public int ItemsTotal => _model.Items.Sum(item => item.Value);
}

我知道这段代码看起来过于简化了,但它是一个更大的、令人沮丧的、困难的应用程序的一部分。

所有我想做的是当我改变一个项目的值在DataGrid我想要itemtotal属性更新texttotal文本框。

到目前为止,我找到的解决方案包括使用ObservableCollection和实现CollectionChanged事件。

模型变为:

public class Model: INotifyPropertyChanged
{
    public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
    public Model()
    {
        Items.CollectionChanged += ItemsOnCollectionChanged;
    }
    .
    .
    .
    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (Item item in e.NewItems)
                item.PropertyChanged += MyType_PropertyChanged;
        if (e.OldItems != null)
            foreach (Item item in e.OldItems)
                item.PropertyChanged -= MyType_PropertyChanged;
    }
    void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Value")
            OnPropertyChanged(nameof(Items));
    }
    public event PropertyChangedEventHandler PropertyChanged;
    .
    .
    .
}

并且视图模型变为:

public class ViewModel : INotifyPropertyChanged
{
    private readonly Model _model = new Model();
    public ViewModel()
    {
        _model.PropertyChanged += ModelOnPropertyChanged;
    }
    private void ModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
    {
        OnPropertyChanged(nameof(ItemsTotal));
    }
    public ObservableCollection<Item> Items => _model.Items;
    public int ItemsTotal => _model.Items.Sum(item => item.Value);

    public event PropertyChangedEventHandler PropertyChanged;
    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

这个解决方案是有效的,但它看起来只是一个应该有一个更雄辩的实现的绕过黑客的工作。我的项目在视图模型中有几个这样的sum属性,就目前而言,有很多属性要更新,有很多代码要写,感觉开销更大。

我还有更多的研究要做,当我写这个问题的时候,出现了几篇有趣的文章。我将更新这篇文章,提供其他解决方案的链接,因为这个问题似乎比我想象的更普遍。

当底层模型数据更改时,通知ViewModel中定义的属性更改

虽然你的项目看起来像是MVVM,但我不认为它实际上是。是的,你有层,但你的模型和视图模型是交换责任。在MVVM情况下保持纯粹的一种方法是永远不要将INotifyPropertyChanged放在视图模型之外的任何东西上。如果你发现自己把它放在一个模型中,那么你的模型就会被视图模型的责任所破坏。视图也是如此(尽管人们不太倾向于将INotifyPropertyChanged堆栈到视图上)。它还有助于打破视图与单个视图模型相关联的假设。这感觉就像MVC思维的交叉。

所以我说的是,你有一个结构性问题,从概念上开始。例如,视图模型没有理由不能有子视图模型。事实上,当我有一个强大的对象层次结构时,我经常发现这很有用。你会有Item和ItemViewModel。不管你的父对象是什么(比如parent)和ParentViewModel。ParentViewModel将具有ItemViewModel类型的可观察集合,并且它将订阅其子的OnPropertyChanged事件(这将为总属性触发OnPropertyChanged)。这样,ParentViewModel既可以提醒UI属性更改,又可以确定该更改是否也需要反映在Parent模型中(有时您希望将聚合存储在父数据中,有时不希望)。计算的字段(如总数)通常只出现在ViewModel中。

简而言之,您的ViewModels处理协调。你的ViewModel是模型数据的主人,对象之间的通信应该发生在ViewModel到ViewModel之间,而不是通过模型。这意味着你的UI可以有一个父视图和一个单独定义的子视图,并保持这些隔离的工作,因为它们通过它们的绑定视图模型进行通信。

明白了吗?

它看起来像:

public class ParentViewModel : INotifyPropertyChanged
{
    private readonly Model _model;
    public ParentViewModel(Model model)
    {
        _model = model;
        Items = new ObservableCollection<ItemViewModel>(_model.Items.Select(i => new ItemViewModel(i)));
        foreach(var item in Items)
        {
            item.PropertyChanged += ChildOnPropertyChanged;
        }
        Items.CollectionChanged += ItemsOnCollectionChanged;
    }
    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (Item item in e.NewItems)
                item.PropertyChanged += ChildOnPropertyChanged;
        if (e.OldItems != null)
            foreach (Item item in e.OldItems)
                item.PropertyChanged -= ChildOnPropertyChanged;
        OnPropertyChanged(nameof(ItemsTotal));
    }
    private void ChildOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
    {
        if (e.PropertyName == "Value")
            OnPropertyChanged(nameof(ItemsTotal));
    }
    public ObservableCollection<ItemViewModel> Items;
    public int ItemsTotal => Items.Sum(item => item.Value);

    public event PropertyChangedEventHandler PropertyChanged;
    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

这是复杂的,但至少所有的复杂性都包含在您的ViewModel中,并从那里协调

这似乎对我的目的足够好。我相信这个解决方案,以各种其他形式,可以在互联网上找到。

public class ChildNotifier<T> : INotifyPropertyChanged where T : INotifyPropertyChanged
{
    private ObservableCollection<T> _list;
    public ObservableCollection<T> List
    {
        get { return _list; }
        set
        {
            if (Equals(value, _list)) return;
            _list = value;
            foreach (T item in _list)
                item.PropertyChanged += ChildOnPropertyChanged;
            OnPropertyChanged();
        }
    }
    protected ChildNotifier(IEnumerable<T> list)
    {
        _list = new ObservableCollection<T>(list);
        _list.CollectionChanged += ItemsOnCollectionChanged;
        foreach (T item in _list)
            item.PropertyChanged += ChildOnPropertyChanged;
    }
    protected abstract void ChildOnPropertyChanged(object sender, propertyChangedEventArgs e);
    private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (T item in e.NewItems)
                item.PropertyChanged += ChildOnPropertyChanged;
        if (e.OldItems != null)
            foreach (T item in e.OldItems)
                item.PropertyChanged -= ChildOnPropertyChanged;
        OnPropertyChanged();
    }
    public event PropertyChangedEventHandler PropertyChanged;
    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}