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

在 WPF MVVM 应用程序中重置 ViewModel,无需视图代码隐藏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (11投票s)

2011 年 2 月 16 日

CPOL

7分钟阅读

viewsIcon

116262

downloadIcon

1044

本文和代码的核心思想是提供一种简单的方法来重置 ViewModel,而无需在视图中调整 DataContext 引用。

引言

本文和代码的核心思想是提供一种简单的方法来重置 ViewModel,而无需在视图中调整 DataContext 引用。一切都始于一个 MSDN 论坛帖子,其中有人询问是否有简单的方法可以清空 ViewModel。他觉得清空 ViewModel 中的每个属性太麻烦,并认为如果能有一个“取消”按钮将 DataContext 重置为新的 ViewModel 实例会更简单。而缺点(可以说是)是他必须在视图中有实际的代码(“取消”按钮的处理程序)来执行此操作。这个问题引起了我的兴趣,我认为设计一组类来使这个过程无缝进行会很有趣。

在设计这个类时,我牢记了两个基本要求:

  1. 开发人员应该能够重置 ViewModel,而无需在视图中编写任何代码。
  2. 这个类应该是透明的,所以用户无需以任何方式更改其现有的 ViewModel 即可使用它。

此代码仅适用于 WPF,并且我已经用 .NET 4.0(完整版和客户端配置文件)以及 3.5(完整版和客户端配置文件)进行了测试。演示项目是为 Visual Studio 2010 准备的,但由于它兼容 .NET 3.5,因此您也可以轻松地在 Visual Studio 2008 中使用它。不幸的是,此代码无法在 Silverlight 上运行,因为它使用的类(如 TypeDescriptionProvider)在 Silverlight 中(即使是 4.0 版本)也不可用。

而且,以免有人认为这是又一个过度设计的 MVVM 类,专门针对那些对避免在视图中使用代码有洁癖的人,我认为这个类在它避免在视图中使用代码的能力之外也具有价值。*坏笑*

为什么还要使用这个类?

Basarat Ali Syed 曾好奇我为什么还需要编写这个类。他的原话是:

为什么不直接使用 ViewModel 定位器(你正在使用)并结合 Messenger(也称为中介者)来将消息从当前视图传递给 ViewModel 定位器以设置新属性。

这大致是我当时回复他的内容:

虽然人们可以按自己的意愿去做,但并非所有人都喜欢这种方法,原因如下:

  1. 几乎不可能避免代码隐藏,因为视图必须触发更新 ViewModel 的需求。
  2. 您可能需要在 ViewModel 中有一个视图引用(这会破坏基本的 MVVM)。即使您使用中介者,那也只是回到视图的一个间接层面。
  3. 设置新属性很麻烦,特别是对于复杂的 ViewModel 类。

我的类实现了这一点:

  1. 您基本上重新实例化了 ViewModel。(这相当于 C++ 中拥有神奇的能力,可以使用相同的 this 指针指向一个全新的实例,但地址相同)
  2. 视图/原始 ViewModel 无需了解发生了什么(意味着该类易于插入和拔出)。
  3. 视图中没有代码隐藏。
  4. 无需添加中介者/中间类进行通信。

最后,重要的是要记住,这并不是那种您将随处、随时使用的类。它有一个特定的设计目的,并且能很好地完成其角色。欢迎通过下方的论坛提出更多问题。

将该类用于现有的 MVVM 应用程序

假设您有一个现有的 MVVM 应用程序,其中 MainWindow 视图绑定到一个 MainViewModel 实例。

<Window x:Class="ResettableViewModelDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ResettableViewModelDemo"
        Title="Resettable View Model Demo Application" Height="350" Width="329" ResizeMode="NoResize"
        DataContext="{Binding Source={x:Static local:ViewModels.MainViewModel}}" Background="#FF485093">
    <Grid Margin="5" TextBlock.Foreground="White" TextBlock.FontSize="15">
        <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
            <StackPanel Orientation="Horizontal">
                <TextBlock Width="90" VerticalAlignment="Center">Item Code</TextBlock>
                <TextBox Text="{Binding ItemCode}" Width="197" Margin="8" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Width="90" VerticalAlignment="Center">Description</TextBlock>
                <TextBox Text="{Binding Description}" Width="196" Margin="8" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Button Command="{Binding AddCommand}" Width="90" Margin="5">Add</Button>
                <Button Command="{Binding ExitCommand}" Width="90" Margin="5">Exit</Button>
            </StackPanel>
            <ListBox ItemsSource="{Binding Entries}" Height="180" Width="293" />
        </StackPanel>
    </Grid>
