当底层模型数据发生更改时,在ViewModel中定义通知属性更改。

6
问题:当列表中单个项目的属性更改值后,更新有界 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);
}

何时以及如何进行通知/更新/调用?

让我们通过一个更完整的示例来尝试。

这是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中更改项目的值时,使ItemsTotal属性更新TxtTotal文本框。

到目前为止,我找到的解决方案包括使用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));
    }
}

这个解决方案可以使用,但它似乎只是一个权宜之计,应该有更加优雅的实现方式。在我的项目中,视图模型中有很多这样的求和属性,目前而言,这需要更新很多属性和编写大量代码,感觉增加了很多开销。
我还需要做更多的研究,写这篇问题时出现了几篇有趣的文章。如果这个问题比我想象的更常见,我会在此帖子中更新其他解决方案的链接。

这在XAML中一直是一个有点困难的问题。我想你正在寻找类似于ko.computed的东西。以下是一些做类似事情的文章:http://philchuang.com/mvvm-pain-points-chaineddependentcalculated-properties/ http://www.codeproject.com/Articles/375192/WPF-The-calculated-property-dependency-problem http://www.codeproject.com/Articles/422581/Knockout-Style-Observables-in-XAML - Filip Cordas
2个回答

4
虽然你的项目看起来像是使用了MVVM,但我认为它实际上并不是。是的,你有层,但是你的模型和ViewModel交换了职责。在MVVM环境中保持纯洁的一个方法是从未将INotifyPropertyChanged放在除了ViewModel以外的任何地方。如果你发现自己在模型中加入它,那么你的模型正在被ViewModel职责污染。视图也一样(尽管人们不太容易把INotifyPropertyChanged堆叠到视图中)。此外,打破你一个视图与单个ViewModel相关联的假设也会有所帮助。这感觉像是跨越了MVC思维的界限。
所以我想说的是,你有一个从概念上开始的结构问题。例如,一个ViewModel可以有一个子ViewModel。实际上,当我有一个强层次结构的对象时,我经常发现这很有用。所以你会有Item和ItemViewModel,以及你的父对象(比如Parent)和ParentViewModel。ParentViewModel将具有类型为ItemViewModel的可观察集合,并订阅其子项的OnPropertyChanged事件(这将为总属性触发OnPropertyChanged)。这样,ParentViewModel既可以警告UI属性更改,还可以确定该更改是否需要反映在Parent模型中(有时你想在父数据中存储聚合,有时不想)。计算字段(如总数)通常仅出现在ViewModel中。
简而言之,你的ViewModel处理协调。你的ViewModel是模型数据的主人,对象之间的通信应该从ViewModel到ViewModel而不是通过模型。这意味着你的UI可以拥有一个父视图和一个单独定义的子视图,并且保持它们隔离是有效的,因为它们通过它们绑定的ViewModel进行通信。
这样看起来会像这样:
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中,并从那里协调。


Jacob- 非常有趣,尤其是“对象之间的通信应该是从视图模型到视图模型而不是通过模型进行”的评论。我理解设计模式的理念,但与 OP 所做的相比,这有什么实际优势呢?正如您所指出的,您的解决方案非常复杂。此外,如果数据模型更改需要与是否已实例化视图无关,会发生什么情况?即使不在视图中,数据操作是否必须为任何数据操作创建视图模型?只是好奇您为什么认为形式如此重要,以至于要证明额外的复杂性是合理的。 - Emperor Eto
问题集很复杂。这只是将所有的复杂性放在一个地方,而不是在模型对象中。事实上,我在这里没有做任何原创编程,我只是从上面提取了原始方法,并修改它们以适用于ViewModel。然后,每个其他对象都变得非常简单,包括绑定;因此,这代表了对上面原始解决方案的简化。如果您的UI应用程序正在更改数据,则最好通过ViewModel进行操作,以便UI不会互相干扰。没有必要教条,但例外情况很少... - Jacob Proffitt

1
这对我的目的来说似乎足够好用。我相信各种其他形式的解决方案都可以在互联网上找到。
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));
    }
}

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