【问题标题】:How to completely decouple view and model in MVC如何在 MVC 中完全解耦视图和模型
【发布时间】:2016-04-29 03:18:40
【问题描述】:

在我的第一个示例中,我有一个这样的模型:

public class GuestResponse
{
    [Required(ErrorMessage = "Please enter your name")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Please enter your email")]
    [RegularExpression(".+\\@.+\\..+", ErrorMessage = "Please enter a valid email address")]
    public string Email { get; set; }

    public string Phone { get; set; }

    [Required(ErrorMessage = "Please specify whether you'll attend")]
    public bool? WillAttend { get; set; }
}

控制器:

public class HomeController : Controller
{
    public ViewResult Index()
    {
        ViewData["greeting"] = (DateTime.Now.Hour < 12 ? "Good morning" : "Good afternoon");
        return View();
    }

    [HttpGet]
    public ViewResult RsvpForm()
    {
        return this.View();
    }

    [HttpPost]
    public ViewResult RsvpForm(GuestResponse guestResp)
    {
        if (ModelState.IsValid)
        {
            return this.View("Thanks", guestResp);
        }
        else
        {
            return this.View();
        }
    }
}

还有一个观点:

@model GuestResponse
<body>
<div>
    <h1>
        Thank you,
        <%: Model.Name  %>.</h1>
    <p>
        <% if (Model.WillAttend == true)
           {  %>
        It's great that you're coming. The drinks are already in
        the fridge!
        <% }
           else
           {  %>
        Sorry to hear you can't make it, but thanks for letting
        us know.
        <% } %>
    </p>
</div>

我觉得奇怪的是,View 与 Model 紧密耦合:它使用 Model.WillAttend 等代码......那么如果将来 Model 发生变化会发生什么?我应该必须更改此特定视图中的所有 sn-ps。

假设我的视图将显示一个注册页面,我将在其中显示姓名、标题、地址 1、地址 2 等的输入,所有这些字段都将绑定到模型,但那个时候模型可能不存在。那么我可以创建一个接口并且模型将实现该接口并且视图将只导入该接口而不是模型类吗?因此,当我们输入 Model. 时,IntelliSense 将显示名称、标题、地址 1、地址 2 等创建 UI?

我应该遵循什么方法才能让两个人分别开发视图和模型?因此,当创建视图时,模型可能不存在,而是稍后创建模型。怎么可能?通过接口?

【问题讨论】:

  • 但是视图与模型紧密耦合——通常是从域/业务对象映射的视图模型。使用这种方法,您将您的业务/域与 ui 分离并将其仅绑定到 ViewModel - 这正是 MVC 的方式。
  • 为什么要分别开发模型和视图?一个动作方法产生一个结果,模型保存这个结果,视图显示这个结果——关系通常是 1:1。是的,您可以为此使用接口。为什么这还不足以回答您的问题?
  • @Mou - 您的 GuestResponse 是否使用实体框架保存在数据库中?
  • 首先,如果您正在考虑纯 .NET MVC,则没有办法做到这一点。但是您可以通过使用敲除或 angularJs 来实现。

标签: asp.net-mvc


【解决方案1】:

将视图与视图模型分开

仔细考虑一下这个问题,不可能将您的View 与您的View Model 分离。如果不以某种方式预测页面上将显示哪些信息以及在何处显示,您就无法开始创建网页 - 因为这正是编写 HTML 代码的目的。如果您不决定这两件事中的至少一项,则无需编写任何 HTML 代码。因此,如果您有一个显示来自控制器的信息的页面,则需要定义一个视图。

您传递给您的视图的View Model 应该只表示要为单个视图(或部分视图)显示的数据字段。它不是“可解耦的”,因为您永远不需要它的多个实现——它没有逻辑,因此没有其他实现。您的应用程序的其他部分需要解耦以使其可重用和可维护。

即使您使用动态 ViewBag 并使用反射来确定其中包含的属性以动态显示整个页面,最终您还是必须决定该信息将在何处显示以及以什么顺序显示.如果您在视图和相关帮助程序之外的任何地方编写任何 HTML 代码,或者在视图中执行显示逻辑以外的任何内容,那么您可能违反了 MVC 的基本原则之一。

但一切都没有丢失,请继续阅读...

独立于视图模型开发视图

两个人分别独立开发您的视图和模型 (正如您在问题中明确提出的那样),拥有未定义模型的视图是完全可以的。只需从视图中完全删除 @model,或将其注释掉,以便稍后取消注释。

//@model RegistrationViewModel
<p>Welcome to the Registration Page</p>

如果没有定义@model,您不必将模型从控制器传递到视图:

public class HomeController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        // Return the view, without a view model
        return View();
    }
}

