65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (25投票s)

2012年5月2日

CPOL

6分钟阅读

viewsIcon

52404

downloadIcon

1

将代码隐藏切换为 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创建的第一部分。我们只需要做的是

  1. 将类名从FirstView更改为FirstViewModel
  2. 将命名空间更改为ViewModels。
  3. 删除调用InitializeComponent的构造函数(此方法显然不适合ViewModel)。
  4. 实现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,以便我们能够看到一切是否正常工作。

如果我们现在查看设计器预览,我们将看到它确实如此

image

别高兴得太早……

这时事情变得复杂起来。如果我们不加注意,就会制造一个糟糕的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>

并运行应用程序,我们将看到……什么都没有。不知道为什么它不起作用……

image

尽管如此,在设计时,它*确实*起作用

image

那么是什么原因呢?为什么它在运行时不起作用,尽管在设计时*确实*起作用?!

每个有经验的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解析器创建的……

image

所以我们需要在运行时调用该方法。该方法在运行时存在,但在设计时不存在,因此我们无法调用它,因为它不会编译。没有什么是通过反射解决不了的!

this.GetType().GetMethod("InitializeComponent").Invoke(this, null);

如果我们现在运行应用程序,我们将看到它正在工作! 

image

只有当我们回到Visual Studio时,才会看到它现在在运行时工作,但是……在设计时却不行。

 image

现在发生的情况是我们尝试调用InitializeComponent,它在运行时有效,但在设计时失败。这很容易解决——我们只需要确保不在设计时调用它

public class ViewBase : UserControl
{
    public ViewBase()
    {
        if (!DesignerProperties.GetIsInDesignMode(this))
            this.GetType().GetMethod("InitializeComponent").Invoke(this, null);
    }
}

现在它将完美地在设计时和运行时都起作用!

值得付出努力吗? 

总而言之,第一次编写ViewBase之后,第二次使用它将非常容易。这为我们带来了一些非常酷且实用的功能

  1. 我们的ViewModel现在位于解决方案资源管理器中View的下方。 IMO非常有帮助。
  2. 不再有冗余的CodeBehind!
  3. 如果我们正在XAML文件中,要转到ViewModel文件,只需按F7键!(遗憾的是,反过来不起作用,但仍然是很大的进步。)

在我看来,这绝对是值得的。MVVM的很大一部分目标是让工具更好地工作,而本文正是朝这个方向努力的。

我建议下载代码并进行尝试。它对您有用吗?

完整的代码可以在此处下载。

© . All rights reserved.