以下是我讨论的示例:当用户想要编辑他们的个人资料时,他们会转到UserEdit视图,该视图使用下面的UserEdit模型.
public class UserEditModel { public string Username { get { return Info.Username; } set { Info.Username = value; } } [required] [MembershipPassword] [DataType(DataType.Password)] public string Password { get; set; } [DataType(DataType.Password)] [DisplayName("Confirm Password")] [Compare("Password",ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } [required] [Email] public string Email { get; set; } public UserInfo Info { get; set; } public Dictionary<string,bool> Roles { get; set; } } public class UserInfo : IRepoData { [ScaffoldColumn(false)] public Guid _id { get; set; } [ScaffoldColumn(false)] public DateTime Timestamp { get; set; } [required] [DisplayName("Username")] [ScaffoldColumn(false)] public string Username { get; set; } [required] [DisplayName("First Name")] public string FirstName { get; set; } [required] [DisplayName("Last Name")] public string LastName { get; set; } [ScaffoldColumn(false)] public string Theme { get; set; } [ScaffoldColumn(false)] public bool IsADUser { get; set; } }
请注意,UserEditModel类包含一个继承自IRepoData的UserInfo实例? UserInfo是保存到数据库的内容.我有一个通用的存储库类,它接受任何继承IRepoData形式并保存它的对象;所以我只是调用Repository.Save(myUserInfo)并完成它. IRepoData定义了_id(MongoDB命名约定)和时间戳,因此存储库可以基于_id进行upsert,并根据Timestamp检查冲突,以及对象刚刚保存到MongoDB的其他任何属性.在大多数情况下,视图只需要使用@ Html.EditorFor,我们很高兴!基本上,只有视图需要的任何内容都会进入基本模型,只有存储库需要的任何内容才能获得[ScaffoldColumn(false)]注释,而其他所有内容在两者之间都很常见. (顺便说一句 – 用户名,密码,角色和电子邮件都保存到.NET提供程序,这就是为什么它们不在UserInfo对象中.)
这种情况的巨大优势是双重的……
>我可以使用更少的代码,因此更易于理解,开发更快,更易于维护(在我看来).
>我可以在几秒钟内重新计算…如果我需要添加第二个电子邮件地址,我只需将其添加到UserInfo对象 – 它只是通过向对象添加一个属性而添加到视图并保存到存储库.因为我使用的是MongoDB,所以我不需要改变我的数据库模式或乱用任何现有数据.
鉴于此设置,是否需要制作用于存储数据的单独模型?你们都认为这种方法的缺点是什么?我意识到明显的答案是标准和关注点分离,但是你能想到的任何真实世界的例子会证明这会引起一些令人头痛的问题吗?
值得注意的是,我正在组建一个由两个开发人员组成的团队,因此很容易看到好处并忽略了一些标准.您是否认为在较小的团队中工作会对这方面产生影响?
解决方法
其中一个重要问题是业务逻辑/数据完整性问题,使用与视图中使用的相同的数据建模/持久性类.在用户类中具有DateTime DateAdded属性的情况下,表示添加用户的时间.如果您提供一个直接挂钩到UserInfo类的表单,最终会得到一个动作处理程序,如下所示:
[HttpPost] public ActionResult Edit(UserInfo model) { }
很可能您不希望用户在添加到系统时能够进行更改,因此您首先想到的是不在表单中提供字段.
但是,出于两个原因,你不能依赖它.首先,DateAdded的值将与您在执行新的DateTime()时获得的值相同,或者它将为null(对于此用户,这两种方式都不正确).
第二个问题是用户可以在表单请求中欺骗这个并添加& DateAdded =<无论日期>对于POST数据,现在您的应用程序将DB中的DateAdded字段更改为用户输入的内容.
这是设计,因为MVC的模型绑定机制查看通过POST发送的数据,并尝试自动将它们与模型中的任何可用属性连接.它无法知道发送的属性不是原始形式,因此它仍然会将它绑定到该属性.
viewmodels没有这个问题,因为你的视图模型应该知道如何将自身转换为数据实体或从数据实体转换,并且它没有要添加的DateAdded字段,它只有显示(或接收)它所需的最小字段.数据.
在您的确切场景中,我可以轻松地使用POST字符串操作重现这一点,因为您的视图模型可以直接访问您的数据实体.
在视图中直接使用数据类的另一个问题是,当您尝试以不太适合数据建模方式的方式呈现视图时.例如,假设您为用户提供以下字段:
public DateTime? BannedDate { get; set; } public DateTime? ActivationDate { get; set; } // Date the account was activated via email link
现在让我们说作为管理员,您对所有用户的状态感兴趣,并且您希望在每个用户旁边显示状态消息,并根据该用户的状态给出管理员可以执行的不同操作.如果您使用数据模型,您的视图代码将如下所示:
// In status column of the web page's data grid @if (user.BannedDate != null) { <span class="banned">Banned</span> } else if (user.ActivationDate != null) { <span class="Activated">Activated</span> } //.... Do some html to finish other columns in the table // In the Actions column of the web page's data grid @if (user.BannedDate != null) { // .. Add buttons for banned users } else if (user.ActivationDate != null) { // .. Add buttons for activated users }
这很糟糕,因为您的视图中有很多业务逻辑(禁用的用户状态始终优先于激活的用户,禁止的用户由具有禁止日期的用户定义等等).它也复杂得多.
相反,更好的(至少是imho)解决方案是将用户包装在viewmodel中,该viewmodel具有状态的枚举,当您将模型转换为视图模型时(视图模型的构造函数是执行此操作的好地方)可以插入您的业务逻辑一次以查看所有日期并确定用户应该处于什么状态.
然后,上面的代码简化为:
// In status column of the web page's data grid @if (user.Status == UserStatuses.Banned) { <span class="banned">Banned</span> } else if (user.Status == UserStatuses.Activated) { <span class="Activated">Activated</span> } //.... Do some html to finish other columns in the table // In the Actions column of the web page's data grid @if (user.Status == UserStatuses.Banned) { // .. Add buttons for banned users } else if (user.Status == UserStatuses.Activated) { // .. Add buttons for activated users }
在这个简单的场景中,这可能看起来不像代码更少,但是当确定用户状态的逻辑变得更复杂时,它会使事情更加可维护.您现在可以更改用户状态的确定逻辑,而无需更改数据模型(您不必因为查看数据的方式而更改数据模型),并且可以将状态确定保存在一个位置.