不带绑定的替代 MVVM 实现





5.00/5 (4投票s)
一种替代的 MVVM 实现,它采用一组代理控件类,作为一种“柏拉图式理想”的 WPF 页面,用于与视图模型进行交互。
摘要
本文介绍了一种替代的 MVVM 实现,它采用一组代理控件类,作为一种“柏拉图式理想”的 WPF 页面,用于与视图模型进行交互。实际的 WPF 页面会注册到代理对象,代理对象会搜索它们并建立必要的链接。这种技术允许以标准方式开发 WPF 页面(添加事件处理程序并设置控件属性),然后只需将代码隐藏剪切并粘贴到单独的视图模型类中。
引言
我开发了一个用于 CNC 机床控制的 WPF 富客户端应用程序。操作员屏幕需要显示大量的过程信息,并且必须对机器状态的变化做出实质性的响应。没什么特别之处——主要是改变标签、颜色、可见性和启用属性——但数量很多。WPF 操作员屏幕遵循标准的模式,即在代码隐藏中有大量的事件处理方法,包括按钮单击等控件事件以及来自底层机器状态的“已更改”事件。我的主要操作员屏幕目前在其代码隐藏中包含 2,400 行 C# 代码!
我的大部分工作都涉及为需要显示额外信息或仅仅希望其产品 HMI 具有独特外观的客户制作定制操作员屏幕。显然,切换到 MVVM 模式并将所有代码隐藏逻辑移至视图模型将是一个好主意。
问题在于:我的许多控件都需要为前景色和背景色、标签文本、可见性和启用属性等设置绑定。我的 WPF 页面将需要大量的绑定——还有值转换器、数据模板、ICommand 和 CanExecute……天哪!我更喜欢设置控件属性的标准事件处理方法模式……
void MachineMode_Changed(ChangedEventArgs<EMachineMode> e)
{
switch (e.NewValue)
{
case EMachineMode.Auto:
tbMode.Text = "AUTO";
bdrMode.Background = Background;
btnCycleStart.IsEnabled = true;
btnAbort.IsEnabled = true;
tabMonitor.IsSelected = true;
break;
目标
我想以标准方式开发我的 WPF 页面(添加事件处理程序并设置控件属性)。我想让一个页面正常运行,然后只需将代码隐藏剪切并粘贴到一个单独的视图模型类中,并且让我的应用程序像以前一样运行。我还没结束!我还想能够创建原始 WPF 页面的变体,这些变体也能与同一个视图模型正确运行。既然如此,我还想能够在一个视图模型上同时运行多个页面——并且我希望它们保持同步。唯一remaining的代码隐藏应该实现页面特定的功能,例如动画或客户特定的要求……而且我不想处理命令和数据绑定!
我的解决方案
信不信由你,实现这些宏伟的目标非常简单。我为我的代码隐藏逻辑涉及的 WPF 元素类型(例如 Border
、Button
、TabItem
、TextBlock
、TextBox
等)创建了一组代理控件类。这些代理类维护一个实际控件的底层列表,响应它们的事件,并公开匹配的属性和事件。支持的属性和事件列表不必很长——只需代码隐藏所需的即可(例如 Background
、Foreground
、IsEnabled
、Visibility
、Click
、Text
、TextChanged
等)。
视图模型创建代理控件对象集以匹配代码隐藏的需求,并充当一种“柏拉图式理想”的 WPF 页面,用于与视图模型逻辑进行交互。然后创建实际的 WPF 页面并将其注册到代理对象——代理对象会搜索它们并建立必要的链接。
请注意 ProxyButton
类如何搜索已注册的页面以查找具有匹配名称的 Button
控件。然后它会添加一个 Click
处理程序,该处理程序会将实际按钮的 Click
事件转发给视图模型……
public class ProxyButton : ProxyFrameworkElement
{
// (partial listing)
void Register(Page page)
{
object obj = page.FindName(name);
if (obj is Button)
{
elements.Add(new Element(obj, page));
(obj as Button).Click += Click2;
}
}
public event RoutedEventHandler Click;
private void Click2(object sender, RoutedEventArgs e)
{
if (Click != null)
Click(sender, e);
}
}
请注意 ProxyTextBlock
类如何将新文本转发给它所有的 TextBlock
元素……
public class ProxyTextBlock : ProxyFrameworkElement
{
// (partial listing)
public string Text
{
get
{
return (elements.Count > 0) ? (elements[0] as TextBlock).Text : "";
}
set
{
foreach (var element in elements)
(element as TextBlock).Text = value;
}
}
}
视图模型为“代码隐藏”所需的每个 WPF 元素创建一个代理对象(“代码隐藏”加上引号是因为代码已从 WPF Page
移至视图模型)。
class ViewModel
{
public ViewModel()
{
// Create the proxy controls and add event handlers. (partial listing)
btnStart = new ProxyButton("btnStart");
btnStart.Click += btnStart_Click;
btnStop = new ProxyButton("btnStop");
btnStop.Click += btnStop_Click;
tbStatus = new ProxyTextBlock("tbStatus");
}
}
应用程序创建视图模型的实例,然后将 WPF 页面或页面注册到视图模型。在注册时,每个代理对象使用 WPF 的 FindName()
方法搜索页面中匹配名称和类型的控件。还可以在页面关闭前取消注册页面(以释放引用,使页面可以被垃圾回收)。
ViewModel viewModel = new ViewModel();
var page = new Page1();
viewModel.Register(page);
整个方案中唯一比较花哨的功能是,一个 Button
可能包含一两个 TextBlock
标签。使用反射来搜索 Button
内容中的 TextBlock
。如果没有找到,则将 Button
内容本身设置为新的标签文本。
还有一个代理画笔类,它为每个已注册的页面维护一个命名的画笔资源的底层列表。这使您可以编写……
ProxyBrush brushHighlight = new ProxyBrush("brushHighlight", Brushes.MistyRose);
btnStop.Background = brushHighlight;
一行代码可以将每个已注册页面的 Stop 按钮背景色设置为该页面的命名画笔资源。请注意,可以指定默认画笔,以防找不到该资源。
WPF 页面要求
WPF Page
不再负责 XAML 中的声明性绑定。唯一的要求是,打算与视图模型交互的 UI 元素和画笔资源必须具有正确的名称。例如,如果页面包含一个“Stop”按钮,则必须将其命名为“btnStop”。
WPF Page
仍然可以包含代码隐藏,并且可以包含视图模型未使用的其他控件。事实上,页面可以包含供另一个视图模型使用的第二组控件(令人震惊)。假设我有一台 CNC 机床,它可能包含激光切割机或水射流切割机。我可以为每种切割机创建视图模型,并且只实例化我需要的那个。
示例应用程序
示例应用程序托管了两个不同的 WPF Pages
,它们都注册到一个视图模型实例——因此它们保持同步。可以动态创建每个类型的其他页面——并且在关闭之前它们将保持同步。两个页面的相应控件具有相同的名称。右侧页面仅在样式上与左侧页面不同,并且它没有 Start 按钮。视图模型会简单地忽略缺失或额外的控件。
留给读者的练习
这些代理对象维护一个实际控件的底层列表,响应它们的事件,并向视图模型公开相同的属性。此技术在 Windows Forms 中同样适用。甚至可以创建一个可以同时用于 WPF 和 Forms 的代理控件类集!
我的代理类由于我的应用程序的结构而设计用于与控件的 WPF Pages
一起使用。但是,它们可以很容易地扩展到与 Windows
和 User Controls
一起使用。
此外,如果您实际在应用程序中使用这些代理类,您无疑需要其他类型的控件以及额外的属性和事件支持。您会发现这些类非常容易扩展。我很快就会将它们应用于我的 CNC 应用程序。如果社区对此感兴趣,我打算稍后更新本文——所以请给我留下您的反馈!
结论
让我们回顾一下这种替代 MVVM 技术代理控件类的优势
- 允许以标准方式开发 WPF 页面(添加事件处理程序并设置控件属性),然后将代码隐藏剪切并粘贴到单独的视图模型类中。
- 链接搜索的责任转移到视图模型,从而使 WPF 页面尽可能简单(它们只需要包含视图模型可识别的命名控件)。
- 可以将多个页面注册到一个视图模型实例,它们将保持同步。
- 多个视图模型可以注册同一个页面(该页面可以包含两组命名控件——每组用于一个视图模型)。
- 在没有绑定、值转换器等的情况下,将 UI 与业务逻辑分离。
- 代理类集可以轻松扩展并用于任何 WPF 程序。
属性设置器和事件通过代理对象传递时会有轻微的性能损失。但是,我怀疑即使对于具有大量业务逻辑的复杂应用程序,用户也无法察觉到延迟。
我对自己感到惊讶,因为我认为这个解决方案真的很巧妙——我期待将其应用于我的 CNC 应用程序。这项技术将使我能够轻松地将复杂页面的功能分解为几个更简单的页面,或者我可以为不同类别的用户包含我的主页的几种变体,或者我可以从外部程序集加载主页的自定义版本。
我想提交这篇文章供同行评审。是否存在实现此技术的现有框架?这是否适合作为开源项目?俗话说,天才与疯狂之间只有一线之隔。我是否已经越界了?我欢迎建设性的批评!