在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
// 添加资源