简化ViewModel
INotifyPropertyChanged的典型实现为类定义的每个公共属性都有一个私有支持字段,例如:
double number;
它还有一个OnPropertyChanged方法,负责触发PropertyChanged事件:
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
典型的属性定义如下所示:
public double Number
{
set
{
if (number != value)
{
number = value;
OnPropertyChanged("Number");
// Do something with the new value.
}
}
get
{
return number;
}
}
潜在问题涉及传递给OnPropertyChanged方法的文本字符串。 如果拼错它,您将不会收到任何类型的错误消息,但涉及该属性的绑定将不起作用。 此外,支持字段在此单个属性中出现三次。 如果您有几个类似的属性并通过复制和粘贴操作定义它们,则可以省略重新命名后备字段的三个外观之一,并且该错误可能很难追踪。
您可以使用C#5.0中引入的功能解决第一个问题。 CallerMemberNameAttribute类允许您使用调用方法或属性的名称替换可选方法参数。
您可以通过重新定义OnPropertyChanged方法来使用此功能。 通过为它赋值null并在其前面加上方括号中的CallerMemberName属性,使参数可选。 您还需要System.Runtime.CompilerServices的using指令:
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
现在,Number属性可以调用OnPropertyChanged方法,而不使用指示属性名称的参数。 该参数将自动设置为属性名称“Number”,因为这是对OnPropertyChanged的调用所在的位置:
public double Number
{
set
{
if (number != value)
{
number = value;
OnPropertyChanged();
// Do something with the new value.
}
}
get
{
return number;
}
}
此方法避免了拼写错误的文本属性名称,并且还允许在程序开发期间更改属性名称,而无需担心还会更改文本字符串。 实际上,发明CallerMemberName属性的主要原因之一是简化实现INotifyPropertyChanged的类。
但是,仅当从值正在更改的属性调用OnPropertyChanged时,此方法才有效。 在早期的ColorViewModel中,除了对OnPropertyChanged的一个调用之外,在所有调用中仍然需要显式属性名称。
可以更进一步简化set访问器逻辑:您需要定义一个通用方法,可能名为SetProperty或类似的东西。 此SetProperty方法也使用CallerMemberName属性定义:
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
SetProperty的第一个参数是对支持字段的引用,第二个参数是设置为该属性的值。 SetProperty自动检查和设置支持字段。 请注意,它在调用OnPropertyChanged时显式包含propertyName参数。 (否则propertyName参数将成为字符串“SetProperty”!)如果属性已更改,则该方法返回true。 您可以使用此返回值来使用新值执行其他处理。
现在Number属性如下所示:
public double Number
{
set
{
if (SetProperty(ref number, value))
{
// Do something with the new value.
}
}
get
{
return number;
}
}
虽然SetProperty是一个通用方法,但C#编译器可以从参数中推断出类型。 如果您不需要对属性集访问器中的新值执行任何操作,则甚至可以将两个访问器减少为单行而不会模糊操作:
public double Number
{
set { SetProperty(ref number, value); }
get { return number; }
}
您可能希望这样简化以至于您希望将SetProperty和OnPropertyChanged方法放在它们自己的类中,并在创建自己的ViewModel时从该类派生。 这样一个名为ViewModelBase的类已经存在于Xamarin.FormsBook.Toolkit库中:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Xamarin.FormsBook.Toolkit
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
本章将在本章的其余两个示例中使用。