初學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 }
初步設計已經完成了,下面需要對各個字段進行約束:
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服務再重啟這樣的笨方法來解決。