</Window>

请注意,DataContext 是如何设置为 ViewModels 类中的一个静态属性的。

class ViewModels
{
    private static object mainViewModel = new MainViewModel();

    public static object MainViewModel
    {
        get { return ViewModels.mainViewModel; }
    }
}

MainViewModel 将是您现有的 ViewModel 实现。

class MainViewModel : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  private void FirePropertyChanged(string propertyName)
  {
      PropertyChangedEventHandler handler = PropertyChanged;

      if (handler != null)
      {
          handler(this, new PropertyChangedEventArgs(propertyName));
      }
  }        

  private int itemCode = -1;

  public int ItemCode
  {
      get
      {
          return itemCode;
      }

      set
      {
          if (itemCode != value)
          {
              itemCode = value;
              FirePropertyChanged("ItemCode");
          }
      }
  }

  private string description = "unknown";

  public string Description
  {
      get
      {
          return description;
      }

      set
      {
          if (description != value)
          {
              description = value;
              FirePropertyChanged("Description");
          }
      }
  }

  private ICommand exitCommand;

  public ICommand ExitCommand
  {
      get
      {
          return exitCommand ?? (exitCommand = new DelegateCommand(() =>
          {
              Application.Current.MainWindow.Close();
          }));
      }
  }

  private ICommand addCommand;

  public ICommand AddCommand
  {
      get
      {
          return addCommand ?? (addCommand = new DelegateCommand(() =>
          {
              entries.Add(new { ItemCode = ItemCode, Description = Description });
          }));
      }
  }

  private ObservableCollection<object> entries = new ObservableCollection<object>();

  public ObservableCollection<object> Entries
  {
      get { return entries; }
  }

}

现在,假设您有一个新需求,要在视图中添加一个“取消”按钮。单击“取消”按钮必须将两个 TextBox 重置为默认值,并且还需要清空 ListBox。在没有 ResettableViewModel 类的情况下,您有两种选择:要么通过一个“取消”命令方法重置所有绑定的属性,要么在视图中添加代码隐藏来将 DataContext 重置为 MainViewModel 类的一个全新实例。前者对于复杂的 ViewModel 来说可能工作量很大,而且如果您添加/更改绑定的属性或集合,您需要记住也要重置它们。后者虽然简单,但许多 MVVM 精英听到这种做法都会皱眉头。哦,这难道不是一个难题吗?现在,隆重推出 ResettableViewModel 类!

将所需的源文件添加到项目中(或者,添加一个包含这些类的程序集的引用)后,您只需要做两件小事。第一项任务是将静态属性更改为返回 ResettableViewModel 实例。(**注意**:如果您以其他方式设置 DataContext,则需要进行相应的更改以执行类似操作)。

class ViewModels
{
  private static object mainViewModel = 
      new NSViewModelExtensions.ResettableViewModel(new MainViewModel());

  public static object MainViewModel
  {
      get { return ViewModels.mainViewModel; }
  }
}

现在,在 XAML 中添加一个“取消”按钮,并按如下方式绑定其 Command

<StackPanel Orientation="Horizontal">
  <Button Command="{Binding ResetCommand}" Width="90" Margin="5">Cancel</Button>
  <Button Command="{Binding AddCommand}" Width="90" Margin="5">Add</Button>
  <Button Command="{Binding ExitCommand}" Width="90" Margin="5">Exit</Button>
</StackPanel>

请注意,“取消”按钮的命令如何绑定到 ResetCommand。这是由 ResettableViewModel 类提供的,其他一切将继续来自您的 MainViewModel 类。就这样,您就完成了!现在运行应用程序,单击“取消”将把您的 ViewModel 重置为其初始状态。这并没有花费太多精力,不是吗?

自定义 ViewModel 初始化

在某些情况下,您的 ViewModel 可能没有无参构造函数,或者您需要设置一些初始属性。对于这些场景,该类提供了一个接受 Func<object> 委托的构造函数,该委托将用于创建 ViewModel 实例。这是一个简单的示例:

object mainViewModel = new NSViewModelExtensions.ResettableViewModel(
                () => new MainViewModel());

这是一个更典型的示例:

object mainViewModel = new NSViewModelExtensions.ResettableViewModel(
    () => 
        {
            return new MainViewModel()
            {
                ItemCode = 999,
                Description = "Default Item"
            };
        });

一个注意事项

如果您的 ViewModel 有一个 ResetCommand 属性,它将被 ResettableViewModel 类中的 ResetCommand 属性覆盖。虽然这可能是一个非常罕见的情况,但如果您遇到了,那么解决方法是重命名您的属性 - 很抱歉!

实现细节

简而言之,ResettableViewModel 类充当实际 ViewModel 实例的代理。ResettableViewModel 类新添加的唯一 API 是重置命令,其他所有内容都来自底层 ViewModel。它通过实现自定义 TypeDescriptionProvider 来实现这一点,该提供程序动态地为 ResettableViewModel 实例提供从底层 ViewModel 对象获取的属性。我将快速回顾涉及的各个类,尽管代码写得相当清晰,但我会在必要的地方添加注释。这是 ResettableViewModel 类的样子。

[TypeDescriptionProvider(typeof(ResettableViewModelTypeDescriptionProvider))]
sealed class ResettableViewModel : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  private void FirePropertyChanged(string propertyName)
  {
      PropertyChangedEventHandler handler = PropertyChanged;

      if (handler != null)
      {
          handler(this, new PropertyChangedEventArgs(propertyName));
      }
  }

  private static string ErrorViewModelTypeHasToMatch = 
    "The type of the new View Model has to match that of the old View Model.";

  private Func<object> creatorMethod;

  public ResettableViewModel(object innerViewModel, Func<object> creatorMethod = null)
  {
      this.InnerViewModel = innerViewModel;
      this.creatorMethod = creatorMethod;
  }

  public ResettableViewModel(Func<object> creatorMethod)
  {
      this.InnerViewModel = (this.creatorMethod = creatorMethod)();            
  }

  public ResettableViewModel(Type innerViewModelType)
  {
      this.InnerViewModel = Activator.CreateInstance(innerViewModelType);
  }

  internal object InnerViewModel { get; private set; }

  private ICommand resetCommand;

  public ICommand ResetCommand
  {
      get
      {
          return resetCommand ?? (resetCommand = new InternalDelegateCommand(() =>
              {                        
                  if (creatorMethod == null)
                  {
                      this.InnerViewModel = Activator.CreateInstance(
                          this.InnerViewModel.GetType());
                  }
                  else
                  {
                      var newViewModel = creatorMethod();
                      
                      if (this.InnerViewModel.GetType() != newViewModel.GetType())
                      {
                          throw new InvalidOperationException(
                              ResettableViewModel.ErrorViewModelTypeHasToMatch);
                      }

                      this.InnerViewModel = newViewModel;
                  }
                  
                  FirePropertyChanged(String.Empty);
              }));
      }
  }

  class InternalDelegateCommand : ICommand
  {
      private readonly Action executeMethod;

      public InternalDelegateCommand(Action executeMethod)
      {
          this.executeMethod = executeMethod;
      }

      public void Execute(object parameter)
      {
          if (this.executeMethod != null)
          {
              this.executeMethod();
          }
      }

      public bool CanExecute(object parameter)
      {
          return true;
      }

      public event EventHandler CanExecuteChanged;
  }
}

请注意,该类具有与之关联的 TypeDescriptionProvider,类型为 ResettableViewModelTypeDescriptionProvider(如下所示)。我有一个私有内部类实现了 ICommand 的原因是,我不想对任何外部命令类产生任何依赖。这样,无论您使用的是哪个 MVVM 框架,ResettableViewModel 类都能顺利运行,因为它有自己的命令类。

更新所有绑定的专业技巧

请注意对 FirePropertyChanged 的调用,参数为空字符串,这将更新视图中的每个绑定。

ResettableViewModelTypeDescriptionProvider 类很简单,它基本上为我们提供了一种为我们的类指定自定义类型描述符的方法。

class ResettableViewModelTypeDescriptionProvider : TypeDescriptionProvider
{
    private static TypeDescriptionProvider defaultTypeProvider = 
        TypeDescriptor.GetProvider(typeof(ResettableViewModel));

