MVVM模式下计算属性的更新

12

我正在学习MVVM,试图找到一种方法来显示经过计算得出的属性随着它所依赖的属性值的改变而改变的过程。目前为止,所有我看到的解决方案都严重违反了封装性原则,我想知道是否有更好的方法。

假设我需要显示的其中一件事情是复杂税务计算的结果。这个计算(以及可能涉及到的依赖项)会不时地发生改变,因此我希望能够将其严格封装起来。

这里最常提供的解决方案似乎是让所有影响税值的属性在 ModelView 中调用 PropertyChanged 事件,包括当前属性本身以及所有依赖于它的属性。这意味着每个属性都需要知道使用它或可能使用它的所有内容。当我的税务规则以一种使计算依赖于以前没有依赖的内容的方式发生变化时,我将需要触及所有那些进入我的计算的新属性(可能在其他类中,可能不在我控制之下),以使它们调用PropertyChanged来更新税值。这完全破坏了任何封装的希望。

我能想到的最好的解决方案是使执行计算的类接受 PropertyChanged 事件,并在涉及到计算的任何内容发生变化时引发一个新的 PropertyChanged 事件,以更新税值。这至少保留了类级别的封装性,但仍然打破了方法级别的封装性:类不应该知道方法如何完成其工作。

所以,我的问题是,是否有更好的方法(如果有,是什么)?或者说,表现封装(MVVM)是否防止业务逻辑的封装?我面临着二选一的选择吗?


我不知道这里是否有一个“正确/错误”的答案。此外,在ViewModel中有很大的封装关注吗?无论你怎么做,从视图的角度来看,VM仍然是封装的“黑盒子”。 - hall.stephenk
我对封装的担忧是关于可扩展性的担忧。我真正考虑的情况是具有复杂UI和多个ViewModel的情况。 - digitig
5个回答

4
这里提供的解决方案通常是让所有影响税值的属性在其ModelView中调用PropertyChanged,以及每个依赖于它们的属性。

如果支持属性不需要自己的更改通知,除非它们正在被显示。但每个属性都需要在setter中直接或间接地调用tax值的OnPropertyChanged("TaxValue"),如下面的示例所示。这样,UI就会因为一个“支持”属性的更改而更新。

话虽如此,我们来考虑一个例子。一种方法是创建一个计算值的方法。当设置最终值(下面的TaxValue)时,它将调用OnNotifyPropertyChange。无论什么值触发它(扣除|率|收入),该操作都会向用户通知整个世界的TaxValue更改:

public class MainpageVM : INotifyPropertyChanged 
{
       public decimal TaxValue 
        {
           get { return _taxValue; }
           set { _taxValue = value; OnPropertyChanged(); }  // Using .Net 4.5 caller member method.
        }

        public decimal Deduction
        {
           get { return _deduction; }
           set { _deduction = value; FigureTax(); }
        }

        public decimal Rate
        {
           get { return _rate; }
           set { _rate = value; FigureTax(); }
        }

        public decimal Income
        {
           get { return _income; }
           set { _income = value; FigureTax(); }
        }

        // Something has changed figure the tax and update the user.
        private void FigureTax()
        {
           TaxValue = (Income - Deduction) * Rate;
        }


    #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>Raises the PropertyChanged event.</summary>
        /// <param name="propertyName">The name of the property that has changed, only needed
        /// if called from a different source.</param>
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

    #endif
    }

编辑

如果您使用的是 .Net 4,请安装 Nuget 包,以使用 CallerMemberName(和其他项):

Microsoft.BCL

如果不使用,请改用标准的 OnPropetyChanged("TaxValue")


1
也许值得一提的是:通过更新,您可以在 .net 4.0/Vs 2010 中使用 .net 4.5 的 OnPropertyChanged 属性名称推断。它不会自动包含在 4.0 中。 - user180326
@jdv-JandeVaan 不好意思...我编辑了帖子以反映如何在 .Net 4 中获取它。谢谢! - ΩmegaMan
1
+1 优秀的答案。我会添加类声明以显示INPC的继承,但除此之外是经典的MVVM。 - Gayot Fow
2
似乎这并没有帮助。如果FigureTax变得依赖于某个新属性,那么该属性的setter代码仍然需要更改,以便它现在调用FigureTax。如果它们都在我的控制下,我可以做到这一点(尽管如果我错过一个,这将是麻烦等待发生 - 我过去20年的编程经验一直在学习使这些事情不可能的方法!)但在复杂的情况下,可能会有多个ViewModel处理UI的不同部分,因此我可能无法更改setter。 - digitig
@digitig,您描述了一个自动系统。为此,另一种方法是创建一个计时器,然后反射类的属性,并在计时器内为每个属性具有值的缓存。当其中一个更改时,在Tax值上调用PropertyChanged。因此,MVVM PropertyChanged是一个推送系统,而另一个则是拉取系统。由您选择哪个是两害相权取其轻。 :-) - ΩmegaMan
从触发器属性设置器显式地触发依赖属性的通知在长期运行中会变成维护噩梦。这基本上是循环依赖,并以微妙的方式违反了DRY原则。 现代UI库转向响应式风格,这是好的,但Rx有一定的学习曲线。然而,在上面的答案中提到的CalculatedProperties非常简单,适用于许多用例。 - KolA

