绑定助手:WinForms 中 UI、视图模型和数据模型的分离
在 WinForms 中进行数据绑定的几种技术,以保持处理和显示的分离。
引言
随着 XAML、WPF 和 Silverlight 的推出,微软还为我们引入了 MVVM 模式来开发图形应用程序。MVVM 提供了出色的关注点分离,但它严重依赖 XAML 的声明式数据绑定能力,并且不直接适用于 WinForms 环境。
WinForms 也提供了数据绑定支持,但远不如 WPF/Silverlight 中的先进。本文演示了一些技术,通过使用类似于视图模型的中间类,您可以实现与 MVVM 类似的关注点分离。
WinForms 和 WPF 中的数据绑定
CodeProject 上有几篇很好的文章详细解释了数据绑定,所以我这里只做简要总结(这里有一篇关于 WPF 的优秀文章)。在 WPF 和 Silverlight 中,控件有一个绑定上下文(通常设置为控件的视图模型),并且数据绑定在 XAML 中以声明方式设置。
<Button Text="{Binding ButtonText}" ... />
至关重要的是,WPF/Silverlight 还允许您通过添加绑定转换器来修改绑定过程。转换器在绑定过程中被调用,用于将绑定属性(通常在视图模型中)的值转换为视图中 UI 控件的值(如果绑定是双向的,则用于反向转换)。这意味着在 WPF/Silverlight 中,视图模型不必公开视图所需的精确属性,而只需要公开可以转换为这些属性的属性。例如,考虑一个单选按钮组
class RadioViewModel {
public enum ButtonName { First, Second, Third };
public ButtonName SelectedButton { get; set; }
public class Converter : IValueConverter {
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture) {
return value == parameter;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) {
return value ? parameter : null;
}
}
}
...以及 XAML 中的声明式绑定
<StackPanel>
<StackPanel.Resources>
<RadioViewModel.Converter x:Key="radioConverter"/>
</StackPanel.Resources>
<RadioButton Content="One" IsChecked=
"{Binding SelectedButton, Converter={StaticResource radioConverter},
ConverterParameter=RadioViewModel.ButtonName.First}"/>
<RadioButton Content="Two" IsChecked=
"{Binding SelectedButton, Converter={StaticResource radioConverter},
ConverterParameter=RadioViewModel.ButtonName.Second}"/>
<RadioButton Content="Three" IsChecked=
"{Binding SelectedButton, Converter={StaticResource radioConverter},
ConverterParameter=RadioViewModel.ButtonName.Third}"/>
</StackPanel>
这个简单的例子忽略了反向绑定,并设置了视图模型以通知更改,因为这不是关于 WPF 绑定的文章,但它展示了转换器可以做什么,以及它如何将大部分绑定逻辑移入转换器。
WinForms 数据绑定要基础得多。它基本上就像将单个 UI 属性绑定到您类中的单个属性一样简单。这意味着您绑定的类必须为您想要绑定的每个 UI 属性公开一个属性;在单选按钮示例中,您需要三个布尔属性。要绑定属性,您需要手动添加到 DataBindings
集合中,通常在窗体或用户控件的构造函数中(如果它是自动设计的控件,则在 InitializeComponent
调用之后)。
myLabel.DataBindings.Add("Text", modelInstance, "LabelText");
视图模型和 WinForms 对应物
在我看来,视图模型的作用至少是转换模型自然想要使用的数据格式与最适合视图需要引用的格式之间的转换。正如我们刚才看到的,这个角色的很大一部分可以直接在绑定中通过转换器来指定,这意味着视图模型通常很简单,甚至可以说是微不足道的。事实上,一个简单的 WPF 或 Silverlight 应用程序通常可以通过将 UI 控件直接绑定到具有适当转换器的模型类来编写。
由于 WinForms 数据绑定不提供转换器,视图模型的作用就更加重要——视图模型需要完成所有的转换。为了避免与 MVVM 中的视图模型混淆,我将这个角色称为绑定助手。它位于模型对象和 UI 控件之间,提供直接可绑定的属性。例如,让我们再次考虑三种选择中的一种,但这次让它成为一个引用业务数据的模型对象
class FavouriteColour {
public enum ColourName { Red, Green, Blue };
public ColourName Value;
}
为了将此绑定到 WinForms 中的单选按钮,我们需要将枚举值转换为可以数据绑定的布尔属性
class FavouriteColourBindingHelper {
public FavouriteColour Model {get; set;}
public bool IsRed {
get { return Model.Value == FavouriteColour.ColourName.Red; }
set { if(value) Model.Value = FavouriteColour.ColourName.Red; }
}
public bool IsGreen {
get { return Model.Value == FavouriteColour.ColourName.Green; }
set { if(value) Model.Value = FavouriteColour.ColourName.Green; }
}
public bool IsBlue {
get { return Model.Value == FavouriteColour.ColourName.Blue; }
set { if(value) Model.Value = FavouriteColour.ColourName.Blue; }
}
}
...现在可以绑定控件到帮助器类的实例
var helper = new FavouriteColourBindingHelper();
helper.Model = ourModelColourObject;
radioButtonRed.DataBindings.Add("Checked", helper, "IsRed");
radioButtonGreen.DataBindings.Add("Checked", helper, "IsGreen");
radioButtonBlue.DataBindings.Add("Checked", helper, "IsBlue");
由于您只能将属性绑定到属性,因此对于这种类型的转换,不幸的是无法避免属性声明的复制粘贴(除非使用反射并发出具有属性的临时类型,或者声明动态类,但这更不清楚)。但是,通过使用一个清晰明了的绑定助手,并且为每个您希望绑定的 UI 控件属性都提供一个绑定助手属性,这并不比在 InitializeComponent
方法中设置 UI 属性的代码差,例如。
通知
上面的简单示例忽略了一个重要问题:更改通知。数据绑定的主要好处之一是,如果两个 UI 控件是同一数据的表示,您就不需要在 UI 层中包含意大利面条式代码来管理这些依赖项——相反,在一个控件中所做的更改会通过数据传递,然后传递给依赖于它的其他控件。
让我们升级喜欢的颜色示例,也为列表或组合框提供一个绑定属性
class FavouriteColourBindingHelper {
... // Same as before
public int SelectedIndex {
get { return (int)Model.Value; }
set { Model.Value = (FavouriteColour.ColourName)value; }
}
}
如果您绑定 SelectedIndex
并同时绑定布尔属性,您会发现(除非您手动刷新绑定)在一个地方更改喜欢的颜色不会正确地更新另一个地方。这是因为没有通知机制可以让控件知道何时应该重新绑定。
INotifyPropertyChanged
框架通过 INotifyPropertyChanged
接口提供了解决方案。如果您将数据绑定到 INotifyPropertyChanged
的属性,数据绑定实现会监听 PropertyChanged
事件,并在该事件触发时刷新绑定。
该接口只定义了事件,所以我喜欢定义一个基类,绑定助手可以从中继承,以简化生活
public abstract class BaseNotifier : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify(string propName) {
// By assigning to a local first, the check becomes thread safe
PropertyChangedEventHandler h = PropertyChanged;
if(h != null) h(this, new PropertyChangedEventArgs(propName));
}
protected void NotifyAll() { Notify(null); }
protected void Notify(params string[] properties) {
foreach(string propName in properties) Notify(propName);
}
}
然后绑定助手可以继承自此类,属性设置器可以通知任何相关属性。再次,不幸的是,没有办法自动声明这一点,并且每个属性设置器中添加一个方法调用的结果会产生凌乱的代码——您可能听说过“INotifyPropertyChanged
代码异味”,这就是它所指的。但是,至少通过将其保留在绑定助手内,它是清晰、明显且隔离良好的。
class FavouriteColourBindingHelper : BaseNotifier {
public bool IsRed {
get { return Model.Value == FavouriteColour.ColourName.Red; }
set {
if(value) {
Model.Value = FavouriteColour.ColourName.Red;
NotifyAll();
}
}
}
... // etc, similarly for the other properties
}
在这个简单的例子中,我们希望在任何更改发生时通知所有属性的更改,但该调用更准确的写法是
Notify("IsRed", "IsGreen", "IsBlue", "SelectedIndex");
...由于这组属性将始终一起修改,因此在实际应用程序中,最好使用常量数组或调用该组的一行方法。
在模型上进行通知?
任何形式的模型、视图和第三方组件结构(MVC、MVP、MVVM 或此结构)都会遇到模型类是否应支持通知,还是应通过单个绑定层实例进行所有数据操作的问题。
这个问题的答案因情况而异。仅在绑定层(绑定助手或视图模型)中实现通知在关注点分离方面更清晰。但是,这意味着您的模型无法修改自身状态,或响应外部事件修改其状态,而这些都是相当常见的需求。所以通常您也需要在模型类中实现通知。在这种情况下,绑定助手应挂接到其模型对应项的 PropertyChanged
事件,并在每种情况下通知正确的助手属性组。
列表
在许多情况下,例如数据网格或列表视图,将控件绑定到可绑定对象的列表非常有用。这意味着您需要一个绑定接口,该接口将 IList<ModelObject>
映射到 IList<ModelObjectBindingHelper>
。最佳方法取决于列表是否会更改,绑定是读写还是仅读,以及所有更改是否可以通过绑定助手列表发送。
最简单的方法是,对于一个不会改变的列表,只需在绑定时创建并填充一个列表
var bindingList = new List<ModelObjectBindingHelper>();
foreach(ModelObject obj in modelList) bindingList.Add(new ModelObjectBindingHelper(obj));
// Or, if using LINQ:
var bindingList = modelList.Select(o => new ModelObjectBindingHelper(o)).ToList();
请注意,这仍然允许列表的单个元素被读写绑定。如果从源列表中添加或删除了项,您可以手动对绑定的列表执行相同的操作。
如果列表将通过用户交互进行编辑,最好创建一个传递式 IList
实现,该实现会将添加和删除请求传递给模型列表,并为其中包含的对象创建绑定助手。
这些方法比用于单个绑定对象的中间辅助类要干净得多,如果您需要将列表绑定到控件,值得考虑以这样一种方式编写您的模型类,即您不需要绑定助手。网格和列表视图通常期望简单的属性,在大多数情况下不需要进行转换。