您还可以使用 MVC 的 HTML 帮助程序的非强类型版本。因此,如果定义了视图 @model,您可能会这样写:

@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)

改为使用名称末尾不带For 的版本,它们接受字符串作为名称,而不是直接引用您的模型:

@Html.Label("UserName")
@Html.TextBox("UserName")

当您为页面完成视图模型后,您可以稍后使用强类型版本的帮助程序更新这些内容。这将使您的代码稍后更加健壮。


关于 ASP.NET MVC 中对象的一般注释

在 cmets 的背面,我将尝试用代码向您展示我倾向于如何在 MVC 中布置我的代码,以及我使用不同的对象来分离事物......这将真正让你代码更易于多人维护。当然,这是对时间的一点投资,但在我看来,随着应用程序的发展,这是非常值得的。

您应该为不同的目的使用不同的类,一些跨层,一些驻留在特定层中,并且不能从这些层之外访问。

我的 MVC 项目通常有以下类型的模型:

  • Domain Models - 代表数据库中行的模型,我倾向于仅在我的服务层中操作这些,因为我使用实体框架,所以我没有这样的“数据访问层”。
  • DTOs - 数据传输对象,用于在 Service LayerUI Layer 之间传递特定数据
  • View Models - 仅在视图和控制器中引用的模型,在将它们传递到视图之前将 DTO 映射到这些模型。

这是我使用它们的方式(您要求提供代码,所以这是一个我刚刚鼓起来的示例,与您的类似,但只是为了简单的注册):

领域模型

这是一个仅表示User 的域模型,它是数据库中的列。我的DbContext 使用域模型,我在Service Layer 中操作域模型。

public User
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

数据传输对象 (DTO)

这里有一些数据传输对象,我映射到我的控制器中的UI Layer 并传递给我的Service Layer,反之亦然。看看它们有多干净,它们应该只包含在层之间来回传递数据所需的字段,每个字段都应该有特定的目的,比如通过服务层中的特定方法接收或返回.

public class RegisterUserDto()
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

public class RegisterUserResultDto()
{
    public int? NewUserId { get; set; }
}

查看模型

这是一个仅存在于我的UI layer 中的视图模型。它特定于单个视图,并且永远不会在您的服务层中触及!您可以使用它来映射回传到控制器的值,但您不必这样做 - 您可以有一个专门用于此目的的全新模型。

