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将显示名称,地址2等.
我应该遵循什么方法,这样两个人可以分别开发视图和模型?因此,当创建视图时,模型可能不存在,而模型将在以后创建.怎么可能?通过界面?
解决方法
一秒钟深入思考这个问题,不可能将View与View模型分离.你无法开始创建一个网页,而不会以某种方式预期会在页面上显示哪些信息以及在哪里 – 因为这正是编写HTML代码的原因.如果您没有至少决定这两件事中的一件,那么就没有任何HTML代码可供编写.因此,如果您有一个显示来自控制器的信息的页面,则需要定义视图.
传递给视图的视图模型应仅表示仅为单个视图(或部分视图)显示的数据字段.它不是“可解耦的”,因为你永远不需要多次实现它 – 它没有逻辑,因此没有其它的实现.这是您的应用程序的其他部分需要解耦才能使它们可重用和可维护.
即使您使用动态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项目通常有以下类型的模型:
>域模型 – 表示数据库中行的模型,我倾向于仅在我的服务层中操作它们,因为我使用实体框架,所以我没有这样的“数据访问层”.
> DTO – 数据传输对象,用于在服务层和UI层之间传递特定数据
>视图模型 – 在视图和控制器中引用的模型,在将DTO传递给视图之前,将DTO映射到这些模型.
这是我如何使用它们(你问了代码,所以这里是一个我刚刚与你的相似的例子,但只是为了一个简单的注册):
领域模型
这是一个域模型,它简单地表示用户及其在数据库中的列.我的DbContext使用域模型,我操纵我的服务层中的域模型.
public User { public string UserName { get; set; } public string Password { get; set; } public string Email { get; set; } public string Phone { get; set; } }
数据传输对象(DTO)
以下是我在控制器的UI层中映射的一些数据传输对象,并传递给我的服务层,反之亦然.看看它们有多干净,它们应该只包含在层之间来回传递数据所需的字段,每个字段都应该有特定的用途,比如服务层中的特定方法接收或返回.
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层的视图模型.它特定于单个视图,永远不会在您的服务层内触及!您可以使用它来映射回发到控制器的值,但您不必这样做 – 您可以专门为此目的使用一个全新的模型.
public class Registrationviewmodel() { public string UserName { get; set; } public string Password { get; set; } public string Email { get; set; } public string Phone { get; set; } }
服务层
这是服务层的代码.我有一个DbContext的实例,它使用域模型来表示数据.我将注册的响应映射到我专门为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" /> }
重复代码
你在评论中说的很正确,有一些(或很多)目标代码重复,但是如果你考虑它,你需要这样做,如果你想真正将它们分开:
View Models != Domain Models
在许多情况下,您在视图上显示的信息不包含仅来自单个域模型的信息,并且某些信息永远不会覆盖到您的UI层,因为它永远不会显示给应用程序用户 – 例如哈希用户密码
在您的原始示例中,您具有模型GuestResponse,其中验证属性用于装饰字段.如果您将GuestResponse对象作为域模型和视图模型加倍,则您使用可能仅与UI层甚至单个页面相关的属性污染了您的域模型!
如果您没有为服务层方法定制DTO,那么当您向该方法返回的任何类添加新字段时,您将必须更新返回该特定类的所有其他方法以包括该方法一条信息也是.您有可能在某一点上添加新字段仅与您正在更新的单一方法相关或计算以从中返回吗?与DTO和服务方法建立1:1的关系使得更新它变得轻而易举,您不必担心使用相同DTO类的其他方法.
另外,如果您考虑一下,只需编写一个目的类(DTO)来从服务层上的方法返回特定信息,您就可以浏览返回的类并准确了解它的内容是什么回来.然而,如果您只是插入一个“符合账单”的对象,就像您的某个数据库表中的一行表示一切的域模型一样,您不知道哪些信息与该特定方法相关,而您’可能会带回你不需要的信息.
如果您使用域模型作为您的视图模型,如果您不小心,您可以将自己打开到overposting attacks.如果使用您的应用程序的人猜测您班级中其他字段的名称,即使您没有提供视图中的表单元素,任何人都可以发布该值并将其保存到数据库中.拥有仅具有针对特定视图定制的字段的视图模型意味着您可以限制在服务器端处理的内容,而无需任何特殊的jiggery pokery.哦,你可以在不查看视图本身的情况下确切地看到从视图中返回的内容.任何视图模型共享确实会让您感到困惑,因为当您尝试找出什么是不应该显示或从视图中回发时.
还有很多其他原因,我可以整天看看这个话题. :P.
我希望这有助于澄清一些事情.当然,这一切都有待讨论,我欢迎它!