初学MVC,踩了不少坑,所以通过实现一个用户注册功能把近段时间学习到的知识梳理一遍,方便以后改进和查阅。
问题清单:
l 为什么EF自动生成的表名后自动添加了s?
l 如何为数据库初始化一些数据?
l 使用WebAPI如何返回JSON?
l 让Action接受Get请求
l 如何使路由匹配不同的URL
l 如何调试路由
l VS2013如何添加jQuery智能提示?
l 为何在Session中的验证码打印出来后与上一次的相同?
l 对一个或多个实体的验证失败(或db.SaveChanges不起作用)
l 数据库正在使用,无法删除
数据库设计(Code First)
这里并没有采用传统的数据库设计方案,而是使用了 代码优先(code first),这种模式适用于开发初期,数据库设计目标还不明确的阶段,可以随时修改表和字段。打开VS,新建一个项目,选择ASP>NET MVC 4 Web应用程序:
操作完成后,可以看到以下目录结构:
选择Models文件夹,新建一个类Model.cs:
1 namespace xCodeMVC.Models
2 {
3 public class UserInfo
4 {
5 //ID
6 public int UserID { get; set; }
7
8 //用户名
9 public string UserName { get; set; }
10
11 //密码
12 public string UserPwd { get; set; }
13
14 //邮箱
15 public string UserEmail { get; set; }
16
17 //用户组:0代表管理员,1代表普通用户
18 public int UserRank { get; set; }
19
20 //注册时间
21 public DateTime RegisterTime { get; set; }
22 }
23 }
View Code
初步设计已经完成了,下面需要对各个字段进行约束:
l UserID:主键、自增长
l UserName:长度为2到15个字符、必填
l UserPwd:长度为6到20个字符、必填
l UserEmail:必填
l UserRank:默认为1
l RegisterTime:注册时间(DateTime格式)
添加约束后的代码:
1 namespace xCodeMVC.Models
2 {
3 public class UserInfo
4 {
5 [Key]
6 [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
7 public int UserID { get; set; }
8
9 [Required(ErrorMessage="用户名不能为空")]
10 [Display(Name="用户名")]
11 [StringLength(20,MinimumLength=2,ErrorMessage="用户名必须为{2}到{1}个字符")]
12 public string UserName { get; set; }
13
14 [Required(ErrorMessage="密码不能为空")]
15 [Display(Name="密码")]
16 [StringLength(50, MinimumLength = 6, ErrorMessage = "密码必须为{2}到20个字符")]
17 [DataType(DataType.Password)]
18 public string UserPwd { get; set; }
19
20
21 [Display(Name="邮箱")]
22 [Required(ErrorMessage="邮箱必填")]
23 [RegularExpression(@"^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$",
24 ErrorMessage = "请输入正确的Email格式\n示例:abc@123.com")]
25 public string UserEmail { get; set; }
26
27 public int UserRank { get; set; }
28
29 public DateTime RegisterTime { get; set; }
30 }
31 }
View Code
至此,一个model就建好了。
接着需要配置一下web.config,在configuration节点内添加数据库连接字符串,后面实体会用到:
1 <configuration>
2 <connectionStrings>
3 <add name="connection" providerName="System.Data.SqlClient" connectionString="Data Source=.;Initial Catalog=cxyDB;Integrated Security=True" />
4 </connectionStrings>
5 </configuration>
View Code
在Models文件夹内再建一个新类DBContext.cs,用于进行数据库的相关操作:
1 namespace xCodeMVC.Models
2 {
3 public class DBContext : DbContext
4 {
5 //connection是webconfig内的连接字符串
6 public DBContext() : base("connection") { }
7
8 public DbSet<UserInfo> userInfo { get; set; }
9 }
10 }
View Code
最后需要在Global.asax文件中添加如下配置(如何为数据库初始化一些数据?):
1 using xCodeMVC.Models;
2
3 public class MvcApplication : System.Web.HttpApplication
4 {
5 //DropCreateDatabaseIfModelChanges表示当模型改变时删除并重新创建数据库
6 //还有一个Always表示总是在启动时执行删除并重建数据库操作
7 public class DBInit:DropCreateDatabaseIfModelChanges<DBContext>
8 {
9 protected override void Seed(DBContext context)
10 {
11 //为数据库insert一些初始数据
12 context.userInfo.Add(new UserInfo
13 {
14 UserName = "troy",
15 UserPwd = "111111",
16 UserEmail = "abc@163.com",
17 UserRank = 0,
18 RegisterTime = DateTime.Now
19 });
20 base.Seed(context);
21 }
22 }
23 protected void Application_Start()
24 {
25 Database.SetInitializer(new DBInit());
26 //省略生成时的代码...
27 }
28 }
View Code启动项目,会发现程序自动生成了cxyDB的数据库,并添加了一个名为UserInfoes的表,里面有一条初始记录:
不过需要注意,这里生成的表名是UserInfoes,后面会说明这种情况(为什么EF自动生成的表名后自动添加了s?)。
表单设计
l 客户端验证
首先焦点移出文本框时,需要远程访问一个API,查询数据库中用户名是否存在。在Controllers文件夹选中AccountController.cs控制器并添加如下代码:
1 namespace xCodeMVC.Controllers
2 {
3 public class AccountController : Controller
4 {
5 private DBContext db = new DBContext();
6 // GET: /Account/CheckUser
7 [HttpGet]
8 public JsonResult CheckUser(string username)
9 {
10 var exists = db.userInfo.Where(a => a.UserName == username).Count() != 0;
11
12 return Json(exists, JsonRequestBehavior.AllowGet);
13 }
14 }
15 }
View Code
客户端用如下代码发起请求:
1 $.getJSON("/Account/CheckUser/?username=" + username, function (data) {
2 if(data) {
3 //用户名存在
4 }
5 });
View Code
l 图形验证码
在解决方案中新建一个类库项目,编写生成图形验证码的代码,编译后在MVC项目中引用其生成的dll文件
1 public ActionResult GetValidateImg()
2 {
3 int width = 60, height = 28, fontsize = 12;
4 string code = string.Empty;
5 byte[] bytes = ValidateCode.CreateCode(out code, 4, width, height, fontsize);
6
7 Session["v_code"] = code.ToLower();
8
9 return File(bytes,@"image/jpeg");
10 }
View Code
视图
这里没有使用原生的form表单,而是使用了MVC的html辅助方法。
首先要在页面中引入所需的model:
@model xCodeMVC.Models.UserInfo
这样就能使用表单增强工具了(省略了一些代码):
1 @using (Html.BeginForm("Register", "Account", FormMethod.Post, new { name = "register",onsubmit = "return checkform()"}))
2 {
3 @Html.LabelFor(model => model.UserName)
4 @Html.TextBoxFor(model => model.UserName, new { @class = "text-box" })
5
6 @Html.LabelFor(model => model.UserPwd)
7 @Html.EditorFor(model => model.UserPwd)
8
9 @Html.LabelFor(model => model.UserEmail)
10 @Html.EditorFor(model => model.UserEmail)
11
12 <input class="regBtn" type="submit" value="注册" />
13 <input class="resetBtn" type="reset" value="重置" />
14
15 //令牌,防止重复提交
16 @Html.AntiForgeryToken()
17 //模型错误信息汇总,也可以在每一项后面添加
18 //@Html.ValidationMessage
19 @Html.ValidationSummary(false)
20 }
View Code
不使用原生form是为了精简代码,将复杂的验证逻辑交给MVC框架去做。
完成后台注册
表单提交的地址是AccountController中的Register方法,该方法只接受HttpPost请求。
1 // POST: /Account/Register
2 [HttpPost]
3 [AllowAnonymous]
4 [ValidateAntiForgeryToken]
5
6 public ActionResult Register(UserInfo userInfo)
7 {
8 string checkPwd = Request["ChkUserPwd"].ToString();
9 string vCode = Request["vCode"].ToString().ToLower();
10
11 if(string.IsNullOrEmpty(checkPwd))
12 {
13 ModelState.AddModelError("ChkUserPwd", "确认密码不能为空");
14 }
15 else
16 {
17 if (Md5Hash(checkPwd) != Md5Hash(userInfo.UserPwd))
18 {
19 ModelState.AddModelError("PwdRepeatError", "确认密码不正确");
20 }
21 }
22
23
24 if (!ChkValidateCode(vCode))
25 {
26 ModelState.AddModelError("vCode", "验证码不正确");
27 }
28
29 bool isUserExists = db.userInfo.Where(a => a.UserName == userInfo.UserName).Count() != 0;
30 bool isEmailExists = db.userInfo.Where(a => a.UserEmail == userInfo.UserEmail).Count() != 0;
31
32 if (isUserExists) ModelState.AddModelError("UserName", "用户名已被占用");
33 if (isEmailExists) ModelState.AddModelError("UserEmail", "邮箱已被注册");
34
35
36 if(!ModelState.IsValid)
37 {
38 return View(userInfo);
39 }
40 userInfo.RegisterTime = DateTime.Now;
41 userInfo.UserPwd = Md5Hash(userInfo.UserPwd);
42 try
43 {
44 db.userInfo.Add(userInfo);
45 db.SaveChanges();
46 return RedirectToAction("Index", "Home");
47 }
48 catch (DbEntityValidationException dbEx)
49 {
50 foreach (var validationErrors in dbEx.EntityValidationErrors)
51 {
52 foreach (var validationError in validationErrors.ValidationErrors)
53 {
54 System.Diagnostics.Trace.TraceInformation("Property: {0} Error: {1}",
55 validationError.PropertyName,
56 validationError.ErrorMessage);
57 }
58 }
59 throw;
60 }
61 }
View Code
问题汇总
l 为什么EF自动生成的表名后自动添加了s?
这种情况是EF默认的,可以修改一些配置去掉默认规则。
方法一:
在Models.cs中修改,在类名前加上属性[Table(TableName)]
1 namespace xCodeMVC.Models
2 {
3 [Table("UserInfo")]
4 public class UserInfo
5 {
6 public int UserID { get; set; }
7 //......
8 }
9 }
View Code
方法二:
在DBContext.cs中修改
1 namespace xCodeMVC.Models
2 {
3 public class DBContext : DbContext
4 {
5 protected override void OnModelCreating(DbModelBuilder modelBuilder)
6 {
7 //modelBuilder.Entity<UserInfo>().ToTable("UserInfo");
8 //或者
9 //移除默认约定规则,比如在表名后默认加上“s”
10 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
11 base.OnModelCreating(modelBuilder);
12 }
13
14 public DBContext() : base("connection") { }
15
16 public DbSet<UserInfo> userInfo { get; set; }
17 }
18 }
View Code
l 如何为数据库初始化一些数据?
l 使用WebAPI如何返回JSON?
打开AppStart中的webapi配置文件
将以下代码添加到Register中:
1 //webapi默认返回xml格式,添加如下代码将返回json格式
2 config.Formatters.JsonFormatter.SupportedMediaTypes.Add(
3 new MediaTypeHeaderValue("text/html"));
View Code
在webapi的Controller中使用object返回json,例如:
1 public object GetUserInfoByName(string username)
2 {
3 username = HttpUtility.UrlDecode(username);
4 return GetUserInfo(a=>a.UserName == username);
5 }
View Code
l 让Action接受Get请求
在方法名前添加属性或者为方法名添加Get前缀
1 [System.Web.Http.HttpGet]
2 public bool GetUserExists(string username)
View Code
l 如何使路由匹配不同的URL
可以参考下面的匹配模式,重点在于为每个路由指定相应的action,url里可以没有action和controller,但为其指定一些值有助于区分各个路由。
1 //api/getuser/1
2 config.Routes.MapHttpRoute(
3 name: "getUserInfoByID",
4 routeTemplate: "api/{controller}/{id}",
5 constraints: new { id = @"^\d*$" },
6 defaults: new { controller = "getuser", id = RouteParameter.Optional }
7 );
8
9 //api/getuser/troy
10 config.Routes.MapHttpRoute(
11 name: "getUserInfoByName",
12 routeTemplate: "api/{controller}/{username}",
13 constraints: new { username = @"^\w*$" },
14 defaults: new { controller = "getuser", action = "GetUserInfoByName" }
15 );
16
17 //访问形式 api/getuser/?ids=1,3,52,100...
18 config.Routes.MapHttpRoute(
19 name: "getUserInfoByCoupleOfIds",
20 routeTemplate: "api/{controller}/ids={ids}",
21 constraints: new { ids = @"^\d+,?$" },
22 defaults: new { controller = "getuser" }
23 );
24
25 //api/getuser/check=troy
26 config.Routes.MapHttpRoute(
27 name: "ChkUserExists",
28 routeTemplate: "api/{controller}/check={username}",
29 constraints: new { username = @"\w*" },
30 defaults: new { controller = "getuser", action = "ChkUserExists" }
31 );
View Code
l 如何调试路由
很多时候不知道程序采用了哪个路由,可以安装RouteDebugger来查看当前匹配了哪个路由。
安装方法:
工具->NuGet程序包管理器->控制台->Install-Package RouteDebugger
等待安装完成,在web.config的appsettings节点下可以看到
<add key="RouteDebugger:Enabled" value="true" />
表示路由调试已经打开了,运行程序就可以看到。
l VS2013如何添加jQuery智能提示?
在脚本中添加如下代码:
/// <reference path="jquery-1.11.1.js" />
l 为何在Session中的验证码打印出来后与上一次的相同?
这其实是正确的,因为页面生成在前,而访问验证码在后,Session是在生成验证码时记录的,此时页面的Session还是空的,随后它的值才被赋为验证码的值,所以刷新页面就看到了上一次Session中的验证码。
客户端通过以下代码访问验证码:
1 <img id="v_code" class="imgborder" src="@Url.Action("GetValidateImg", "Account")?t=@DateTime.Now.Ticks"
2 alt="看不清,点击换一张" />
View Code
l 对一个或多个实体的验证失败(或db.SaveChanges不起作用)
检查模型的约束要求与数据库设计是否一致,字符串长度超限等等这样的错误是不能保存成功的,但往往VS调试时又不能给出具体的错误在哪,所以可以添加一些代码查看错误详细信息。这样就能在输出窗口中可以看到具体的错误。
l 数据库正在使用,无法删除
当模型改动时,之前在Global中的设置会删除并重建数据库,但如果此时你对这个数据库有操作,比如查询之类的,删除就会失败,提示你数据库在使用。这个没找到好的解决方法,我只好采取关掉SQL Server服务再重启这样的笨方法来解决。