public class RegistrationViewModel()
{
    public string UserName { get; set; }
    public string Password { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

服务层

这是服务层的代码。我有一个 DbContext 实例,它使用 Domain Models 来表示数据。我将注册的响应映射到我专门为RegisterUser() 方法的响应创建的DTO

public interface IRegistrationService
{
    RegisterUserResultDto RegisterUser(RegisterUserDto registerUserDto);
}

public class RegistrationService : IRegistrationService
{
    public IDbContext DbContext;

    public RegistrationService(IDbContext dbContext)
    {
        // Assign instance of the DbContext
        this.DbContext = dbContext;
    }        

    // This method receives a DTO with all of the data required for the method, which is supposed to register the user
    public RegisterUserResultDto RegisterUser(RegisterUserDto registerUserDto)
    {
        // Map the DTO object ready for the data access layer (domain)
        var user = new User()
                   {
                       UserName = registerUserDto.UserName,
                       Password = registerUserDto.Password,
                       Email = registerUserDto.Email,
                       Phone = registerUserDto.Phone
                   };

        // Register the user, pass the domain object to your DbContext
        // You could pass this up to your Data Access LAYER if you wanted to, to further separate your concerns, but I tend to use a DbContext
        this.DbContext.EntitySet<User>.Add(user);
        this.DbContext.SaveChanges();

       // Now return the response DTO back
       var registerUserResultDto = RegisterUserResultDto()
       {
            // User ID generated when Entity Framework saved the `User` object to the database
            NewUserId = user.Id
       };

       return registerUserResultDto;
    }
}

控制器

在控制器中,我们映射一个 DTO 以向上发送到服务层,作为回报,我们收到一个 DTO。

public class HomeController : Controller
{
    private IRegistrationService RegistrationService;

    public HomeController(IRegistrationService registrationService)
    {
        // Assign instance of my service
        this.RegistrationService = registrationService;
    }

    [HttpGet]
    public ActionResult Index()
    {
        // Create blank view model to pass to the view
        return View(new RegistrationViewModel());
    }

    [HttpPost]
    public ActionResult Index(RegistrationViewModel requestModel)
    {
        // Map the view model to the DTO, ready to be passed to service layer
        var registerUserDto = new RegisterUserDto()
        {
            UserName = requestModel.UserName,
            Password = requestModel.Password,
            Email = requestModel.Email,
            Phone = requestModel.Phone
        }

        // Process the information posted to the view
        var registerUserResultDto = this.RegistrationService.RegisterUser(registerUserDto);

        // Check for registration result
        if (registerUserResultDto.Id.HasValue)
        {
            // Send to another page?
            return RedirectToAction("Welcome", "Dashboard");
        }

        // Return view model back, or map to another view model if required?
        return View(requestModel);
    }
}

查看

@model RegistrationViewModel
@{
    ViewBag.Layout = ~"Views/Home/Registration.cshtml"
}

<h1>Registration Page</h1>
<p>Please fill in the fields to register and click submit</p>

@using (Html.BeginForm())
{

    @Html.LabelFor(x => x.UserName)
    @Html.TextBoxFor(x => x.UserName)

    @Html.LabelFor(x => x.Password)
    @Html.PasswordFor(x => x.Password)

    @Html.LabelFor(x => x.Email)
    @Html.TextBoxFor(x => x.Email)

    @Html.LabelFor(x => x.Phone)
    @Html.TextBoxFor(x => x.Phone)

    <input type="submit" value="submit" />
}

代码重复

你在 cmets 中说的很对,有一点(或很多)目标代码重复,但仔细想想,如果你想真正分开,你需要这样做他们出去:

查看模型!= 领域模型

在许多情况下,您在视图上显示的信息不仅仅包含来自单个 domain model 的信息,并且某些信息永远不应归结为您的 UI Layer,因为它永远不应显示给应用程序用户- 比如用户密码的哈希值。

在您的原始示例中,您拥有模型GuestResponse,并带有装饰字段的验证属性。如果您将GuestResponse 对象作为Domain ModelView Model 的双重对象,则您的域模型可能只与您的UI Layer 甚至单个页面相关!

如果您没有为您的 service layer 方法定制 DTO,那么当您将新字段添加到该方法返回的任何类时,您将必须更新所有其他返回该特定方法的方法类也包含该信息。您是否会遇到一个点,即添加一个新字段仅在您更新以从中返回它的单一方法中相关或计算? DTO 和服务方法的 1:1 关系使更新变得轻而易举,您不必担心使用相同 DTO 类的其他方法。

此外,如果您考虑一下,有一个专门编写的单一用途类 (DTO),用于从您的 service layer 上的方法返回特定信息,您可以查看返回的类并准确理解 它将返回什么。而如果您只是插入一个“符合要求”的对象,例如您的域模型,它代表您的一个数据库表的一行中的所有内容,您不知道哪些信息与该特定方法相关,而您可能会带回您不需要的信息。

如果您使用Domain Model 作为您的View Model,如果您不小心,您可以让自己对overposting attacks 开放。如果使用您的应用程序的人猜到了您的类中附加字段的名称,即使您没有在view 中为其提供表单元素,任何人都可以发布该值并将其保存到数据库中。拥有一个仅具有针对您的特定视图定制的字段的View Model 意味着您可以限制将在服务器端处理的内容,而无需任何特殊的诡计。哦,您可以在不检查视图本身的情况下准确地看到将从视图返回的内容。任何视图模型共享都会在您尝试确定哪些内容应该显示或不应该从视图中显示或发回时让事情变得混乱。

还有很多其他原因,看起来我可以整天讨论这个话题。 :P。

我希望这有助于澄清一些事情。当然,这一切都值得商榷,我欢迎!

【讨论】:

  • 有什么不清楚....你如何解耦视图和模型?你是否通过视图模型解耦?
  • 我发现类代码重复,它们是 User、RegisterUserDto、RegistrationViewModel。所有类结构看起来都一样。这意味着如果我有 100 个 POCO 类,那么我需要为这些 POCO 类和 100 个视图模型创建 100 个 DTO ......它强制编写更多代码。请回答。
  • 看到这个你的代码// Now return the response DTO back var registerUserResultDto = RegisterUserResultDto() { NewUserId = userId };用户ID来自哪里??
  • 抱歉耽搁了。确实,它并不完美,我会用更多信息更新答案。我已经澄清了user.Id(我不小心错过了期间,哎呀!)
  • 你说得对,确实是重复,但如果你真的想把它们分开,你必须这样。我会在答案中解释更多
【解决方案2】:

使用 ViewModel

ViewModel 是专门为页面创建的类。以LoginViewModel 为例。

What is a view model

ViewModel 的重点是支持关注点分离,这是 MVC 的主要卖点。这种分离允许您的视图和模型安全地独立发展。您可以保护模型免受视图中的任何更改,并且您可以保护视图免受模型中的任何更改。

MVC 本身非常有限。为了满足日益增加的复杂性,必须引入其他层。比如 ViewModel、Service、Domain、Infrastructure 等。

【讨论】:

  • 从这个意义上说,我猜 OP 是在谈论他们的 ViewModel。
  • OP 有一个视图模型,它是GuestResponse
  • @Coulton 是吗?或者它是一个非常适合页面的模型?也许 GuestResponse 是一个实际的域实体并映射到数据库中。
  • 如果它是一个“域”模型,它不应该有验证属性。
  • @Yorro 能否请您与代码示例讨论如何使用viewmodel实现SOC,以便可以独立开发视图和模型。
【解决方案3】:

您注意到的是 MVC 模型的弱点之一。这就是为什么有些人正在远离它。相反,我们有很多其他模式可以更好地解耦它们。

在 MVVM 中你有一个视图和一个模型,但在两者之间你有一个模型视图,它应该包含所有的表示逻辑。这完全解耦了视图和模型。

您也可以使用接口执行此操作,但这是另一种形式的解耦。即使你解耦了类,你仍然有层之间的依赖关系(视图层和模型层会有一些链接)。

【讨论】:

  • 你是说MVC模式不再流行了?你能证明吗?还有一件事——即使使用 MVVM,视图仍然使用模型对象中的属性/方法等,因此仍然存在一定程度的耦合。
  • 你说得对,我不能。由于 Web 开发,它可能不正确。我的感觉是,在学术文献中,人们更多地谈论其他模式作为更好的方法,但这与它不那么受欢迎不同。我的错误
  • 即使在 web 开发中你仍然可以使用 mvvm - angularjs 可以用于此目的,但它仍然依赖于视图绑定到对象的模型,即{{MyModel.Something}}跨度>
  • 人们也在MVC中使用ViewModel
  • 在 MVVM 中,视图与视图模型进行通信……在我看来,这绝不是使视图与视图模型分离。如果我错了,请纠正我。
【解决方案4】:

我个人的意见是,您应该不使用视图模型。如果您发布的内容正是您要存储在数据库中的内容,那么为什么要有视图模型?这只是要维护的另一段代码。对我来说,这就是重复。 当然,如果您需要发布其他内容,那么您总是需要一个视图模型。我并不是说你永远不应该拥有一个。只是在你不需要它的时候。 我听到的所有关于总是有视图模型的论点是为了避免将来可能发生的想象中的问题。这属于yagni。 如果您遇到问题,您始终可以创建一个视图模型并在控制器中替换您的模型。这无论如何都不是不可能的。其实很简单。 我认为您应该尝试不使用视图模型的典型情况是,当您有很多带有模型、视图和控制器相互映射的 crud 类型页面时。每个视图的视图模型只会使应用程序难以维护,因为您需要在两个地方更改内容并处理映射的复杂性。 只是不要将视图相关的代码放在模型中。如果需要,则需要一个视图模型。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-08
    • 1970-01-01
    • 2011-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多