初学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),这种模式适用于开发初期,数据库设计目标还不明确的阶段,可以随时修改表和字段。打开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 }
初步设计已经完成了,下面需要对各个字段进行约束:
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 }
至此,一个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>
在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 }
最后需要在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 }
启动项目,会发现程序自动生成了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 }
客户端用如下代码发起请求:
1 $.getJSON("/Account/CheckUser/?username=" + username, function (data) { 2 if(data) { 3 //用户名存在 4 } 5 });
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 }
这里没有使用原生的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 }
不使用原生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 }
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 }
方法二:
在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 }
l 如何为数据库初始化一些数据?
l 使用WebAPI如何返回JSON?
打开AppStart中的webapi配置文件
将以下代码添加到Register中:
1 //webapi默认返回xml格式,添加如下代码将返回json格式 2 config.Formatters.JsonFormatter.SupportedMediaTypes.Add( 3 new MediaTypeHeaderValue("text/html"));
在webapi的Controller中使用object返回json,例如:
1 public object GetUserInfoByName(string username) 2 { 3 username = HttpUtility.UrlDecode(username); 4 return GetUserInfo(a=>a.UserName == username); 5 }
l 让Action接受Get请求
在方法名前添加属性或者为方法名添加Get前缀
1 [System.Web.Http.HttpGet] 2 public bool GetUserExists(string username)
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 );
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="看不清,点击换一张" />
l 对一个或多个实体的验证失败(或db.SaveChanges不起作用)
检查模型的约束要求与数据库设计是否一致,字符串长度超限等等这样的错误是不能保存成功的,但往往VS调试时又不能给出具体的错误在哪,所以可以添加一些代码查看错误详细信息。这样就能在输出窗口中可以看到具体的错误。
l 数据库正在使用,无法删除
当模型改动时,之前在Global中的设置会删除并重建数据库,但如果此时你对这个数据库有操作,比如查询之类的,删除就会失败,提示你数据库在使用。这个没找到好的解决方法,我只好采取关掉SQL Server服务再重启这样的笨方法来解决。