MVVM 和 XAML 一起玩得非常好。在 XAML 中构建 UI 的主要构建块是能够绑定视图模型(MVVM 中的 VM,我们在这里讨论模式的一半)。一种方式或两种方式绑定,没关系。您的逻辑可以更新属性,UI 将神奇地刷新。
可绑定对象及其属性
XAML 期望视图模型满足特定条件以实现该绑定。实现
INotifyPropertyChanged
的类被认为
是可绑定的
,对基础属性的任何更改都必须使用通知来正确地通知 XAML 该值需要再次获取。
考虑这个小视图模型:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
那里有很多噪音,我们刚刚(正确)实现了一个属性。代码看起来是重复的,许多 MVVM 框架涌现出来,带有帮助类来减少噪音。一个这样的例子是 MVVMLight ,这是一个用于快速实现 MVVM 的很棒的库。我们上面的课程可以缩短为:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
我们仍然保留了实现细节,即我们的私有字段,我们损失了一些性能,但是每个属性和类的减少是显而易见的。
通过消除对支持字段的需要,可以进一步减少此代码。你可以在 YAWL.Common.Mvvm.ViewModelBase 中找到这样的尝试:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
为每个属性自动生成私有字段,并将其放入字典中供以后查找。进一步减少代码需要在性能部门做出小的牺牲。
然而,人们可以走得更远,使用 Fody、PostSharp 和任何其他编织器来生成通知代码,作为构建过程的一部分。我们只剩下简单明了的类(使用 Fody/PropertyChanged ):
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
简短,没有性能损失,构建时间增加很小,如果不熟悉编织框架,可能会缺乏清晰度。
派生属性
无论选择哪种方法来实现视图模型,都存在一个小问题:创建依赖于其他属性的属性。例如,可以像这样实现购物车视图模型:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
很明显,
Price
只不过是
Items
属性中项目的价格总和。由于它没有 setter,因此无论何时从购物车中添加或删除商品,它都不会自行更新。我们仍然可以通过在
Items
集合上添加一个事件处理程序来手动触发更新,该事件处理程序将在它发生更改时发出通知。代码可能看起来像这样:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
有几种方法可以编写此类依赖项,并且有一些库可以帮助构建此类通知链。通知其他属性的变化很快变得乏味,并且通知的碎片散布在整个视图模型中。更糟糕的是,跨父/子关系的链接通知变得很麻烦,因为一切都需要手动完成。当代码演化时,这些关系如此耦合,以至于增加了维护成本。
很明显,不能从其他属性创建依赖属性和派生属性。您不能将属性传递给其他人并允许他们依赖更改。随着应用程序复杂性的增加,仅具有用于将 XAML 绑定到视图模型的单一级别的通知是相当有限的。
像
c := a + b
这样简单的东西不是更好吗?
有一种方法可以做到这一点。
可观察属性
受 ReactiveProperty 和 ReactiveUI 的启发,我们可以构建一个能够发出更改事件并可以与其他属性组合的属性。让我们看看我们将如何从上面重构我们的代码:
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
有点冗长,不需要基类,XAML 需要从
{Binding Name}
更改为
{Binding Name.Value}
,因为值现在被包装类似于
Nullable<T>
类。要更改值,请更改内部
Value
属性。
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
好的,访问属性时增加复杂性值得吗?有些视图模型可能不会从这种方法中受益,有些可能。但是,在整个代码库中应用此模式会产生一致性。
让我们来看看我们将如何编写派生属性。熟悉 LINQ 的读者会注意到与可观察属性的相似之处。
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
Reduce 是一种扩展方法,它将自身附加到
ObservableCollection
并监视任何更改。给一个
Func<IEnumerable<T>, R>
类型的 reducer lambda 计算初始值。当原始集合发生变化时,将执行重新计算并且目标属性将自行更新。
代码简洁、一致,并以声明方式写在一个地方。通过将派生属性初始化放在一个地方,如果他们想了解它何时更改,则不再需要扫描整个文件或搜索引用。它还可以防止在值更改时忘记更新依赖/派生属性。程序员更少的工作意味着更好的代码。这段代码也是反应式的,这在我们想要解耦事物时很重要。
让我们看看其他组合运算符。
public class UserViewModel : INotifyPropertyChanged
{
// Implementing INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
= new PropertyChangedEventArgs(nameof(Name));
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, NameChangedArgs);
}
}
}
}
请注意,最后一个示例展示了如何自然而简洁地组合布尔属性。
Map
创建一个新的依赖属性,给定一个转换函数确保目标属性始终是原始值的转换版本。
Combine
可以通过将多个值与指定的组合器函数组合来构建新值。
熟悉 Rx 的读者会注意到,这些是在流中使用的相同基础块。在已经提到的 ReactiveUI 库中可以找到类似的实现。这种风格实际上是受函数式编程的启发,试图用通用的操作和简单的构建块来构建复杂的特性。
在下一篇文章中,我们将研究如何转换 MVVM 中的其他构建块,例如命令。
ObservableProperty
可以在 github 上找到:
YAWL.Composition.ObservableProperty
。