繁体   English   中英

在基础模型数据更改时,通知ViewModel中定义的属性更改

[英]Notify property change defined in the ViewModel when underlying Model data changes

问题:在列表中的单个项目中的属性更改值之后,更新有界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中更改项目的值时,我希望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;

    .
    .
    .

}

并且viewmodel更改为:

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));
    }
}

这个解决方案有效,但它似乎是一个围绕黑客的工作,应该有一个更有说服力的实现。 我的项目在viewmodel中有几个sum属性,而我现在看来,有很多属性需要更新,而且要编写的代码很多,感觉就像开销更多。

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

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

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

简而言之,您的ViewModel处理协调。 您的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));
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM