2016 年 7 月

第 31 卷,第 7 期

数据绑定 - 在 .NET 中实施数据绑定的更好方法

作者 Mark Sowul

数据绑定是一种开发 UI 的有效技术: 数据绑定可以更轻松地区分视图逻辑和业务逻辑,而且还可以更简便地测试生成的代码。虽然从一开始数据绑定在 Microsoft .NET Framework 中就一直存在,但它是随着 Windows Presentation Foundation (WPF) 和 XAML 的产生而日趋重要的,因为在“模型-视图-视图模型”(MVVM) 模式中,数据绑定充当着“视图”和“视图模型”的“粘合剂”。

一直以来,魔幻字符串和样本代码要广播属性的更改及绑定 UI 元素时,都需要处理实施数据绑定中的问题。近年来,出现了各种工具包和技术来降低数据绑定的难度。本文旨在进一步简化数据绑定的流程。

首先,我将回顾实施数据绑定的基本知识及简化流程的通用技术(如果对本主题已有所了解,请自行跳过相关章节)。之后,我将开发一种你之前可能从未想过的技术(“第三种方法”),并介绍对使用 MVVM 开发应用程序时遇到的相关设计难题的解决方案。你可以在附带的“代码下载”中获取我在此处开发的框架的最终版,或将 SolSoft.DataBinding NuGet 包添加到自己的项目中。

基础知识: INotifyPropertyChanged

实施 INotifyPropertyChanged 是启用要绑定到 UI 的对象的首选方法。此方法非常简单,只包括一个成员:PropertyChanged 事件。当可绑定的属性更改时,该对象会引发此事件,以通知视图应刷新属性值的表示。

相互作用的关系很简单,但实施过程不简单。使用硬解码的文本属性名称来手动引发事件不是一种很好的解决方案,也无法进行重构: 你必须仔细确保文本名称与代码中的属性名称保持同步。这一点会使后续人员对你有所不满。例如:

public int UnreadItemCount
{
  get
  {
    return m_unreadItemCount;
  }
  set
  {
    m_unreadItemCount = value;
    OnNotifyPropertyChanged(
      new PropertyChangedEventArgs("UnreadItemCount")); // Yuck
  }
}

人们开发了多种技术来解决上述问题,以维持正常运行(例如,可以参阅 bit.ly/24ZQ7CY 中的“堆栈溢出”问题);其中大部分技术可以归为两种类型之一。

通用技术 1: 基类

简化问题的方法之一是使用基类,以重复使用样本逻辑中的一部分。这样还可以提供几种以编程方式获取属性名称的方法,而不必再对其进行硬编码。

使用表达式获取属性名称: .NET Framework 3.5 引入了表达式,可以对代码结构进行运行时检查。LINQ 使用此 API 发挥了巨大的作用,例如,将 .NET LINQ 查询转换为 SQL 语句。勇于创新的开发人员还利用此 API 来检查属性名称。使用基类来执行此检查时,上述资源库可以重新编写为:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(() => UnreadItemCount);
}

这样,重命名 UnreadItemCount 也会重命名表达式引用,这样代码仍然有效。RaiseNotifyPropertyChanged 的签名将如下所示:

void RaiseNotifyPropertyChanged<T>(Expression<Func<T>> memberExpression)

存在各种从 memberExpression 中检索属性名称的技术。bit.ly/25baMHM 中的 C# MSDN 博客提供了一个简单示例:

public static string GetName<T>(Expression<Func<T>> e)
{
  var member = (MemberExpression)e.Body;
  return member.Member.Name;
}

StackOverflow 在 bit.ly/23Xczu2 中展示了更为详细的列表。任何情况下,此技术都存在不足: 检索表达式名称使用反射,而反射很缓慢。根据属性更改通知的数量,性能开销可能很大。

使用 CallerMemberName 获取属性名称: C# 5.0 和 .NET Framework 4.5 带来了另外一种检索属性名称的方法,即使用 CallerMemberName 属性(你可以通过 Microsoft.Bcl NuGet 包中的 .NET Framework 的较旧版本来使用此属性)。此时,编译器负责所有工作,所以不存在运行时开销。通过此方式,对应的方法变成:

void RaiseNotifyPropertyChanged<T>([CallerMemberName] string propertyName = "")
And the call to it is:
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged();
}

属性指示编译器填充调用者名称、UnreadItemCount,来作为可选参数 propertyName 的值。

