MVVM模式中Model的属性更改接口实现探讨

在MVVM(Model-View-ViewModel)模式中,是否应该在Model中实现属性更改接口(INotifyPropertyChanged)是一个常见问题。理论上,Model实现属性更改接口会违反MVVM模式。本文旨在探讨这种情况通常何时发生,并尝试解决这一问题,以保持MVVM模式的有效性和可靠性。

首先,简要介绍一下MVVM模式。由于网上和书籍中已经有很多相关介绍,这里不再赘述。本文的重点是澄清属性更改接口,因此,如果对MVVM模式完全不了解,建议先从基础知识开始学习。本文的信息来源是Robert McCarter的文章《Design Patterns - Problems and Solutions with Model-View-ViewModel》。

Model是简单的类对象,用于存储数据,有时包含逻辑。Model不直接引用View,也不依赖于View的实现方式。从技术角度讲,Model类通常与服务或存储库一起使用,后者封装了数据访问。

ViewModel类的主要目的是将数据暴露给View。它包括表示逻辑,并且可以在不依赖Model的情况下独立测试。与Model类似,ViewModel从不引用View,但它暴露属性和命令以绑定View数据。本质上,ViewModel充当View和Model之间的协调者。

View类中不应该包含逻辑代码。View仅用于UI视觉行为。

挑战

MVVM模式建议不要将UI代码(表示逻辑)和数据代码(业务逻辑)混合在一起,目标是将这两个领域分开。标准的MVVM方法是仅在ViewModel上实现属性更改接口。因此,如果在Model上实现接口,就会违反模式并使Model变得复杂。

然而,如果一个Model类有20个属性需要在View中暴露,ViewModel通常会有20个相同的属性,这些属性只是简单地代理调用底层Model实例。这些代理属性通常在设置时引发属性更改事件,以指示View属性已更改。因此,每个需要在View中暴露的Model属性都应该有一个代理属性。但是,假设一个专业的应用程序可能需要将多个Model类通过ViewModel暴露给View,这使得实现变得非常复杂。在这种情况下,许多开发人员最终会在Model类中添加属性更改事件。这种方法使Model变得复杂,并降低了以后引入特定于ViewModel的功能的能力。

代码示例

应用程序的想法非常简单。只有一个窗口,分为两个部分,第1部分和第2部分。第1部分包含16个控件,其中一半是文本,其余的是圆形,充当绿色和红色灯的角色。最后,一个“生成”按钮生成一些值。这些值是数字,来自一个模拟服务,该服务在-100到100的范围内生成它们。如果生成的随机数是正数,则相应的灯变为绿色。相应地,如果数字是负数,灯变为红色。

第2部分只是一个简单的求和计算器。用户输入数字,“求和”按钮返回结果。这里没有复杂的逻辑,只是一个求和函数。当前示例使用了MVVM Light Toolkit,这是一个轻量级工具包,可以加速MVVM应用程序的开发。

现在,让看看在构建第1部分时的常见错误。许多开发人员习惯于在Model中实现RaisePropertyChanged。当View直接绑定到Model时,正在混合UI代码和数据代码。结果如下:

// Model public class Section1Model : ViewModelBase { private int _value1; private SolidColorBrush _lightIndicator1; // ... public int Value1 { get { return _value1; } set { _value1 = value; RaisePropertyChanged("Value1"); } } // ... 更多代理属性 public SolidColorBrush LightIndicator1 { get { return _lightIndicator1; } set { _lightIndicator1 = value; RaisePropertyChanged("LightIndicator1"); } } // ... 更多代理属性 }

正如已经提到的,这违反了MVVM模式。知道有时这可能很有用,但在类似的情况下,可以找到(稍后会展示)一种更好的方法,它遵循MVVM模式。下一张图片展示了模式的架构。

此外,想提一下另一个违反MVVM最佳实践的错误。这是SolidColorBrush的使用。再次强调,这是一个与UI相关的职责(视觉行为),它应该位于View中。

现在,为了避免在Model中实现属性更改事件,更喜欢在ViewModel中为Model创建一个包装器,并在XAML中绑定其属性。以下是一个示例,从更新的Model开始:

// Model public class Section1Model { private int _value1; private SolidColorBrush _lightIndicator1; // ... public int Value1 { get { return _value1; } set { _value1 = value; } } // ... 更多代理属性 public SolidColorBrush LightIndicator1 { get { return _lightIndicator1; } set { _lightIndicator1 = value; } } // ... 更多代理属性 }

现在ViewModel实现了属性更改事件。

// ViewModel public class MainViewModel : ViewModelBase { private Section1Model _section1Model; public Section1Model Section1Model { get { return _section1Model; } set { _section1Model = value; RaisePropertyChanged("Section1Model"); } } }

最后,XAML中的绑定变为:

// View

现在为了使Model摆脱与UI相关的代码,丢弃了SolidColorBrush。可以创建自己的类型来表示Model中的颜色(例如布尔值)。然后可以编写自定义ValueConverter将Model颜色类型转换为表示框架依赖的颜色表示。

Converter应该与绑定表达式一起使用。一个示例可以在这里找到。

// 自定义 ValueConverter class BoolToColorConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value == null) { return new SolidColorBrush(Colors.Gray); } return System.Convert.ToBoolean(value) ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }

布尔值扮演绿色/红色灯的角色(true/false)。一个干净的Model看起来像这样:

// Model public class Section1Model { private int _value1; private bool _lightIndicator1; // ... public int Value1 { get { return _value1; } set { _value1 = value; } } // ... 更多代理属性 public bool LightIndicator1 { get { return _lightIndicator1; } set { _lightIndicator1 = value; } } // ... 更多代理属性 }

XAML中的绑定变为:

// View // 添加资源
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485