如何在MVC中完全解耦视图和模型

时间:2015-10-01 14:10:55

标签: asp.net-mvc

在我的第一个例子中,我有一个类似的模型:

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.WillAttend等的代码......那么,如果将来模型发生变化,会发生什么?我应该更改此特定视图中的所有片段。

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

我应该采用什么方法让两个人分别开发视图和模型?因此,当创建视图时,模型可能不存在,而模型将在以后创建。怎么可能?通过界面?

4 个答案:

答案 0 :(得分:5)

将视图与视图模型分开

一秒钟深入思考这个问题,不可能将ViewView Model分开。{1}}。您无法以某种方式预期会在页面上显示哪些信息以及在哪里开始创建网页 - 因为这正是编写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中对象的一般注释

在评论的背面,我将尝试向您展示我如何倾向于在MVC中布置我的代码以及我使用的不同对象以便将事情分开......这将是真正的使您的代码更易于被多人维护。当然,这是一个时间上的投资,但是随着应用程序的发展,我认为它非常值得。

您应该为不同的目的设置不同的类,一些跨层,一些位于特定层,不能从这些层外部访问。

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

  • Domain Models - 代表数据库中行的模型,我倾向于仅在我的服务层操作这些行,因为我使用实体框架,所以我没有“数据访问层” #39;就这样。
  • 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实例,它使用域模型来表示数据。我将注册的响应映射到我专门为DTO方法的响应创建的RegisterUser()

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" />
}

代码重复

你对评论中所说的内容非常正确,有一些(或很多)目标代码重复,但如果你想一想,如果你想真正分开,你需要这样做他们出去了:

  

查看模型!=域模型

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

在原始示例中,您拥有模型GuestResponse,其中验证属性用于装饰字段。如果您将GuestResponse对象加倍为Domain ModelView Model,则表明您的域模型污染了可能仅与您的UI Layer或甚至单个版本相关的属性页面!

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

另外,如果您考虑一下,只需编写一个目的类(DTO)来从service layer上的方法返回特定信息,您就可以浏览返回的类并理解完全它将会返回什么。然而,如果你只是碰到一个符合条件的物品。就像你的一个域模型代表你的一个数据库表中的一行一样,你不知道哪些信息与那个特定的方法相关,而你可能会带回你不喜欢的信息。 #39; t need。

如果您使用Domain Model 作为View Model ,如果您不小心,可以让自己对overposting attacks开放。如果使用您的应用程序的某人猜测了您班级中其他字段的名称,即使您没有在view中为其提供表单元素,任何人都可以发布该值并将其保存到数据库中。拥有View Model仅具有针对您的特定视图定制的字段意味着您可以限制将在服务器端处理的内容,而无需任何特殊的jiggery pokery。哦,你可以在不查看视图本身的情况下看到从视图中返回的确切内容。任何视图模型共享确实会让您感到困惑,因为当您试图弄清楚什么是不应该从视图中显示或回发时。

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

我希望这有助于澄清一些事情。当然,这一切都有待讨论,我欢迎它!

答案 1 :(得分:4)

使用ViewModel

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

What is a view model

ViewModel的观点是支持关注点的分离,这是MVC的主要卖点。这种分离使您的View和Models可以独立安全地进化。您可以保护模型免受视图中的任何更改,并且可以保护视图免受模型中的任何更改。

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

答案 2 :(得分:0)

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

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

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

答案 3 :(得分:0)

我个人认为你应该没有视图模型。如果您要发布的内容正是您要在db中存储的内容,为什么要有一个viewmodel?这只是维护的另一段代码。对我来说这是重复。 当然,如果你需要发布其他内容然后你建模你总是需要一个viewmodel。我不是说你永远不应该有一个。只是当你不需要它时。 我听到的所有参数总是有一个视图模型,以避免将来可能出现的想象问题。今年秋天属于yagni。 如果遇到问题,您可以随时创建一个viewmodel并在控制器中替换它的模型。这绝不是不可能的。实际上非常简单。 我认为你应该尝试不使用viewmodels的典型情况是你有很多crud类型的页面,模型,视图和控制器映射到彼此。每个视图的视图模型只会使应用程序难以维护,因为您需要在两个位置更改内容,并处理映射的复杂性。 只是不要在模型中放入与视图相关的代码。如果需要,您需要一个视图模型。