将 XAML 控件直接绑定到 ViewModel 方法






4.73/5 (5投票s)
在本文中,我将展示如何将控件直接绑定到 ViewModel 方法,从而简化 XAML UI 的开发,并完全消除手动处理 Commands 的工作。
典型问题
使用 XAML (WPF, Silverlight) 进行客户端开发时,通常会采用 MVVM 模式。这意味着 ViewModel 的业务方法不是直接通过事件(如 WinForms 和其他技术中的方式)绑定到 XAML 视图,而是绑定到 ViewModel 需要发布的 Commands。
要调用 ViewModel 方法,您需要发布一个 Command,该 Command 一方面绑定到控件,另一方面绑定到 ViewModel 方法。这样,当点击控件时,相应的命令会直接从 Command 调用该方法。
这种方法可以确保代码的正确运行。然而,在最常见的实现中使用它既不方便又“冗长”。直接使用方法是正确的,因为方法包含了所需的业务逻辑,而 Command 仅用于符合 MVVM 模式。因此,对于每个方法,您都必须创建一个调用它的 Command,而这个 Command 的代码不包含任何额外的逻辑。尽管这段代码非常简单,但它仍然占用了一席之地,并增加了代码的复杂性。然后,您还必须将两个 ViewModel 方法绑定到这个 Command——一个执行 Command 的方法,另一个检查 Command 是否可以执行的方法。
如果您的 ViewModel 很简单,代码量不多,那么这种方法是可以接受的。然而,当您编写大型 UI 时,您肯定希望节省时间和精力,并自动化这个过程。
提出的解决方案
我们如何移除冗余代码并简化事情?我们需要确保控件直接绑定到方法,而不是 Command。此时,我们还必须保留绑定到 Commands 的两个最重要的优点:
- 当 Command 不可用时,自动禁用控件。
- 将多个控件绑定到同一个 Command。
现在我想展示一下我们如何实现这一点。
首先,让我简单回顾一下负责连接控件和 ViewModel Commands 的绑定原理。
视图的属性之一是 DataContext。当我们为 ViewModel 实例分配 DataContext 时,基础设施会使用反射探索 ViewModel 元数据,并获取公共属性列表。然后,根据绑定表达式,这些公共属性会被分配给 XAML 中的控件。只有当公共属性的名称与绑定表达式中指定的名称相同时,并且公共属性的类型是 ICommand 时,绑定才会成功。
由于我们希望节省时间并摆脱手动工作,我们需要教 ViewModel 如何“欺骗”Binding,让 ViewModel 返回动态生成的属性(Command 类型),并且这些属性一方面会自动绑定到 XAML 控件,另一方面会自动绑定到必要的方法。
使用 Binding 时,基础设施通过 TypeDescriptor 类接收 ViewModel 的元数据。我们可以利用这一点来欺骗基础设施,因为我们可以改变 TypeDescriptor 的行为,使其对我们有利。这是通过实现 ICustomTypeDescriptor 接口来实现的。
当使用命名约定时,这个解决方案非常简单:也就是说,控件绑定的方法名称必须与 XAML 中的绑定表达式名称相同。
实现示例
解决方案结构如下图所示。
我开始编码时编写了验收测试。
当点击“Message”按钮时,
然后在文本框中显示“Hello, World!”。
这是验收测试代码。
[CodedUITest]
public class CommandToMethodBindingTests
{
private UIMap map;
private ApplicationUnderTest application;
[TestInitialize]
public void SetupTest()
{
var autName = ConfigurationManager.AppSettings["ApplicationUnderTest"];
application = ApplicationUnderTest.Launch(autName);
}
[TestMethod]
public void When_button_is_pressed_Then_messagebox_contains_a_HelloWorld_message()
{
UIMap.ClickButton();
UIMap.AssertMessageIsChanged();
}
[TestCleanup]
public void CleanupTest()
{
application.Close();
}
public TestContext TestContext { get; set; }
public UIMap UIMap
{
get
{
if ((this.map == null))
{
this.map = new UIMap();
}
return this.map;
}
}
}
这是视图。
public partial class DialogView : Window
{
public DialogView()
{
InitializeComponent();
var viewModelFactory = new AutobindViewModelFactory();
var viewModel = viewModelFactory.Create(this);
DataContext = viewModel;
}
}
这是 ViewModel。
public class DialogViewModel : AutobindViewModel
{
public DialogViewModel(IBindableMethodFinder methodFinder, IMethodToCommandConverter methodToCommandConverter, IPropertyDescriptorMapper propertyDescriptorMapper)
: base(methodFinder, methodToCommandConverter, propertyDescriptorMapper)
{
}
public string Message { get; set; }
public void ShowMessage()
{
Message = "Hello, World!";
OnPropertyChanged("Message");
}
}
让我们从单元测试开始,看看这个基础设施是如何工作的。
这里有一个测试方法,它检查基础设施是否按预期运行,即返回一个 ICommand 类型的属性集合,其中只包含一个属性 - ShowMessage。
此测试创建一个 ViewModel 实例,然后请求 TypeDescriptor 类获取 ViewModel 属性的集合。
[TestMethod]
public void Если_запросить_у_вьюмодели_список_свойств_То_в_нем_будет_свойство_ShowMessage_типа_ICommand()
{
// arrange
var expectedPropertyMetadata = new { Name = "ShowMessage", Type = typeof(ICommand) };
// act
var viewModelProperties = TypeDescriptor.GetProperties(viewModel);
var actualProperty = viewModelProperties[expectedPropertyMetadata.Name];
// assert
Assert.AreEqual(expectedPropertyMetadata.Name, actualProperty.Name);
Assert.AreEqual(expectedPropertyMetadata.Type, actualProperty.PropertyType);
}
现在让我们来看看 DialogViewModel 类。正如您所见,这是一个非常简单的类。它包含 ShowMessage 方法,并且继承自 AutobindViewModel。仔细查看代码。
public abstract class AutobindViewModel : ViewModel, ICustomTypeDescriptor { private readonly IBindableMethodFinder methodFinder; private readonly IMethodToCommandConverter methodToCommandConverter; private readonly IPropertyDescriptorMapper propertyDescriptorMapper; protected AutobindViewModel(IBindableMethodFinder methodFinder, IMethodToCommandConverter methodToCommandConverter, IPropertyDescriptorMapper propertyDescriptorMapper) { this.methodFinder = methodFinder; this.methodToCommandConverter = methodToCommandConverter; this.propertyDescriptorMapper = propertyDescriptorMapper; } #region Implementation of ICustomTypeDescriptor PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() { var propertyDescriptors = GetPropertyDescriptors(); return new PropertyDescriptorCollection(propertyDescriptors); } #endregion Implementation of ICustomTypeDescriptor private PropertyDescriptor[] GetPropertyDescriptors() { var methodsThatCanBindToCommands = methodFinder.FindMethodsFrom(this); var commandsToBind = ConvertMethodsToCommands(methodsThatCanBindToCommands); return propertyDescriptorMapper.MapToCollection(commandsToBind, this); } private IEnumerableConvertMethodsToCommands(IEnumerable methodsToBind) { return from method in methodsToBind let command = methodToCommandConverter.Convert(method, this) select new CommandInfo(method.Name, command); } }
因此,该类实现了 AutobindViewModel 接口 ICustomTypeDescriptor。为了让基础设施按我们想要的方式工作,只需实现该接口的一个方法即可,即 GetProperties 方法,该方法生成并返回一个动态生成的属性集合。
在 GetProperties 方法内部,我们调用 GetPropertiesDescriptor 方法。它检索 ViewModel 方法的集合(可以绑定到 Commands)。该集合通过 ConvertMethodsToCommands 方法转换为 Commands。
AutobindViewModel 类使用了三个服务。
第一个服务实现了 IBindableMethodFinder 接口。
它有一个 DefaultMethodFinder 实现,该实现枚举 ViewModel 中的所有方法,并从中找到可以绑定到命令的方法。您可以看到,一切都非常简单。
public class DefaultMethodFinder : IBindableMethodFinder { private readonly FuncbindableMethodCriteria = method => method.IsPublic && !method.IsAbstract && !method.IsConstructor && !method.IsStatic && method.GetParameters().Length <= 1 && method.ReturnType == typeof(void) && !method.Attributes.HasFlag(MethodAttributes.SpecialName); #region Implementation of IBindableMethodFinder public IEnumerable FindMethodsFrom(AutobindViewModel autobindViewModel) { return autobindViewModel .GetType() .GetMethods() .Where(bindableMethodCriteria); } #endregion Implementation of IBindableMethodFinder }
第二个服务实现了 IMethodToCommandConverter 接口。它只包含一个 Convert 方法,该方法接收 MethodInfo 和 ViewModel,并返回一个 Command 实例。
public class DefaultMethodToCommandConverter : IMethodToCommandConverter { #region Implementation of IMethodToCommandConverter public ICommand Convert(MethodInfo method, AutobindViewModel viewModel) { var arguments = new object[0];//TODO: потом понадобится передача параметров. Action<object> executeAction = o => method.Invoke(viewModel, arguments); return new DelegateCommand(executeAction); } #endregion Implementation of IMethodToCommandConverter }</object>
最后,第三个服务实现了 IPropertyDescriptorMapper 接口。它只包含一个 MapToCollection 方法,它生成一个 PropertyDescriptors 数组,我们使用它来处理 ICustomTypeDescriptor 接口。
public class DefaultPropertyDescriptorMapper : IPropertyDescriptorMapper { #region Implementation of IPropertyDescriptorMapper public PropertyDescriptor[] MapToCollection(IEnumerablecommandsToBind, AutobindViewModel viewModel) { return commandsToBind .Select(info => new AutobindPropertyDescriptor(info.Name, info.Command)) .ToArray(); } #endregion Implementation of IPropertyDescriptorMapper }
这就是实现这个想法所需的所有代码。正如您所看到的,这是一个非常简单的解决方案,它消除了大量的代码,并且由于疏忽造成的错误。
下图显示了支持自动绑定的基础设施的所有主要类。
结论和展望
您在本文章中看到了一个非常简单的解决方案,展示了如何将控件直接绑定到 ViewModel 方法,从而简化 XAML UI 的开发,并完全消除手动处理 Commands 的工作。
我为本文选择的示例相当简单,因此我想简要说明一下在更大、更复杂的项目中需要使用此方法进行哪些改进。
- 1. 显然,有时命名约定是不够的(当方法名由于某些原因无法与绑定表达式匹配时)。然后您可以添加一个特殊属性来标记方法。
- 2. 在实际项目中,您可能还需要支持 CanExecute 方法,该方法决定命令是否可以执行。
- 3. 有时您可能需要确保向 Command 传递参数。