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

Tra*_*vis 6 c# wpf xaml mvvm

问题:在列表中单个项目中的属性更改值后,更新有界 UI 元素以显示仅在 ViewModel 中定义的属性值的正确方法是什么。

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

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

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

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

这是 XAML

<Grid>
    <DataGrid x:Name="GrdItems" ... ItemsSource="{Binding Items}"/>
    <TextBox x:Name="TxtTotal" ... Text="{Binding ItemsTotal, Mode=OneWay}"/>
</Grid>
Run Code Online (Sandbox Code Playgroud)

这是模型:

<Grid>
    <DataGrid x:Name="GrdItems" ... ItemsSource="{Binding Items}"/>
    <TextBox x:Name="TxtTotal" ... Text="{Binding ItemsTotal, Mode=OneWay}"/>
</Grid>
Run Code Online (Sandbox Code Playgroud)

和视图模型:

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 });
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我想要做的就是当我在 DataGrid 中更改项目的值时,我希望 ItemsTotal 属性更新 TxtTotal 文本框。

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

模型变为:

public class ViewModel
{
    private readonly Model _model = new Model();

    public List<Item> Items => _model.Items;
    public int ItemsTotal => _model.Items.Sum(item => item.Value);
}
Run Code Online (Sandbox Code Playgroud)

并且视图模型更改为:

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;

    .
    .
    .

}
Run Code Online (Sandbox Code Playgroud)

这个解决方案有效,但它似乎是一个解决 hack 的方法,应该有一个更雄辩的实现。我的项目在视图模型中有几个这样的 sum 属性,就我而言,有很多要更新的属性和很多要编写的代码,只是感觉开销更大。

我有更多的研究要做,在我写这个问题的时候出现了几篇有趣的文章。我会用指向其他解决方案的链接更新这篇文章,因为这个问题似乎比我想象的更常见。

Jac*_*itt 4

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

所以我想说的是,你有一个从概念上开始的结构性问题。例如,视图模型没有理由不能有子视图模型。事实上,当我有很强的对象层次结构时,我经常发现这很有用。所以你会有 Item 和 ItemViewModel。无论您的父对象是什么(例如,Parent)和 ParentViewModel。ParentViewModel 将具有 ItemViewModel 类型的可观察集合,并且它将订阅其子级的 OnPropertyChanged 事件(这将为整个属性触发 OnPropertyChanged)。这样,ParentViewModel 既可以向 UI 发出属性更改警报,又可以确定该更改是否也需要反映在父模型中(​​有时您希望将聚合存储在父数据中,有时则不需要)。计算字段(如总计)通常仅出现在 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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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