4
请查看Stephen Cleary的“Calculated Properties”(计算属性):https://github.com/StephenCleary/CalculatedProperties 它非常简单,只是传递依赖属性的通知,而不会污染触发属性设置器。
一个基本的例子:
public string Name 
{
  get { return Property.Get(string.Empty); }
  set { Property.Set(value); }
} 

public string Greeting => Property.Calculated(() => "Hello, " + Name + "!");

它在尺寸方面非常强大:可以将其视为View Model属性的类似于Excel的公式引擎。

我在几个项目中都使用过它,包括领域和View Model类。它帮助我消除了大部分命令式控制流(一个主要的错误源)并使代码更加声明性和清晰。

最好的事情是,相关属性可以属于不同的View Model,并且依赖关系图在运行时可能会发生巨大变化,但它仍然可以正常工作。


3
有一个叫做Fody/PropertyChanged的插件,它在编译时工作,自动实现PropertyChanged。当复杂的税务计算发生变化时,它将自动查看同一类中使用您的属性的属性,并引发所有适当的PropertyChanged事件。
您可以使用ILSpy反编译已编译的代码,查看它所做的并验证它是否引发了所有适当的事件。

有趣的是,看起来我不是唯一一个担心这个问题的人,解决方案是可能的。 - digitig
Fody和PostSharp不支持依赖关系图,这是在运行时已知或更改的(例如,Total是子视图模型子总计列表的总和)。然而,https://github.com/StephenCleary/CalculatedProperties可以在没有额外样板文件的情况下做到这一点。 - KolA

3
在这里提供的解决方案通常是获取所有税值依赖的属性,使其调用PropertyChanged,并为每个依赖于它的属性和其本身的属性触发该事件。但是,只针对该对象:每个属性应在setter中触发自己的属性更改事件。此外,setter应以某种方式触发取决于该值的属性。您不应主动尝试触发其他对象的更新:它们应该侦听该对象的PropertyChanged。
我能想到的最好的解决方案是让进行计算的类接收PropertyChanged事件,并在任何进入计算的内容发生更改时引发新的PropertyChanged事件。这至少保留了类级别的封装性,但它仍然违反了方法级别的封装性:该类不应该知道方法如何完成其工作。
这确实是标准方法。每个类都有责任监视其所依赖的属性,并为其自身的属性触发属性更改事件。
可能有一些框架可以帮助您完成此操作,但了解应该发生什么是值得的。

好的,我可以看到在复杂情况下,处理程序可能会以可能更改的属性为基础形成一个大型开关语句,并调用可能影响的每个属性上的PropertyChanged。即使它不是噩梦,但听起来像是维护的噩梦。我想答案是将具有复杂计算的属性重构为自己的类,因此PropertyChanged处理程序只需要处理该属性,而不是包含类可能具有的(潜在)许多计算属性。 - digitig

0
我能想到的最好解决方案是使执行计算的类“接收” PropertyChanged 事件,并在任何用于计算的值发生更改时引发一个新的 PropertyChanged 事件,至少可以保留类级别的封装性,但它仍然违反了方法的封装性:类不应该知道方法如何工作。
我认为您将“封装性”这个术语扩展到了关于语法的吹毛求疵的程度。例如,这里没有问题:
private int _methodXCalls;
public void MethodX() {
    Console.WriteLine("MethodX called {0} times", ++_methodXCalls);
}

该字段仅在MethodX内相关,但仅仅因为声明不在MethodX语法上下文中并不意味着它会破坏方法封装。

同样,在类初始化中为每个属性设置事件处理程序也没有问题。只要它只出现一次且没有其他内容需要“知道”这些特定的处理程序已添加,那么您的属性仍然是逻辑上自包含的。您可能可以在属性上使用属性,例如[DependsOn(property1, property2)],但这实际上只是一个代码可读性问题。


我不认为这是一个特别贴切的比喻。我更关心避免错误的实际问题,而不是封装纯度的教义问题。_methodXCalls 的其他用户 - 属于它的类的维护者 - 不关心 MethodX 如何保持更新,只关心它是否能够做到。如果他们必须更改 MethodX,他们不需要知道谁还在使用它。更改不必跨越边界传播。 - digitig
@digitig 它非常接近;实际上,它甚至不是一个“类比”,仅仅是相同概念的另一个例子:初始化。唯一的区别在于,字段可以在类级别进行初始化,而事件订阅必须放置在构造函数中。这主要是一种语法限制。在两种情况下都没有“边界交叉”:属性的其他用户不关心事件订阅,而本身及其初始化也不关心什么更新属性,只关心它们的更新。问题出在哪里? - nmclean
问题在于我很难预测几年后维护代码时可能会犯的错误。我很可能会搞砸更新复杂依赖关系,但只有在经历了特别繁重的彻夜工作之后才可能会搞砸_methodXCalls的使用。 - digitig

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