将视图与视图模型分开
仔细考虑一下这个问题,不可能将您的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 Layer 和 UI 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 Model 和View Model 的双重对象,则您的域模型可能只与您的UI Layer 甚至单个页面相关!
如果您没有为您的 service layer 方法定制 DTO,那么当您将新字段添加到该方法返回的任何类时,您将必须更新所有其他返回该特定方法的方法类也包含该信息。您是否会遇到一个点,即添加一个新字段仅在您更新以从中返回它的单一方法中相关或计算? DTO 和服务方法的 1:1 关系使更新变得轻而易举,您不必担心使用相同 DTO 类的其他方法。
此外,如果您考虑一下,有一个专门编写的单一用途类 (DTO),用于从您的 service layer 上的方法返回特定信息,您可以查看返回的类并准确理解 它将返回什么。而如果您只是插入一个“符合要求”的对象,例如您的域模型,它代表您的一个数据库表的一行中的所有内容,您不知道哪些信息与该特定方法相关,而您可能会带回您不需要的信息。
如果您使用Domain Model 作为您的View Model,如果您不小心,您可以让自己对overposting attacks 开放。如果使用您的应用程序的人猜到了您的类中附加字段的名称,即使您没有在view 中为其提供表单元素,任何人都可以发布该值并将其保存到数据库中。拥有一个仅具有针对您的特定视图定制的字段的View Model 意味着您可以限制将在服务器端处理的内容,而无需任何特殊的诡计。哦,您可以在不检查视图本身的情况下准确地看到将从视图返回的内容。任何视图模型共享都会在您尝试确定哪些内容应该显示或不应该从视图中显示或发回时让事情变得混乱。
还有很多其他原因,看起来我可以整天讨论这个话题。 :P。
我希望这有助于澄清一些事情。当然,这一切都值得商榷,我欢迎!