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

揭秘和简化 MVC 框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2017年7月25日

MIT

10分钟阅读

viewsIcon

30892

MVC 作为一种设计模式,已经被“四人帮”明确定义,但近年来,它更多地被用作一个营销术语,而非设计模式。本文旨在简化和定义 MVC 设计模式是什么,并提供一个你可以重用于自己项目的简单 MVC 框架。

引言

模型-视图-控制器 (MVC) 设计模式已经存在很长时间了。多年来,它一直是创建能够在技术、实现甚至理念之间扩展的优秀应用程序的黄金标准。在20世纪90年代,微软将其 MFC (Microsoft Foundation Classes) 应用程序框架建立在 MVC 设计模式之上;将 GUI 从逻辑和数据访问中抽象出来是一个新颖的概念,事实证明,这对开发人员和微软来说都是一个巨大的竞争优势。

背景

随着 VB6 和 .Net 的推出,企业编码理念变得更像一个“狂野西部”(这些领域的开发人员比 C++ MFC 的开发人员便宜得多)。经典 ASP 与 VB6 COM 对象成为一种潮流,并被 ASP.Net WebForms 所取代。ASP.Net WebForms 遵循与 VB6 桌面应用程序相同的事件模型,这使得它对那些在商业世界中构建客户端-服务器应用程序的开发人员非常容易上手。世界似乎正在从 MVC 转向一个以网络为中心、前端为主导的世界,这让人回想起大型机时代。

接着,微软让世界大吃一惊,发布了 ASP.Net MVC。哇。这简直是回到了原点!实际上,这对于微软来说是一个相当可预测的举动。.Net 开发环境正在迅速成熟,开发人员渴望抛弃 ASP.Net WebForms 及其开销。许多人已经开始创建自己的 MVC 框架(例如我的白天“另一个自我”),因为他们研究了真正的面向对象编程(OOP)原则。有一点是肯定的,MVC 作为一种设计模式正在重新回到前沿。

自 ASP.Net MVC 推出以来,Javascript 开发者创建了自己的 MVC 框架,例如 AngularJS。虽然这在理论上是一项崇高的努力,但用 Javascript 编写的单页应用程序 (SPA) 的性能差异很大,因为它们受浏览器、计算机、平板电脑和手机的不可预测性影响。大型企业的内部网受到的影响不如公共网站大,因为它们通常对运行 Javascript 的客户端有更多的控制权。

关于 SimpleMVC

我有多年的各种类型(ASP.Net、WinForms、WPF、UWP、Android、Xamarin)MVC 应用程序开发经验,我开发了许多可重用的概念(参见我链接到我的“另一个自我”的文章),这些概念帮助我快速轻松地在几分钟内启动并运行一个基于 MVC 的应用程序。我认真思考了这个问题,并决定是时候创建一个 MVC 库了,任何希望构建易于实现和测试的应用程序的人都可以重用它。

我刚才提到测试了吗?是的,我确实提到了。你看,MVC 最伟大的特性之一是各层对其他层的内部工作一无所知,除了它们公开的公共契约。这通过适当使用接口来抽象各个层并隔离相关部分来实现。MVC 是真正的 OOP,它呈现了封装(模型和控制器)、多态(视图和模型)和继承(控制器)。当你学习 MVC 时,你也在学习 OOP。

SimpleMVC 实际上是经过十多年的酝酿而成的。我创建过更简单的 MVC 框架,也创建过更复杂的 MVC 框架。SimpleMVC 是两者之间的平衡,与爱因斯坦的主张非常一致:事物应该简单,但不能比它们本来的样子更简单。

说了这么多,我们来谈谈 SimpleMVC 是什么,不是什么。

SimpleMVC 是

  • 可扩展
  • 简单
  • 可复用的
  • 健壮的
  • 松散耦合的
  • 紧密内聚的
  • 基于标准的

SimpleMVC 不是

  • 脆弱的
  • 过程化的
  • 一个营销工具

SimpleMVC 作为一个框架,公开了一些非常有用的公共接口和基类。

利用 MVC 架构构建一个出色的应用程序,您所需要的就是这些。

使用代码

请遵循以下步骤

  1. 通过扩展 ModelBase 来创建您的模型。模型基类允许模型公开对模型进行 CRUD 操作的基本功能。如果您正在使用 Entity Framework 或您希望的任何 DAL 提供程序,您只需在从 ModelBase 扩展的方法中实现访问代码。控制器将对模型内部的详细信息一无所知;控制器所要做的就是请求数据操作并处理结果。为了进一步增强应用程序的可重用性,您应该使用适配器模式为您的模型提供一个框架,以便利用该框架访问数据。
  2. 使用表达应用程序用户界面的接口来扩展 ISimpleView 接口。每一组独立的功能都值得拥有自己的接口。如果您正在创建一个 WPF 应用程序,您将为每个页面以及可重用控件创建一个接口。对于 ASP.net Webforms 应用程序,页面将获得一个接口,任何用户控件也将获得自己的接口。
  3. 扩展 SimpleControllerBase。在这里您需要做出一些重要的决定。在微软的 ASP.Net 框架中,您有一个控制器-每个-模型的范式。我不认为这是应用程序的准确划分。我建议将*相关*功能的业务逻辑分组在一起。例如,如果您有一个学生注册课程并参加考试。在微软的范式中,您将有三个模型和三个控制器。在 SimpleMVC 的世界中,建议您有三个模型但只有一个控制器。控制器是模型和视图之间的粘合剂。没有业务逻辑,模型和视图就没有意义,而且业务逻辑很少能 neatly 封装到单个模型的数据中。
  4. 创建模拟 UI 对象,它们实现步骤 1 中创建的抽象视图。
  5. 创建模拟 DAL 对象,它们实现操作模型持久化所需的 CRUD 功能。
  6. 创建单元测试,通过模拟视图的事件和回调来执行控制器中的业务规则。
  7. 让所有测试都通过。(这意味着让你的测试成功)。

假设我们接到任务,要创建一个小的 WinForms 应用程序来管理学生的入学注册。这个 WinForms 应用程序将由一两位管理员运行。该应用程序将与学校网络上的 SQL Server 数据库进行通信。该应用程序只需要创建、更新、打印和删除学生。

接下来,假设我们接到任务,要创建一个小的 Asp.Net WebForms 应用程序,允许学生随时随地注册课程。学生数据将从学校网络上的 SQL Server 数据库中检索。

最后,设想我们被要求创建一个网络服务,供教授们提交成绩。他们可能使用 Excel 或其他应用程序来记录学生的测试和作业,但最终,无论成绩来自何处,都必须汇总到一个地方。

这里我们有三个截然不同的应用程序,但它们都围绕同一个问题域工作:学生。

我们知道所有数据都将存储在学校的 SQL Server 中。我们知道与数据交互的逻辑在这三个应用程序之间是一致的。我们知道从视图中与控制器交互时要使用哪些数据。

第一步:模型

我们知道我们需要存储一些关于学生的信息,例如他们的出生日期、姓氏、名字和照片。学生将通过一个 StudentID 号码来识别。此外,学生将参加课程,我们也必须定义这些课程。一门课程将有名称、时间表和教授。最后,课程将有成绩,成绩将包括日期、百分比分数和名称。所有这些都将存储在 SQL Server 中,因此我们将创建一个数据库图来构建模型。

现在,尽管这是很好的第三范式关系数据,但由于几个原因,它与应用程序融合得不好。

  1. 交叉表操作起来很不方便。用 List 来表示多对多关系的另一端的聚合要简单得多。
  2. 尽管 Grade 表在技术上是一个交叉表,但其范围首先是针对 Class,因为输入将来自 Professor,而不是 Student。因此,Grade 的唯一链接将通过 Class 对象作为 Dictionary<Student, List<Grade>>

这样我们就需要创建三个模型,如下所示

public class StudentModel : ModelBase
{
    public string StudentID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public byte[] Picture { get; set; }
    
    public List<Class> Classes { get; set; }

    // Boilerplate CRUD methods
}

public class ClassModel : ModelBase
{
    public Guid ClassID { get; set; }
    public string ClassName { get; set; }
    public string Professor { get; set; }
    public DateTime Time { get; set; }
    public bool IsMonday { get; set; }
    public bool IsTuesday { get; set; }
    public bool IsWednesday { get; set; }
    public bool IsThursday { get; set; }
    public bool IsFriday { get; set; }

    public List<StudentModel> Students { get; set; }
    public List<StudentModel, List<GradeModel>> Grades { get; set; }

  // Boilerplate CRUD methods
}

public class GradeModel : ModelBase
{
    public Guid GradeID { get; set; }
    public string GradeName { get; set; }
    public decimal GradeValue { get; set; }

    public StudentModel Student { get; set; }
    public ClassModel Class { get; set; }

  // Boilerplate CRUD methods
}

 

第二步,视图

我们现在准备定义我们的视图。首先,是 WinForms 应用程序——我们从您喜欢的工具中模拟表单开始。

这个窗口包含了一些有趣的元素。首先,有一个搜索框用于查找学生,还有一个网格用于显示搜索结果。这是一个主视图,将作为它自己的 ISimpleView 实现,名为 IStudentMasterView

IStudentMasterView 需要一个属性来保存搜索条件,一个学生模型集合,一个事件来通知控制器执行搜索,以及一个回调函数来让控制器通知视图搜索结果。

public interface IStudentMasterView : ISimpleView
{
    string Criteria { get; }
    List<StudentModel> StudentsResult { get; set; }

    event EventHandler PerformSearch;

    void Databind();
}

窗口的下半部分是详情区。当用户从网格中选择用户时,表单将填充其出生日期、名字、姓氏和照片。另外,用户可以点击“新建”按钮来初始化新学生的子表单。在子表单中,我们将需要学生 ID、名字、姓氏、出生日期和照片的属性。接下来,我们需要事件来初始化新学生和保存学生。最后,我们需要一个回调函数来通知视图保存已完成,以防保存是异步的(也应该如此)。

public interface IStudentDetailView : ISimpleView
{
    string StudentID { get; set; }
    string FirstName { get; set; }
    string LastName { get; set; }
    DateTime DOB { get; set; }

    event EventHandler LoadStudent;
    event EventHandler SaveStudent;

    void StudentLoaded();
    void StudentSaved();
}

最后,您将在 Visual Studio 中创建您的 WinForms 窗口,并为主控和详细信息创建两个用户控件。主控用户控件将实现 IStudentMasterView,详细信息用户控件将实现 IStudentDetailView

将相应的属性连接到窗口控件的访问器属性,将网格绑定到学生模型列表,并将按钮连接以触发事件。很快,您将拥有一个完整的用户界面,以满足学生注册应用程序的需求!

第三步,控制器

到目前为止,我们已经设计了分别与数据库和用户交互的模型和视图。现在是时候应用一些逻辑来完成两者之间的交互了。关于控制器和业务逻辑,有两种思想流派。一方面,您会听到所有业务规则都应该封装在独立的类和程序集中,以便逻辑可以在应用程序和架构之间共享。另一方面,MVC 模式被认为是如此通用,以至于控制器*就是*业务规则,并且只是提供了这些规则的公共接口;新的应用程序无论如何都应该使用 MVC 模式,以利用控制器和模型,即使视图完全不同。就我个人而言,我喜欢第二种立场,因为它在逻辑、模型和 UI 之间保持了强大的内聚性,而无需强制任何紧密耦合。此外,它使得应用程序更加健壮,因为业务逻辑的更改不必在多个地方实现。如果业务逻辑代码与控制器分离,并且您在另一个项目中有一个控制器使用相同的库,那么对该业务逻辑代码的更改可能需要对两个控制器进行代码更改,从而强制对两个应用程序进行质量保证周期。

在我们的示例应用程序中,我们已经定义了学生的视图。将来,将有用于注册课程和发布成绩的新视图,两者都使用不同的实现技术。现在,我们将从控制器的事件处理程序开始,用于两个视图接口。

public class StudentController : SimpleControllerBase
{
    public override bool Initialize()
    {
        var result = true;

        foreach(var view in Views.Values)
        {
            if(view is IStudentHeaderView)
            {
                var shv = view as IStudentHeaderView;

                shv.PerformSearch += ShvOnPerformSearch;

                result &= true;
            }
            else if(view is StudentDetailView)
            {
                var sdv = view as IStudentDetailView;

                sdv.PerformSearch += SdvOnLoadStudent;
                sdv.SaveStudent += SdvOnSaveStudent;

                result &= true;
            }
            else
            {
                result = false;
            }
        }

        return result;
    }

    public async void ShvOnPerformSearch(object sender, EventArgs e)
    {
        var view = sender as IStudentHeaderView;

        if(view != null)
        {
            var models = await StudentModel.SearchAsync(
                new SearchCriteria<StudentModel> { 
                    SearchCriteriaTypes = SearchCriteriaTypes.Contains,
                    CriteriaValues = new List<object> { view.Criteria }
                });

            view.StudentsResult = models;

            view.Databind();
        }
    }

    public async void SdvOnLoadStudent(object sender, EventArgs e)
    {
        var view = sender as IStudentDetailView;

        if(view != null)
        {
            // Marshal the data to the View.
            // We don't have an overload of LoadAsync that accepts a string
            var model = await StudentModel.LoadSearchAsync(
                new SearchCriteria<StudentModel {
                    SearchCriteriaTypes = SearchCriteriaTypes.IsEquals,
                    CriteriaValues = new List<object> { view.StudentID }
            }).FirstOrDefault();

            if(model != null)
            {
                view.StudentID = model.StudentID;
                view.FirstName = model.FirstName;
                view.LastName = model.LastName;
                view.DateOfBirth = model.DateOfBirth;
                view.Picture = model.Picture;

                view.Classes = model.Classes;

                view.StudentLoaded();
            }
        }
    }

    public async void SdvOnSaveStudent(object sender, EventArgs e)
    {
        var view = sender as IStudentDetailView;

        if(view != null)
        {
            // Business rules here!
            if(view.DateOfBirth >= DateTime.Today) 
                throw new ApplicationException("Invalid Date of Birth.");


            // Marshal the data from the View.
            // We don't have an overload of LoadAsync that accepts a string
            var model = await StudentModel.LoadSearchAsync(
            new SearchCriteria<StudentModel {
                SearchCriteriaTypes = SearchCriteriaTypes.IsEquals,
                CriteriaValues = new List<object> { view.StudentID }
            }).FirstOrDefault();

           if(model == null)
           {
               model = new StudentModel();
           }

           model.StudentID = view.StudentID;
           model.FirstName = view.FirstName;
           model.LastName = view.LastName;
           model.DateOfBirth = view.DateOfBirth;
           model.Picture = view.Picture;

           model.Classes = view.Classes;

           if(await Model.SaveAsync() == 1)
           {
              view.StudentSaved();
           }
          
       }
    }
}

信不信由你,这就是我们所需要的一切。

几点说明

  • 事件处理程序被标记为异步,以便它们可以在响应式 UI 环境中使用。WPF、UWP 甚至 WinForm 应用程序都从中受益匪浅。
  • 显示逻辑位于视图的实现中(显示逻辑与业务规则是另一个话题),这保持了松散耦合。
  • 模型公开了一个迷你搜索引擎,其详细信息在适配器类中实现(这是另一个话题)。

在 GitHub 上,SimpleMVC 仓库包含一个单元测试项目。克隆仓库,研究它并运行测试。项目站点位于http://simplemvc.gatewayprogramming.school

结论

人们普遍对编程充满热情,关于如何实现 MVC 模式的争论自“四人帮”引入它以来就一直激烈。本文只是又一个立足点。我强烈建议您在盲目遵循单一实现方法之前,多了解 MVC 模式。

© . All rights reserved.