Asp.mvc(四)~ 构建Web层
继前三篇博文,已经认识了项目层次,MongoDB驱动, AutoMapper转换领域模型与DTO,Autofac 注入依赖, 下面我们会通过这些知识点来完善这个 asp.net mvc 项目。 上篇中讲到了 UserController,
在这里创建了一个 Index 的 Action :
public async Task<ActionResult> Index()
{
var result = await this._userService.FindManyAsync(x => true, 10, 0);
return View(result);
}
我们可以看到, 这里我们给 Index.cshtml 这个视图传递了一个 List<Core.Domain.User>
, 我们再来看看这个这个领域模型:
using System;
namespace Core.Domain
{
public partial class User : MongoEntity
{
public string LoginName { get; set; }
public string LoginPwd { get; set; }
public string NickName { get; set; }
public string PhoneNo { get; set; }
public Gender Gender { get; set; }
public DateTime Birthday { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public DateTime RegisterTime { get; set; }
}
public enum Gender
{
Male, Female
}
}
其实,在这里去传入这个领域模型给视图是存在不足之处的:
- 并不需要给用户展示 Core.Domain.User 的所有属性。
- 并不希望将 Core.Domain.User 暴露给视图。
- 若将 Core.Domain.User 传递给视图, 我们在使用 System.ComponentModel.DataAnnotations 来做数据验证的时候就需要去更改领域模型,为领域模型增加一些验证特性, 这样做就会将 Core.Domain.User 与视图紧紧的耦合在一起。
创建DTO
所以我们需要构建一个 ViewModel 用以贯穿在 Controller 与 View 之间来传递数据:
using System;
using System.ComponentModel.DataAnnotations;
using Core.Domain;
using System.Web.Mvc;
namespace Web.Models.User
{
public class UserModel
{
public string Id { get; set; }
[Display(Name = "账号名")]
[Required(ErrorMessage = "登录账号必填")]
[StringLength(20, MinimumLength = 8, ErrorMessage = "登录账号长度在8~20个合法字符内")]
[Remote("IsLoginNameExists", "User", ErrorMessage = "账户名已存在")]
public string LoginName { get; set; }
[Display(Name = "密码")]
[Required(ErrorMessage = "密码必填")]
[DataType(DataType.Password)]
[StringLength(32, MinimumLength = 6, ErrorMessage = "密码长度必须在6~32内")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "确认密码")]
[System.ComponentModel.DataAnnotations.Compare("Password", ErrorMessage = "两次密码不一致")]
public string Repassword { get; set; }
[Display(Name = "昵称")]
[Required(ErrorMessage = "昵称必填")]
[StringLength(20, ErrorMessage = "昵称长度保持在20字符以内")]
public string NickName { get; set; }
[Display(Name = "性别")]
[EnumDataType(typeof(Gender))]
public Gender Gender { get; set; }
[Display(Name = "联系电话")]
[DataType(DataType.PhoneNumber)]
public string PhoneNo { get; set; }
[Display(Name = "生日")]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
[DataType(DataType.Date, ErrorMessage = "日期格式不正确")]
public DateTime Birthday { get; set; }
[Display(Name = "电子邮箱")]
[DataType(DataType.EmailAddress, ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; }
[Display(Name = "现居地址")]
[StringLength(50)]
public string Address { get; set; }
}
}
现在来观察这个 UserModel 的结构, 可以发现, 我们去掉了一些非必要显示的属性, 留下了一些需要做显示或者标示一条数据的属性, 下面介绍一下这里用到的一些特性(特性省略了 Attribute后缀):
Attribute | Namespace | Direction |
---|---|---|
Display | System.ComponentModel.DataAnnotations | 指定属性显示名称 |
Required | System.ComponentModel.DataAnnotations | 指定属性为必须 |
StringLength | System.ComponentModel.DataAnnotations | 指定字符串属性长度 |
Remote | System.Web.Mvc | 提供使用Jquery远程验证 |
DataType | System.ComponentModel.DataAnnotations | 指定属性类别(邮箱,电话,密码等) |
EnumDataType | System.ComponentModel.DataAnnotations | 指定为枚举类型 |
DisplayFormat | System.ComponentModel.DataAnnotations | 动态数据显示格式(常用在数字,日期时间等) |
Compare | System.ComponentModel.DataAnnotations | 提供比较两个属性的值(常用在密码与确认密码) |
创建映射
通过这个系列的第二篇博文对 AutoMapper 的学习, 大家应该都对 AutoMapper 有了一定的了解, 下面我们来配置领域模型(Core.Domain.User)与 Dto (UserModel)之间的映射关系, 在 Web 根目录下建立一个 Infrastructure 目录, 意为项目的基础设施部分, 创建 AutoMapperStartupTask 类:
using AutoMapper;
using Core.Domain;
using MongoDB.Bson;
using Web.Models.User;
namespace Web.Infrastructure
{
public class AutoMapperStartupTask
{
public static void Execute()
{
//ObjectId --> string
Mapper.CreateMap<ObjectId, string>().ConvertUsing((ObjectId source) =>
{
if (source != null)
{
return source.ToString();
}
return string.Empty;
});
//string --> ObjectId
Mapper.CreateMap<string, ObjectId>().ConvertUsing((string source) =>
{
var objId = ObjectId.Empty;
if (ObjectId.TryParse(source, out objId))
{
return objId;
}
return ObjectId.Empty;
});
//User
Mapper.CreateMap<User, UserModel>()
.ForMember(dest => dest.Password, mo => mo.MapFrom(src => src.LoginPwd))
.ForMember(dest => dest.Repassword, mo => mo.Ignore());
Mapper.CreateMap<UserModel, User>()
.ForMember(dest => dest.LoginPwd, mo => mo.MapFrom(src => src.Password))
.ForMember(dest => dest.RegisterTime, mo => mo.Ignore());
}
}
}
我们先来对比一下 Core.Domain.User 与 UserModel 之间结构的异同:
Core.Domain.User | UserModel |
---|---|
Id(MongoDB.Bsono.ObjectId) | Id(System.String) |
LoginName(System.String) | LoginName(System.String) |
LoginPwd(System.String) | Password(System.String) |
– | Repassword(System.String) |
NickName(System.String) | NickName(System.String) |
PhoneNo(System.String) | PhoneNo(System.String) |
Gender(Core.Domain.Gender) | Gender(Core.Domain.Gender) |
Birthday(System.DateTime) | Birthday(System.DateTime) |
Email(System.String) | Email(System.String) |
Address(System.String) | Address(System.String) |
RegisterTime(System.DateTime) | – |
AutoMapper默认情况下会将类型相同且名称相同的属性自动映射,因此我们需要处理的映射有:
1.Core.Domain.User(下面简称为 User ) 转化为 UserModel 的过程中:
`User.Id --> UserModel.Id`
`User.LoginPwd --> UserModel.Password`
`UserModel.Repassword需要被 Mapper 忽略`
由于直接从 MongoDB 中取出的 _id 为 ObjectId 类型, 因此在映射过程中我们需要将其单独列出进行配置转换规则:
//ObjectId --> string
Mapper.CreateMap<ObjectId, string>().ConvertUsing((ObjectId source) =>
{
if (source != null)
{
return source.ToString();
}
return string.Empty;
});
可以看到, 我们通过一个匿名函数来定义了 ObjectId 转换为 string 的规则, 下面我们来定义其他几个属性转换的规则:
Mapper.CreateMap<User, UserModel>()
.ForMember(dest => dest.Password, mo => mo.MapFrom(src => src.LoginPwd))
.ForMember(dest => dest.Repassword, mo => mo.Ignore());
可以看到,我们将 LoginiPwd 映射为 Password , 且使用 Ignore() 方法来指定 Mapper 在转换过程中忽略 Repassword 属性。
2.UserModel 转化为 User 的过程中:
`UserModel.Id --> User.Id`
`UserModel.Password --> User.LoginPwd`
`UserModel.RegisterTime 需要被 Mapper 忽略`
同样,我们需要单独定义 string 与 ObjectId 转换的规则, 以保证 UserModel.Id 可以转化为 User.Id:
//string --> ObjectId
Mapper.CreateMap<string, ObjectId>().ConvertUsing((string source) =>
{
var objId = ObjectId.Empty;
if (ObjectId.TryParse(source, out objId))
{
return objId;
}
return ObjectId.Empty;
});
继续定义其他属性转化规则, 指定 Password 转换为 LoginPwd, 指定 RegisterTime 在转换过程中被 Mapper 忽略:
Mapper.CreateMap<UserModel, User>()
.ForMember(dest => dest.LoginPwd, mo => mo.MapFrom(src => src.Password))
.ForMember(dest => dest.RegisterTime, mo => mo.Ignore());
注册映射规则
下面我们需要对这个规则进行注册: 在 Global.asax 的 Application_Start 方法中加入 :
AutoMapperStartupTask.Execute();
完善 UserController 控制器
在 UserController 中只有 Index Action , 我们往其中加入 Create, Update, Delete, IsLoginNameExists。 分别作用为, 新增用户, 编辑用户, 删除用户, 判断用户登录账号是否重复(这里我就直接贴上核心代码了, 都贴出来太占版面):
Create:
if (ModelState.IsValid)
{
var entity = model.ToEntity();
entity.RegisterTime = DateTime.Now;
this._userService.CreateUser(entity);
return RedirectToAction("Index");
}
注意: 添加 ValidateAntiForgeryToken
的作用就是会验证请求表单中是否存在一个程序自动生成的隐藏字段值,可以防止伪造请求。 另外,在我们将 UserModel
对象转换为 User
对象之后, User.RegisterTime
即注册时间自然为当前时间, 设置后交由 UserService 创建
Edit
ModelState.Remove("LoginName");
ModelState.Remove("Repassword");
if (ModelState.IsValid)
{
var entity = await this._userService.FindOneAsync(id);
entity = model.ToEntity(entity);
this._userService.UpdateUser(entity);
return RedirectToAction("Index");
}
注意: 在编辑的时候, 此时的登录名数据库肯定是存在的, 那么我们在 LoginName
上设置的 RemoteAttribute
对 LoginName
的验证结果肯定是失败的,所以我们通过 ModelState.Remove("LoginName")
来移除对 LoginName
属性的验证, 只做这一点是不够的,在 Edit.cshtml 中还要做下面的工作:
div class="form-group">
@Html.LabelFor(model => model.LoginName, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.HiddenFor(model => model.LoginName)
@Html.DisplayFor(model => model.LoginName)
</div>
</div>
可以看到, 在这里将验证标记去掉, 将 LoginName
作为隐藏域, 显示方面则使用 DisplayFor
而不去使用 EditorFor
。
Details:
var result = await this._userService.FindOneAsync(id);
return View(result.ToModel());
Details 较为简单, 只需要根据 Id
获取相对应的记录, 然后传递给 Details.cshtml 强类型模板即可。
Delete:
this._userService.DeleteUser(model.Id);
return RedirectToAction("Index");
Delete 与 Details 差别不大, 都是通过传递的 Id
来操作相对应的数据记录。
IsLoginNameExists:
[HttpGet]
public async Task<ActionResult> IsLoginNameExists(string loginName)
{
var result = await this._userService.IsLoginNameExists(loginName);
return Json(!result, JsonRequestBehavior.AllowGet);
}
根据用户输入的 LoginName
作为条件检索数据库 ,若结果条数为 0 ,则说明 LoginName
没有占用, 即验证结果通过。
业务处理
最后, 我们来看看较为关键的业务逻辑层: Service
IUserService :
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using M = Core.Domain;
namespace Services.User
{
public interface IUserService
{
Task<List<M.User>> FindManyAsync(Expression<Func<M.User, bool>> exp, int limit, int skip);
Task<M.User> FindOneAsync(string id);
void CreateUser(M.User entity);
void UpdateUser(M.User entity);
void DeleteUser(string id);
Task<bool> IsLoginNameExists(string loginName);
}
}
IUserService 实现类 UserService
using Core.Data;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using M = Core.Domain;
namespace Services.User
{
public partial class UserService : IUserService
{
private IRepository<M.User> _userRepository;
public UserService(IRepository<M.User> userRepository)
{
this._userRepository = userRepository;
}
public async Task<List<M.User>> FindManyAsync(Expression<Func<M.User, bool>> exp, int limit, int skip)
{
var result = await this._userRepository.Collection.Find(exp)
.Limit(limit).Skip(skip).ToListAsync();
return result;
}
public async Task<M.User> FindOneAsync(string id)
{
var result = await this._userRepository.FindOne(id);
return result;
}
public void CreateUser(M.User entity)
{
this._userRepository.Create(entity);
}
public void UpdateUser(M.User entity)
{
this._userRepository.Update(entity);
}
public void DeleteUser(string id)
{
this._userRepository.Delete(id);
}
public async Task<bool> IsLoginNameExists(string loginName)
{
var result = await this._userRepository.Collection.Find(x => x.LoginName == loginName.Trim()).CountAsync();
return result != 0;
}
}
}
这里面包含了刚才在 UserController 中各个业务所需要用到的方法 。
页面效果
技能点
OK, 到这里, 这个 Demo 相对来说就已经较为完整了, 下面简述一下其中用到的技能点:
- MongoDB 驱动
- 领域模型与 MongoDB 数据模型之前的映射
- 使用 WebActivatorEx 注册配置
- 使用 AutoMapper 实现领域模型与DTO 之间的映射
- 使用 Autofac 实现依赖注入
- 对 ViewModel 进行数据验证
源码:http://download.csdn.net/detail/zhanxueguang/8931245 (只要1分,打赏一下)。