如何让 WPF 的行为就像原生支持 MVVM 一样






4.85/5 (25投票s)
将代码隐藏切换为 ViewModel 以获得更简单的开发工作流程
引言
我经常讲授MVVM,几乎总有人问为什么WPF“开箱即用地”不支持它。
如果我们仔细想想,CodeBehind和ViewModel在功能上非常相似。它们都包含UI背后的逻辑,唯一的区别在于ViewModel是一个完全不同的类,这有助于在逻辑(ViewModel)和UI(View)之间建立更好的分离。
当你实现MVVM时,Visual Studio并不知道View和ViewModel之间的连接,因此你无法像平时按F7键在XAML和CodeBehind之间切换那样轻松地切换它们。此外,如果Visual Studio能像CodeBehind那样,在解决方案资源管理器中将ViewModel文件“放置”在View文件下方,那将非常有帮助。
如果我们仔细想想,实际的CodeBehind文件我们甚至都不需要!我宁愿一开始就没有它,这样我的团队成员就不会被诱惑去使用它。如果没有CodeBehind文件,你就无法编写CodeBehind,这是一件好事。
将CodeBehind转化为ViewModel
这个想法我已经考虑了很久,我认为我已经找到了一个很好的技巧来实现这一点。我们将把CodeBehind文件转化为ViewModel,这样我们就能“两全其美”!
例如,假设我有一个名为FirstView的View。原始的XAML将如下所示:
<UserControl x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
</Grid>
</UserControl>
这是有趣的一行:
x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
它使得XAML解析器创建了一个名为FirstView
并继承自UserControl
的类。这个类将通过'partial'关键字与CodeBehind类连接。切换到CodeBehind,我们将看到以下代码,如前所述——我们实际上不需要它
namespace ViewModelAsCodeBehindTrick.Views
{
/// <summary>
/// Interaction logic for FirstView.xaml
/// </summary>
public partial class FirstView : UserControl
{
public FirstView()
{
InitializeComponent();
}
}
}
这里我们看到了FirstView
类的第二部分,它连接着由XAML创建的第一部分。我们只需要做的是
- 将类名从FirstView更改为
FirstViewModel
。 - 将命名空间更改为ViewModels。
- 删除调用
InitializeComponent
的构造函数(此方法显然不适合ViewModel)。 - 实现
INotifyPropetyChanged
,以便我们的ViewModel为之后的数据绑定做好准备。
最终,我们的文件将如下所示
namespace ViewModelAsCodeBehindTrick.ViewsModels
{
/// <summary>
/// Interaction logic for FirstView.xaml
/// </summary>
public class FirstViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
}
为了演示,我们将向ViewModel添加一个属性,以便稍后可以从View绑定到它,这样我们就能看到一切是否正常工作。这个属性只是一个文本属性,其值为“Hello MVVM”。
public class FirstViewModel : INotifyPropertyChanged
{
private string someText;
public string SomeText
{
get { return someText; }
set
{
someText = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SomeText"));
}
}
public FirstViewModel()
{
SomeText = "Hello MVVM";
}
public event PropertyChangedEventHandler PropertyChanged;
}
现在剩下要做的就是将View连接到ViewModel。这可以通过所有标准方式完成(通常我使用ViewModelLocator),但在这个例子中,我使用了最简单的方式
<UserControl ... >
<UserControl.DataContext>
<vm:FirstViewModel />
</UserControl.DataContext>
<Grid>
<TextBlock Text="{Binding SomeText}" />
</Grid>
</UserControl>
此外,我还添加了一个绑定到“SomeText”的TextBlock,以便我们能够看到一切是否正常工作。
如果我们现在查看设计器预览,我们将看到它确实如此
别高兴得太早……
这时事情变得复杂起来。如果我们不加注意,就会制造一个糟糕的bug,这个bug只会在运行时显现。如果我们把我们的View(“FirstView”)放在主窗口上
<Window x:Class="ViewModelAsCodeBehindTrick.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:ViewModelAsCodeBehindTrick.Views"
Title="MainWindow" Height="350" Width="525">
<Grid>
<v:FirstView x:Name="firstView1" />
</Grid>
</Window>
并运行应用程序,我们将看到……什么都没有。不知道为什么它不起作用……
尽管如此,在设计时,它*确实*起作用
那么是什么原因呢?为什么它在运行时不起作用,尽管在设计时*确实*起作用?!
每个有经验的WPF程序员的第一反应是立即检查Output窗口,看看是否有任何绑定错误,但这在这里没有帮助。没有任何绑定错误,Output窗口将是干净的。这是一个完全不同的问题。
(我建议花几分钟时间自己尝试找出原因。我也花了些时间…… :-)
.
.
.
.
.
.
.
.
.
.
.
理解XAML解析过程
在编译过程中,XAML文件会生成两个文件
1. FirstView.g.cs – 这里包含FirstView类。这个类加载第二个文件——
2. FirstView.Baml,这是我们的XAML在经过某种编译后(实际上是预标记化——提前解析文件,以便在运行时加载比加载未解析的XML文件更快)
这两个文件的加载和连接是在FirstView.g.cs中的InitializeComponent方法中执行的,只是……现在我们去掉了CodeBehind,没有人调用这个方法。发生的情况是,没有任何东西加载BAML文件,因此一切都保持空白。我们看不到绑定错误,因为甚至连Binding都没有加载。
这里有趣的是,在设计时,Visual Studio会自动加载并运行BAML文件,这就是为什么在设计时它有效的原因。
所以,为了修复这个问题,我们只需要确保在运行时调用这个方法。但如何做呢?
创建一个自动调用InitializeComponent的UserControl
我找到的解决方案是一个很好的技巧。与其让我们的View实现UserControl,不如让它实现一个名为ViewBase的新类。
让我们创建一个名为ViewBase
的新类,它继承自UserControl
public class ViewBase : UserControl
{
}
现在,View将不再直接继承自UserControl
,而是继承自ViewBase
<v:ViewBase x:Class="ViewModelAsCodeBehindTrick.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:ViewModelAsCodeBehindTrick.ViewsModels"
xmlns:v="clr-namespace:ViewModelAsCodeBehindTrick.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<vm:FirstViewModel />
</UserControl.DataContext>
<Grid>
<TextBlock Text="{Binding SomeText}" />
</Grid>
</v:ViewBase>
(当我们把UserControl改为v:ViewBase时,Visual Studio一开始可能不喜欢,并且在几秒钟内不会提供智能提示。这没关系。)
现在,我们只需要让新创建的类(ViewBase
)在构造函数中调用InitializeComponent
。
问题是,如果我们尝试调用InitializeComponent
,会得到一个错误——该方法还不存在。这个方法是在编译时由XAML解析器创建的……
所以我们需要在运行时调用该方法。该方法在运行时存在,但在设计时不存在,因此我们无法调用它,因为它不会编译。没有什么是通过反射解决不了的!
this.GetType().GetMethod("InitializeComponent").Invoke(this, null);
如果我们现在运行应用程序,我们将看到它正在工作!
只有当我们回到Visual Studio时,才会看到它现在在运行时工作,但是……在设计时却不行。
现在发生的情况是我们尝试调用InitializeComponent
,它在运行时有效,但在设计时失败。这很容易解决——我们只需要确保不在设计时调用它
public class ViewBase : UserControl
{
public ViewBase()
{
if (!DesignerProperties.GetIsInDesignMode(this))
this.GetType().GetMethod("InitializeComponent").Invoke(this, null);
}
}
现在它将完美地在设计时和运行时都起作用!
值得付出努力吗?
总而言之,第一次编写ViewBase之后,第二次使用它将非常容易。这为我们带来了一些非常酷且实用的功能
- 我们的ViewModel现在位于解决方案资源管理器中View的下方。 IMO非常有帮助。
- 不再有冗余的CodeBehind!
- 如果我们正在XAML文件中,要转到ViewModel文件,只需按F7键!(遗憾的是,反过来不起作用,但仍然是很大的进步。)
在我看来,这绝对是值得的。MVVM的很大一部分目标是让工具更好地工作,而本文正是朝这个方向努力的。
我建议下载代码并进行尝试。它对您有用吗?