    public ResettableViewModelTypeDescriptionProvider()
        : base(defaultTypeProvider)
    {
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(
        Type objectType, object instance)
    {
        ICustomTypeDescriptor defaultDescriptor = 
            base.GetTypeDescriptor(objectType, instance);
        return instance == null ? defaultDescriptor : 
          new ResettableViewModelCustomTypeDescriptor(defaultDescriptor, instance);
    }
}

ResettableViewModelCustomTypeDescriptor 是我们将内部类的属性与我们的 *代理* 类关联起来的地方(这里的“代理”一词用得很松散)。

class ResettableViewModelCustomTypeDescriptor : CustomTypeDescriptor
{
  public ResettableViewModelCustomTypeDescriptor(ICustomTypeDescriptor parent, object instance)
      : base(parent)
  {
      customFields.AddRange(TypeDescriptor.GetProperties(
        ((ResettableViewModel)instance).InnerViewModel)
          .Cast<PropertyDescriptor>()
          .Select(pd => new ResettableViewModelCustomField(
                pd.Name, pd.PropertyType))
          .Select(cf => new ResettableViewModelCustomFieldPropertyDescriptor(
                cf)).Cast<PropertyDescriptor>());
  }

  private List<PropertyDescriptor> customFields = new List<PropertyDescriptor>();

  public override PropertyDescriptorCollection GetProperties()
  {
      return new PropertyDescriptorCollection(base.GetProperties()
              .Cast<PropertyDescriptor>()
              .Union(customFields)
              .ToArray());
  }

  public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
  {
      return new PropertyDescriptorCollection(base.GetProperties(attributes)
              .Cast<PropertyDescriptor>()
              .Union(customFields)
              .ToArray());
  }
}

ResettableViewModelCustomField 是一个简单的类,表示一个属性。它用于存储底层 ViewModel 实例中所有属性的属性名和属性类型。

class ResettableViewModelCustomField
{
  public ResettableViewModelCustomField(String name, Type dataType)
  {
      Name = name;
      DataType = dataType;
  }

  public String Name { get; private set; }

  public Type DataType { get; private set; }
}

ResettableViewModelCustomFieldPropertyDescriptor 是我们实际从底层实例获取属性值并将其返回给任何询问外部类实例上这些属性的人的地方。

class ResettableViewModelCustomFieldPropertyDescriptor : PropertyDescriptor
{
  public ResettableViewModelCustomField CustomField { get; private set; }

  public ResettableViewModelCustomFieldPropertyDescriptor(
    ResettableViewModelCustomField customField)
      : base(customField.Name, new Attribute[0])
  {
      CustomField = customField;
  }

  public override bool CanResetValue(object component)
  {
      return true;
  }

  public override Type ComponentType
  {
      get 
      {
          return typeof(ResettableViewModel);
      }
  }

  public override object GetValue(object component)
  {
      return GetInnerPropertyInfo(component).GetValue(
        ((ResettableViewModel)component).InnerViewModel, new object[0]);
  }

  public override bool IsReadOnly
  {
      get 
      {
          return false;
      }
  }

  public override Type PropertyType
  {
      get
      {
          return CustomField.DataType;
      }
  }

  public override void ResetValue(object component)
  {
      this.SetValue(component, Activator.CreateInstance(CustomField.DataType));
  }

  public override void SetValue(object component, object value)
  {
      GetInnerPropertyInfo(component).SetValue(
        ((ResettableViewModel)component).InnerViewModel, value, new object[0]);
  }

  public override bool ShouldSerializeValue(object component)
  {
      return false;
  }

  private PropertyInfo propertyInfo;

  private PropertyInfo GetInnerPropertyInfo(object component)
  {
      return propertyInfo ?? (propertyInfo = ((ResettableViewModel)component)
        .InnerViewModel.GetType().GetProperty(this.CustomField.Name));
  }
}

上面代码片段中突出显示的代码是我们获取和设置底层 ViewModel 实例上的属性的地方。

结论

就这样,我想。一如既往,请随时通过文章论坛向我提供反馈和批评。您投的 5 票将非常感激,每获得 5 票,我将享用一杯单一麦芽威士忌,然后再多喝几杯!

历史

  • 2011 年 2 月 16 日 - 文章首次发布。
  • 2011 年 2 月 17 日 - 添加了更好地解释该类设计原因的文本。
© . All rights reserved.