使用 nameof 获取属性名称: CallerMemberName 属性可能是为此使用案例(在基类中引发 PropertyChanged)量身定制的,但是在 C# 6 中,编译器团队最后提供了用途更为广泛的方法,即 nameof 关键字。nameof 方便用于多种用途;在此案例中,如果我用 nameof 替代了基于表达式的代码,则编译器将再一次负责所有工作(且不存在运行时开销)。需要注意的是,这完全属于编译器版本特性,而非 .NET 版本特性: 你可以使用此技术,且仍使用 .NET Framework 2.0。然而,你(及团队所有成员)必须至少在使用 Visual Studio 2015。使用 nameof 的过程如下:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(nameof(UnreadItemCount));
}

但是所有基类技术都存在一个普遍的问题: 正如大家所言,基类技术“会消耗你的基类”。如果你希望视图模型扩展不同的类,那么你的希望会落空。基类技术也不会处理“依赖”属性(例如,连接 FirstName 和 LastName 的 FullName 属性): 对 FirstName 或 LastName 的任何更改还必须触发在 FullName 上的更改。

通用技术 2: 面向方面的编程

面向方面的编程 (AOP) 是一种基本上在运行时或编译后对已编译的代码进行后续处理以添加某些行为(称为“方面”)的技术。通常以替换重复的样本代码为目的,例如日志记录或异常处理(即所谓的“横切关注点)。毫无意外,实施 INotifyPropertyChanged 是个不错的选择。

此方法有多个工具包可用。PostSharp 是其中之一 (bit.ly/1Xmq4n2)。我意外而惊喜地发现 PostSharp 恰当地处理了依赖属性(例如,前文中提到的 FullName 属性)。称为“Fody”的开源框架与此相似 (bit.ly/1wXR2VA)。

这是一种很吸引人的方法,其不足几乎不值一提。一些实施方案会在运行时拦截行为,从而导致性能成本。而编译后框架则截然不同,它不会产生任何运行时开销,但可能需要进行一些安装或配置。PostSharp 目前作为 Visual Studio 的扩展提供。其免费的 Express 版本仅限将 INotifyPropertyChanged 方面用于 10 个类,因此这也似乎意味着一笔金钱支出。另一外面,Fody 是一款免费的 NuGet 包,这也使其看似是一种诱人的选择。无论如何,请记住使用任何 AOP 框架编写的代码与你将运行和调试的代码并不完全相同。

第三种方法

处理上述状况的备选方法是利用面向对象的设计: 让属性自己负责引发事件! 虽然这不是一个非常创新的计划,但却是我在自己的项目外没有见到过的想法。基本来说,此方法类似以下内容:

public class NotifyProperty<T>
{
  public NotifyProperty(INotifyPropertyChanged owner, string name, T initialValue);
  public string Name { get; }
  public T Value { get; }
  public void SetValue(T newValue);
}

计划是你提供一个具有名称和对所有者的引用的属性,然后让此属性负责引发 PropertyChanged 事件 - 类似以下:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.PropertyChanged(m_owner, new PropertyChangedEventArgs(Name));
  }
}

问题在于这个过程无法真正实施: 我无法以此方式从其他类中引发事件。我需要与所属类建立一些协定来允许我引发 PropertyChanged 事件:这完全属于接口的工作范围,因此我将创建一个接口:

public interface IRaisePropertyChanged
{
  void RaisePropertyChanged(string propertyName)
}

具备此接口后,我就可以真正实施 Notify­Property.SetValue 了:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.RaisePropertyChanged(this.Name);
  }
}

实施 IRaisePropertyChanged: 需要属性所有者来实施接口即代表每个视图模型类都需要图 1 中所示的一些样本。第一部分是所有类实施 INotifyPropertyChanged 必备的;第二部分专门针对新的 IRaisePropertyChanged。注意:由于 RaisePropertyChanged 方法不用于普通用途,因此我倾向于将实施过程清楚地呈现出来。

图 1 实施 IRaisePropertyChanged 需要的代码

// PART 1: required for any class that implements INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
  // In C# 6, you can use PropertyChanged?.Invoke.
  // Otherwise I'd suggest an extension method.
  var toRaise = PropertyChanged;
  if (toRaise != null)
    toRaise(this, args);
}
// PART 2: IRaisePropertyChanged-specific
protected virtual void RaisePropertyChanged(string propertyName)
{
  OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
// This method is only really for the sake of the interface,
// not for general usage, so I implement it explicitly.
void IRaisePropertyChanged.RaisePropertyChanged(string propertyName)
{
  this.RaisePropertyChanged(propertyName);
}

我可以将此样本放入基类并进行扩展,这好像又回到了前面的讨论。毕竟,如果我将 CallerMemberName 应用到 RaisePropertyChanged 方法,那么我基本上是重新设计了第一种技术,那有什么意义呢? 在这两种情况下,如果其他类无法从基类中派生,我都只能将样本复制到其他类。

与前面基类技术相比,关键区别在于此时在样本中没有真正的逻辑;所有逻辑都封装在 NotifyProperty 类。在引发事件前检查属性值是否已更改属于简单逻辑,但最好不要复制此逻辑。如果想要使用一个不同的 IEqualityComparer 来执行此检查,请考虑一下会发生什么。使用此模型,你只需要改变 NotifyProperty 类。即使你有多个具有相同 IRaisePropertyChanged 样本的类,每个实施方案无需更改自身的任何代码即可从对 NotifyProperty 的更改中获得需要的内容。无论你想要引入何种行为更改,IRaisePropertyChanged 代码都不会更改。

整理汇总: 现在我有了视图模型需要实施的接口和用于进行数据绑定的属性的 NotifyProperty 类。最后一步是构造 NotifyProperty;为此,你仍需要以某种方式传入一个属性名。如果你恰好在使用 C# 6,就可以轻松地通过 nameof 运算符实现这一点。如果不是,你可以借助表达式创建 NotifyProperty,例如通过使用扩展方法(很不幸,这次 Caller­MemberName 无法发挥作用):

public static NotifyProperty<T> CreateNotifyProperty<T>(
  this IRaisePropertyChanged owner,
  Expression<Func<T>> nameExpression, T initialValue)
{
  return new NotifyProperty<T>(owner,
    ObjectNamingExtensions.GetName(nameExpression),
    initialValue);
}
// Listing of GetName provided earlier

通过这种方式,你仍将支付反射成本,但仅限于在创建对象时支付,而不是每次属性更改时都支付。如果(你在创建许多对象时)这仍然太过昂贵,你可以总是缓存对 GetName 的调用,并将其保存为视图模型类中的一个静态只读值。图 2 表示的是以上两种情况中的简单视图模型的示例。

图 2 具有 NotifyProperty 的基本视图模型

public class LogInViewModel : IRaisePropertyChanged
{
  public LogInViewModel()
  {
    // C# 6
    this.m_userNameProperty = new NotifyProperty<string>(
      this, nameof(UserName), null);
    // Extension method using expressions
    this.m_userNameProperty = this.CreateNotifyProperty(() => UserName, null);
  }
  private readonly NotifyProperty<string> m_userNameProperty;
  public string UserName
  {
    get
    {
      return m_userNameProperty.Value;
    }
    set
    {
      m_userNameProperty.SetValue(value);
    }
  }
  // Plus the IRaisePropertyChanged code in Figure 1 (otherwise, use a base class)
}

绑定和重命名:在讨论名称时,也是探讨其他数据绑定问题的好时机。安全引发不含硬编码的字符串的 PropertyChanged 事件表示重构成功了一半;数据绑定自身则是另一半。如果你对用于 XAML 中绑定的属性进行重命名,我会说,这未必成功(例如,可以参阅 bit.ly/1WCWE5m)。

备选做法是在代码隐藏文件中手动编码数据绑定。例如,

// Constructor
public LogInDialog()
{
  InitializeComponent();
  LogInViewModel forNaming = null;
  m_textBoxUserName.SetBinding(TextBox.TextProperty,
    ObjectNamingExtensions.GetName(() => forNaming.UserName);
  // Or with C# 6, just nameof(LogInViewModel.UserName)
}

只为使用表达式功能而保留空对象显得有些奇怪,但这确实有效(如果你有 nameof 的权限,则无需如此)。

我认为此技术有一定价值,但我也承认其中的利弊。从好的方面来说,如果我重命名 UserName 属性,我可以很自信地保证重构会成功。另外一个巨大的好处则是“查找所有引用”按预期起作用。

不足的一方面则是没有像在 XAML 中进行绑定那样尽可能地简便和自然,并且使我无法保持 UI 设计的“独立性”。 例如,我无法仅仅在“混合”工具中重新设计外观而不必更改代码。此外,此技术不适用于数据模板;你可以将该模板提取到自定义控件,但需要耗费更多精力。

总的来说,我获得了更改“数据模型”端的灵活性,却以在“视图”端失去灵活性为代价。总之,由你决定是否优势取胜,并决定使用此方法进行绑定。

“派生”属性

前面我介绍了一种在其中引发 PropertyChanged 事件极为不便的场景,即为值依赖于其他属性的属性引发 PropertyChanged 事件时。我提到了一个简单示例,即依赖于 FirstName 和 LastName 的 FullName 属性。我实施此场景的目的在于传入基本 NotifyProperty 对象(FirstName 和 LastName)及计算其派生值的函数(例如,FirstName.Value + " " + LastName.Value),然后基于上述,生成将自动为我处理剩下部分的属性对象。为了实现上述目的,我将对最初的 NotifyProperty 进行一些调整。

第一个任务是在 NotifyProperty 上公开一个单独的 ValueChanged 事件。派生属性将在其基本属性中侦听此事件,并通过计算出一个新值进行回应(并为自身引发正确的 PropertyChanged 事件)。第二个任务是提取一个接口,即 IProperty<T>,来封装通用的 NotifyProperty 功能。此外,这允许我从其他派生的属性中获取派生的属性。生成的接口很直观,并列于此处(对 NotifyProperty 的相应更改非常简单,因此不会列出):

public interface IProperty<TValue>
{
  string Name { get; }
  event EventHandler<ValueChangedEventArgs> ValueChanged;
  TValue Value { get; }
}

在你开始尝试将步骤综合前,创建 DerivedNotifyProperty 类似乎也比较简单。基本计划是传入基本属性和计算其一些新值的函数,但由于是泛型此过程立即陷入困境。没有传入多种不同属性类型的实际方法:

// Attempted constructor
public DerivedNotifyProperty(IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

我可以通过使用静态的创建方法解决问题的前半部分(接受多种泛型类型):

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

但是派生的属性仍需要侦听每个基础属性上的 ValueChanged 事件。解决此问题需要两个步骤。首先,我将 ValueChanged 事件提取到一个单独的接口中:

public interface INotifyValueChanged // No generic type!
{
  event EventHandler<ValueChangedEventArgs> ValueChanged;
}
public interface IProperty<TValue> : INotifyValueChanged
{
  string Name { get; }
  TValue Value { get; }
}

这将允许 DerivedNotifyProperty 传入非泛型INotifyValueChanged 而不是泛型 IProperty<T>。其次,我需要计算不含泛型的新值: 我将使用接受两个泛型参数的原始 derivedValueFunction,并由此创建新的无需任何参数的匿名函数 - 新函数将引用两个属性传入的值。换言之,我将创建一个闭包。你可以在下列代码中查看此过程:

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)
{
  // Closure
  Func<TDerived> newDerivedValueFunction =
    () => derivedValueFunction (property1.Value, property2.Value);
  return new DerivedNotifyProperty<TValue>(owner, propertyName,
    newDerivedValueFunction, property1, property2);
}

新的“派生值”函数仅是没有任何参数的 Func<TDerived>;现在 DerivedNotifyProperty 无需基本属性类型的任何内容,所以我可以愉快地从多个不同类型的属性中创建一个 DerivedNotifyProperty。

另一个要点在于何时真正调用派生值函数。一种明显的实施方案是侦听每个基本属性的 ValueChanged 事件,一旦属性更改则调用函数。但在同一个操作中多个基本属性更改时(想象一下“重置”按钮清除窗体时),此方法效率并不高。一种更好的方法是根据需要生成值(并进行缓存),如果任何基本属性更改则使该值无效。Lazy<T> 是实施此过程的最好方法。

你可以在图 3 中查看 DerivedNotifyProperty 类的缩写列表。注意:虽然我仅列出了两种基本属性的创建方法,但此类可以传入任意数量的要侦听的属性。我创建其他的重载以传入一个基本属性、三个基本属性,以此类推。

图 3 DerivedNotifyProperty的核心实施

public class DerivedNotifyProperty<TValue> : IProperty<TValue>
{
  private readonly IRaisePropertyChanged m_owner;
  private readonly Func<TValue> m_getValueProperty;
  public DerivedNotifyProperty(IRaisePropertyChanged owner,
    string derivedPropertyName, Func<TValue> getDerivedPropertyValue,
    params INotifyValueChanged[] valueChangesToListenFor)
  {
    this.m_owner = owner;
    this.Name = derivedPropertyName;
    this.m_getValueProperty = getDerivedPropertyValue;
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    foreach (INotifyValueChanged valueChangeToListenFor in valueChangesToListenFor)
      valueChangeToListenFor.ValueChanged += (sender, e) => RefreshProperty();
  }
  // Name property and ValueChanged event omitted for brevity 
  private Lazy<TValue> m_value;
  public TValue Value
  {
    get
    {
      return m_value.Value;
    }
  }
  public void RefreshProperty()
  {
    // Ensure we retrieve the value anew the next time it is requested
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    OnValueChanged(new ValueChangedEventArgs());
    m_owner.RaisePropertyChanged(Name);
  }
}

注意:基本属性可能来自不同的所有者。例如,假定你具有一个包含 IsAddressValid 属性的“地址”视图模型。你还具有一个“订单”视图模型,其中含有两个用于账单地址和送货地址的“地址”视图模型。在父级“订单”视图模型上创建 IsOrderValid 属性来合并子“地址”视图模型中的 IsAddressValid 属性是明智的做法,这样只有在两个地址都有效时你才能提交订单。若要实现此目的,“地址”视图模型要公开 IsAddressValid { get; } 和 IProperty<bool> IsAddressValidProperty { get; } 两个布尔值,这样“订单”视图模型就可以创建一个 DerivedNotifyProperty 来引用子 IsAddressValidProperty 对象。

DerivedNotifyProperty 的实用性

我提供给派生属性的全名示例很大一部分是人为设计的,但我十分希望讨论一些真正的使用案例,并将案例与某些设计原则相结合。我仅在介绍一种示例: IsValid。这是一种相当简单而强大的方法,例如,可以禁用窗体的“保存”按钮。注意:不会限制你只将此技术用于 UI 视图模型的上下文中。你还可以使用此技术来验证业务对象;业务对象只需实施 IRaisePropertyChanged。

派生属性用途非常广泛的第二种情况是在“深化”场景中。列举一个简单的示例 - 想一想用于选择国家/地区的组合框,在其中选择一个国家/地区就会填充一系列的城市。你可以将 SelectedCountry 作为 NotifyProperty,并且在给定 GetCitiesForCountry 方法的情况下创建 AvailableCities 来作为在所选国家/地区更改时将自动保持同步的 DerivedNotifyProperty。

我使用 NotifyProperty 对象的第三种场合是用于指示对象是否“忙碌”。 如果对象被视为忙碌,会禁用某些 UI 功能,并且或许用户可以看到一个进度指示器。这是一个看似简单的场景,但是其中有许多需要指明的要点。

第一部分是跟踪对象是否忙碌;在简单的案例中,我可以使用布尔 NotifyProperty 来实现这个目的。但是,经常会发生的是对象可能出于多种原因中的一种而“忙碌”:例如,我正在加载多个区域的数据,有可能是并行执行。总体“忙碌”的状态应取决于是否有以上项中的任意项仍正在进行。这听起来几乎像是派生属性的工作范围,但使用派生属性进行这项工作会很不便(如果可能的话): 我需要为每个可能的操作设置各自的一个属性来追踪各个操作是否正在进行。不过,我想要使用单个 IsBusy 属性来对每个操作执行类似以下的内容:

try
{
  IsBusy.SetValue(true);
  await LongRunningOperation();
}
finally
{
  IsBusy.SetValue(false);
}

为了实现此目的,我创建了 IsBusyNotifyProperty 类来扩展 NotifyProperty<bool>,并在其中保留了“忙碌计数”。 我重写了 SetValue,这样 SetValue(true) 会增加计数,而 Set­Value(false) 会减少计数。计数从 0 到 1 进行时,我调用 base.SetValue(true),而计数从 1 到 0时,则调用 base.SetValue(false)。使用这种方法时,启动多个未完成的操作仅会导致 IsBusy 成为 True 一次,之后在只有所有操作都完成后才会再次成为 False。你可以在代码下载中查看此实施过程。

以下就处理了事务“忙碌”端的问题: 我可以将“忙碌”绑定到进度指示器的可见性。要禁用 UI,则需进行相反的设置。当“忙碌”为 True 时,“UI 已启用”应为 False。

XAML 中具有 IValueConverter 这一概念,它可以将值和显示方式相互转换。BooleanToVisibilityConverter 是一个通用的示例 - 在 XAML 中,一个元素的“可见性”不由布尔表示,而由枚举值表示。这就表示无法将元素的可见性直接绑定到布尔属性(例如,IsBusy);你需要绑定该值,还需要使用一个转换器。例如,

<StackPanel Visibility="{Binding IsBusy,
  Converter={StaticResource BooleanToVisibilityConverter}}" />

我提到过“启用 UI”是“忙碌”的对立面;创建一个值转换器来反转一个布尔属性并使用它执行操作,这个想法让人跃跃欲试:

<Grid IsEnabled="{Binding IsBusy,
   Converter={StaticResource BooleanToInverseConverter}}" />

事实上,在我创建 DerivedNotifyProperty 类之前,这是最简单的方法。创建一个单独的属性,将其绑定到 IsBusy 的反面,然后引发正确的 PropertyChanged 事件,这个过程相当枯燥。但是现在变得轻松,而且少了这种人为障碍(即惰性),我更明确地意识到将 IValueConverter 用于何处更有意义。

最终,无论视图(例如,WPF 或 Windows 窗体,甚至一款控制台应用都是一种类型视图)是如何实现的,都应是对基础应用程序中的现状的一种可视化(或“投影”),而无需决定正在发生的事务的机制和商业规则。上述情况中,IsBusy 和 IsEnabled 恰好彼此紧密关联,这个状况属于实施细节;并非禁用 UI 就必定要专门关联到应用程序忙碌与否。

目前为止,我认为此方法尚不成熟,如果你想要使用值转换器来实施此步骤,我绝无异议。但是,我可以通过向此示例添加其他部分来构造一个更为严谨的案例。假设应用程序丢失了网络访问权限,其还要禁用 UI(并显示指示状况的面板)。那么,这可以分为三种情况: 如果应用程序忙碌,我要禁用 UI(并显示进度面板)。如果应用程序丢失了网络访问权限,我也要禁用 UI(并显示“丢失连接”面板)。第三种情况是应用程序处于连接状态,且并不忙碌,此时要准备接受输入。

在没有单独的 IsEnabled 属性的情况下尝试实施过程,最好的状态下也很不便;你可以使用 MultiBinding,但是这仍然很吃力,并且它并非在所有环境中都受支持。最终,这种不便通常表示还存在更好的方法,而现在我们知道确实存在:此逻辑在视图模型中更易处理。现在很轻松地即可将两个 NotifyProperties,即 IsBusy 和 IsDisconnected 公开,然后创建一个 DerivedNotifyProperty,即 IsEnabled,而只有以上两者都为 False 时,IsEnabled 才为 True。

如果你使用了 IValueConverter,并将 UI 的“启用”状态直接绑定到了 IsBusy(使用转换器来反转),现在你将需要进行很多处理了。相反,如果你公开了一个单独的派生的 IsEnabled 属性,添加这条新逻辑工作量要小的多,而且 IsEnabled 绑定自身甚至不需要更改。这是个好的预示,表明你的操作正确。

总结

布置此框架是个漫长的旅程,但获得的回报是现在我无需重复的样本、魔幻字符串,即可实施属性更改通知,还可以支持重构。我的视图模型不需要来自特定基类的逻辑。我可以创建派生的属性,而该属性无需进行额外的处理也可以引发正确的更改通知。最后,我看到的代码即是正在运行的代码。而我通过使用面向对象的设计开发了一个相当简单的框架,就实现了这一切。希望这对你自己的项目有所帮助。


Mark Sowul从一开始就是一名敬业的 .NET 开发人员,通过纽约咨询公司 SolSoft Solutions 分享了他在 Microsoft .NET Framework 和 SQL Server 方面有关体系结构和性能的丰富专业知识。可以通过 mark@solsoftsolutions.com 与他取得联系。如果你认为他的观点很有趣,想要订阅他的时事通讯,请在 eepurl.com/_K7YD. 中注册。

衷心感谢以下技术专家对本文的审阅: Francis Cheung (Microsoft) 和 Charles Malm (Zebra Technologies)
Francis Cheung 是 Microsoft 模式与实践组的首席开发人员。Francis 一直在参与各种项目系列,包括 Prism 在内。目前他正在致力于提供与 Azure 相关的指导。

Charles Malm 是一位游戏、.NET 和 Web 软件工程师,还是 RealmSource, LLC 的联合创始人。