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

裸金属 MVVM - 代码与现实的碰撞 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (32投票s)

2016 年 12 月 29 日

CPOL

9分钟阅读

viewsIcon

33952

downloadIcon

296

本系列文章将从头开始讲解 MVVM;不使用任何框架或辅助工具,我们将从基础知识入手学习 MVVM。

引言

关于 MVVM 的文章已经有很多很多了,所以你可能会想,我为什么还要写更多关于它的文章。原因很简单,关于 MVVM 的信息有很多是错误的,我希望本系列文章能帮助大家破除一些关于这个实际上非常直观的模式的迷思。

现在,你可能知道 MVVM 最初是 WPF 应用程序的一种模式,并且它已经从最初的定位扩散到了其他语言、框架和平台。然而,由于这些不同的实现都依赖于一些对我们开发者不可见的底层特性,我们往往会错过 MVVM 为什么是如此有用的开发工具,以及我们应该从哪些特性开始使用 MVVM。因此,在第一篇文章中,我们将抛弃所有那些花哨的平台,并从头开始构建一个控制台应用程序作为 MVVM 应用。是的,你没听错,我们将重新实现一些幕后工作,来展示一些使 MVVM 成为可能的“魔法”。

到本文结束时,我们还不会涵盖关于 MVVM 所需的所有知识,但我们会讲解绑定操作的基础以及 `INotifyPropertyChanged` 的使用。哦,顺便说一句,我们还会谈谈 MVVM 的一些迷思。

系列文章

开发环境

我使用 VS 2015 开发了这个应用程序,并大量使用了 C#6 的特性。如果你没有安装 VS2015,我希望你至少能够跟随我的思路,因为我将在这里解释整个代码库。

模型

我们的应用程序将很简单。用户输入一个名字,如果不是空字符串,我们的视图将显示有关更新的详细信息。为此,我们将使用两个模型。第一个模型将是一个简单的模型,包含一个字符串用于用户输入的名称。这可以说是最基本的数据模型了。

public class PersonModel
{
  private string name;

  public string Name
  {
    get { return name; }
    set
    {
      if (name == value)
      {
        return;
      }
      name = value;
    }
  }
}

从这个实现中可以看出,没有任何东西可以阻止用户输入空字符串,所以我们需要某种形式的验证来确保用户不能错误地设置字符串。首先,让我们看看我们的验证器是什么样的。

public class PersonModelValidator
{
  public bool IsValid(string name)
  {
    if (!string.IsNullOrWhiteSpace(name))
    {
      return true;
    }
    Console.WriteLine("Name cannot be an empty string.");
    return false;
  }
}

同样,这个验证器是一段简单的代码,但我们现在必须回答的问题是,在 MVVM 中,这个验证逻辑应该放在哪里?我们选择将其作为一个单独的类,所以我们需要一个合适的地方来存放它。我们总可以将验证器放在数据模型内部,但我们不希望这样做是有原因的。

  1. 如果这个模型是由 ORM 生成的呢?将一个单一的验证点混合在这里可能是不正确的。
  2. 我们在这里混合了关注点。这个类是一个模型 - 它不真正负责定义什么值是可以接受的,什么是不可以的。这通常是业务逻辑,所以最好将其保留为外部验证,并让它检查值的“适合性”。

因此,我们决定希望我们的验证作为外部因素应用,而这个外部因素必须放在某个地方。最合理的地方就是 MVVM 的 Model 部分。是的,你没听错,我们将它放在 Model 中。我们这样做是因为我们即将打破围绕 MVVM 产生的一个迷思。

迷思破解 #1。在 MVVM 中,Model DOES NOT 意味着它只用于数据模型。

尽管我们可能认为,MVVM 是一种架构模式,而不是一种表示模式。换句话说,它旨在跨越应用程序的整个范围,从 UI 一直到我们可能开发的许多不同层,无论是云服务、数据访问层、业务层,甚至是其他模式。这通常是 MVVM 新手最难理解的事情之一,在 MVVM 中,所有其他逻辑都去哪里了?很多时候,我们看到人们试图将层塞进 ViewModel,因为有一种迷思认为只有数据实体才能存在于 Model 层。这是不正确的,我们应该将其从脑海中抹去。Model 部分是除 View 和 ViewModel 之外的所有内容。我知道这此刻听起来有点愚蠢,因为我们还没有介绍 View 或 ViewModel 是什么,但请耐心等待,我们稍后会看这些层,事情应该会开始变得更有意义。

View

在视图的第一个迭代中,我们将保持简单,只让用户输入一次。如果他们确实输入了什么,我们应该看到一条消息,说明他们将名字更改为他们输入的内容。那么,让我们看看视图是什么样的。

public class ProgramView : Framework
{
  private readonly PersonViewModel viewModel = new PersonViewModel();
  public ProgramView()
  {
    DataContext = viewModel;
    SetBinding("Name");
  }

  public void Input()
  {
    string input = Console.ReadLine();
    viewModel.Name = input;
  }
}

这需要一些解释,所以让我们从基础开始。我们的视图继承自一个我们称之为 `Framework` 的类。`Framework` 类隐藏了我们将需要的许多底层细节。在继续解剖视图之前,我们应该停下来研究一下这些底层细节究竟是什么。

public abstract class Framework
{
  private readonly Dictionary<string, Binding> bindings = new Dictionary<string, Binding>();
  public object DataContext { get; set; }

  protected void SetBinding(string property)
  {
    Binding binding = new Binding(property)
    {
      Source = DataContext
    };
    binding.Parse();
    bindings.Add(property, binding);
  }
}

实际上,这有两个部分。第一部分是我们必须将某个东西设置为我们将绑定视图的上下文,以便接收输入和更新,并最终接收命令。这就是 `DataContext`,由于我们可以绑定到任何东西,所以它将是一个对象。

值得注意的是,这是一个粗略的实现,因为我们依赖于在执行任何绑定之前设置 `DataContext`。

我们需要的下一部分是我们能够绑定到属性的某种机制,以便我们能够对它们进行操作。MVVM 实现的底层粘合剂很大程度上依赖于自动绑定的强大功能。事实上,这就是 WPF 和 MVVM 如此契合的原因;WPF 中的绑定机制帮助使 MVVM 早期获得普及。所以,我们需要自己复制一个绑定实现。暂时打破 YAGNI(你不需要它),我将把绑定存储在一个字典中,以属性名作为键。我知道目前我们只绑定一个属性,但我们将使用它来构建一个更复杂的实现。如果我们暂时回到我们的视图,我们会看到我们将 `DataContext` 设置为 `PersonViewModel` 的一个实例,并且我们设置了一个到 `ViewModel` 中 `Name` 属性的绑定。考虑到我们已经讨论过绑定机制几次了,这实际看起来是什么样的呢?

public class Binding
{
  public Binding(string property)
  {
    Name = property;
  }

  public object Source { get; set; }

  public string Name { get; set; }

  public void Parse()
  {
    INotifyPropertyChanged inpc = Source as INotifyPropertyChanged;
    if (inpc == null)
    {
      return;
    }
    inpc.PropertyChanged += Inpc_PropertyChanged;
  }

  private void Inpc_PropertyChanged(object sender, PropertyChangedEventArgs e)
  {
    PropertyInfo propertyInfo = Source.GetType().GetProperty(e.PropertyName);
    object value = propertyInfo?.GetValue(Source);
    Console.WriteLine($"{e.PropertyName} changed to {value}");
  }
}

这个类真的不是很复杂。`Source` 是从 `Framework` 类中的 `DataContext` 填充的,而 `Name` 是我们想要绑定的 `ViewModel` 中的属性的名称。有趣的部分是 `Parse` 方法。我们在这里所做的是检查 `Source` 是否实现了 `INotifyPropetyChanged`,如果实现了,就挂接到 `PropertyChanged` 事件处理程序。这段代码暗示了我们的 `ViewModel` 可能不支持 `INotifyPropertyChanged`(我们通常将其缩写为 INPC)。

迷思破解 #2。ViewModel 不必支持 INPC。如果 ViewModel 中没有任何东西在改变,为什么还要从中发出更改通知?

我们可以看到,这同样不是生产级别的代码 - 为了示例的目的,我们不会担心事件的生命周期等。随着本系列的进展,我们将把它变成一个更强大的系统。
眼尖的读者会注意到,我们已经将 ViewModel 限制为单层属性。换句话说,目前我们无法绑定到“SomeType.SomeOtherType.Property”。这是此代码实现的一个故意限制,我们将在下一篇文章中解决这个问题,届时我们将开始扩展我们的绑定机制。

如果我们回到我们的视图,我们会看到我们有一个简单的 Input 方法,用于接收用户的输入并更新我们的 ViewModel。当 `Name` 属性更新时,我们将引发一个 INPC 事件,该事件将触发 `Inpc_PropertyChanged` 事件处理程序中的代码,在那里我们提取属性的属性信息,并使用它从属性中检索值。`Input` 方法等同于在 `TextBox` 中输入值。

ViewModel

现在我们来看它。我们来到代码的 ViewModel 部分。

public class PersonViewModel : INotifyPropertyChanged
{
  private readonly PersonModel personModel;
  private readonly PersonModelValidator validator;

  public PersonViewModel()
  {
    personModel = new PersonModel();
    validator = new PersonModelValidator();
  }

  public string Name
  {
    get { return personModel.Name; }
    set
    {
      if (!validator.IsValid(value))
      {
        return;
      }
      personModel.Name = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

在这里,我们开始看到我们之前讨论的内容,即 Model 部分是除 View 或 ViewModel 之外的所有内容。ViewModel 是负责让 View 和 Model 一起工作的那个部分。View 不应该了解如何与应用程序的 Model 部分进行交互,所以 ViewModel 是中间的“粘合剂”。因此,我们的构造函数创建了两个模型;我们之前看过的 `PersonModel` 和 `PersonModelValidator` 类。当用户更新 Name 时,我们使用验证器检查是否可以更新人的姓名,然后我们引发 `PropertyChanged` 事件,这将触发 `Binding` 中的 INPC 事件处理程序。

那么,当我们的应用程序运行时是什么样的?诚然,它看起来不是最吸引人的界面,但考虑到我们正在使用控制台应用程序,以 MVVM 的方式完成这一切非常有趣。

Application running.

就这样;这是创建 MVVM 控制台应用程序的第一步。在下一篇文章中,我们将扩展我们的代码库,以支持更复杂的 ViewModel 和 Model 交互,并通过 `ICommand` 接口引入命令支持。

历史

  • 2016 年 12 月 29 日:初始版本
© . All rights reserved.