7天玩转 ASP.NET MVC
在开始时请先设置firefox中about:config中browser.cache.check_doc_frequecy设置为1,这样才能在关闭浏览器时及时更新JS
第一、二天的内容与之前的重复,这里不再重复
弱类型ViewData 中的数据类型是Object。所以我们在使用之前需要进行正确的类型转换,没有类型安全
public ActionResult GetView() { Employee emp = new Employee(); emp.FirstName = "Sukesh"; emp.LastName = "Marla"; emp.Salary = 20000; ViewData["Employee"] = emp; return View("MyView",ViewData); }
视图
<div> @{ MyWeb.Models.Employee emp = (MyWeb.Models.Employee)ViewData["Employee"]; } <b>Employee Details </b><br /> Employee Name : @emp.FirstName@emp.LastName <br /> Employee Salary: @emp.Salary.ToString("C") </div>
强类型
视图中要先@using 项目.models.类型,然后再使用@model 每个视图只有一个model
在这前请理解一下MVVM的模式:
其是把model转变为一个ViewModel再显示给用户
相关代码请查看PDF中的30页Lab 7 - View 中运用Collection
主要代码:
public ActionResult GetView() { EmployeeListViewModel employeeListViewModel = new EmployeeListViewModel();//实例化雇员视图列表模型 EmployeeBusinessLayer empBal = new EmployeeBusinessLayer(); //雇员实体模型 List<Employee> employees = empBal.GetEmployees(); //实体获取数据 List<EmployeeViewModel> empViewModels = new List<EmployeeViewModel>(); //雇员列表视图模型 foreach (Employee emp in employees) //把实体模型转换成雇员列表视图模型 { EmployeeViewModel empViewModel = new EmployeeViewModel(); empViewModel.EmployeeName = emp.FirstName + " " + emp.LastName; empViewModel.Salary = emp.Salary.ToString("C"); if (emp.Salary > 15000) { empViewModel.SalaryColor = "yellow"; } else { empViewModel.SalaryColor = "green"; } empViewModels.Add(empViewModel); } employeeListViewModel.Employees = empViewModels; //把列表转换至视图列表 employeeListViewModel.UserName = "Admin"; return View("MyView", employeeListViewModel); //输出 }
第三天
<connectionStrings> <add name="SalesERPDAL" connectionString="Data Source=(local);Initial Catalog=SalesERPDB;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings>
name为根名称,connectionString的字段设置
- Data Source 可以Serve代替其值写为计算机名,或.\SQLExpress
- Initial Catalog 数据库
- Integrated Security 连接帐号 True表示当前windows帐号,如果为网络连接就应该改为uid=sa;pwd=12345678a.;
- providerName表示使用的命名空间是"System.Data.SqlClient".而ACCESS则是"System.Data.OleDb".
- Connect Timeout表示超时秒数 30则30秒
连接数据库实体,类中重写OnModelCreating 方法,DbSet 将会展示所有可以在数据库中查询到的Employees。
public class SalesERPDAL : DbContext { /// <summary> /// 用于控制生成表的 /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Employee>().ToTable("TblEmployee");//表名称 base.OnModelCreating(modelBuilder); } /// <summary> /// 表示上下文中给定类型的所有实体的集合或可从数据库中查询的给定类型的所有实体的集合 /// </summary> public DbSet<Employee> Employees { get; set; } public SalesERPDAL():base( "testdb")//这里的testdb对应wed中的configSections名称 { } }
为Employee 类创建主键
using System.ComponentModel.DataAnnotations;//注意要声明 public class Employee { [Key] public int EmployeeId { get; set; } //设定为主键 public string FirstName { get; set; } public string LastName { get; set; } public int Salary { get; set; } }
设置 EmployeeBusinessLayer.GetEmployees()的方法
public List<Employee> GetEmployees() { SalesERPDAL salesDal = new SalesERPDAL(); return salesDal.Employees.ToList(); }
GetView()方法请查看上面
验证与提交
在Global.asax加入(数据库初始化,如果模型变更)
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SalesERPDAL>());
视图中加入消息框与提交与取消
这里没有用「input type = reset」控件,因为不会清除值,它只是将控件的值改为默认的值。例如:
<input type="text" name="FName" value="Sukesh">点完reset控件,只会变回默认值,在更新数据时可以用这按钮
在视图中呈现错误
<script> function ResetForm() { document.getElementById('TxtFName').value = ""; document.getElementById('TxtLName').value = ""; document.getElementById('TxtSalary').value = ""; } </script> @{ ViewBag.title = "CreateEmployee"; } <div> <form action="/Employee/SaveEmployee" method="post"> <table> <tr> <td> First Name: </td> <td> <input type="text" id="TxtFName" name="FirstName" value="" /> </td> </tr> <tr> <td colspan="2" align="right"> @Html.ValidationMessage("FirstName") </td> </tr> <tr> <td> Last Name: </td> <td> <input type="text" id="TxtLName" name="LastName" value="" /> </td> </tr> <tr> <td colspan="2" align="right"> @Html.ValidationMessage("LastName") </td> </tr> <tr> <td> Salary: </td> <td> <input type="text" id="TxtSalary" name="Salary" value="" /> </td> </tr> <tr> <td colspan="2" align="right"> @Html.ValidationMessage("Salary") </td> </tr> <tr> <td colspan="2"> <input type="submit" name="BtnSubmit" value="Save Employee" /> <input type="submit" name="BtnSubmit" value="Cancel" /> <input type="button" name="BtnReset" value="Reset" onclick="ResetForm();" /> </td> </tr> </table> </form> </div>
模型相关的验证 相关链接 int?表示可以为空值
- DataType。确保数据是指定的类型,例如邮箱、信用卡号、URL 等。
- EnumDataTypeAttribute。确保在Enumeration 中存在该值。
- Range Attribute。确保值在一个指定的区域内。
- Regular。认证值是否是指定的表达式。
- Required。确保值是存在的。
- StringLength。认证字符串中的最大和最小字符长度。
public class Employee { [Key] public int EmployeeId { get; set; } [Required(ErrorMessage = "请输入你的名字")] public string FirstName { get; set; } [StringLength(5, ErrorMessage = "姓不能超过5个字符")] public string LastName { get; set; } public int Salary { get; set; } }
控制器的修改
public ActionResult SaveEmployee(Employee e, string BtnSubmit) { switch (BtnSubmit) { case "Save Employee": if (ModelState.IsValid) { EmployeeBusinessLayer empBal = new EmployeeBusinessLayer(); empBal.SaveEmployee(e); return RedirectToAction("Index"); } else { return View("CreateEmployee"); } case "Cancel": return RedirectToAction("Index"); } return new EmptyResult(); }
多个提交Form
- 隐藏 Form 元素 视图中通过JavaScript将form提交
<form action="/Employee/CancelSave" id="CancelForm" method="get" style="display:none"> </form> <input type="button" name="BtnSubmit" value="Cancel" onclick="document.getElementById('CancelForm').submit()" />
- 运用 JavaScript 动态地改变动作URL
<form action="" method="post" id="EmployeeForm" > <input type="submit" name="BtnSubmit" value="Save Employee" onclick="document.getElementById('EmployeeForm').action = '/Employee/SaveEmployee'" /> ... <input type="submit" name="BtnSubmit" value="Cancel" onclick="document.getElementById('EmployeeForm').action = '/Employee/CancelSave'" /> </form>
- Ajax 不再运用 Submit 按钮,取而代之的是简单的输入按钮,然后运用JQuery 或者其它库来实现纯Ajxa 请求。
Model Binder模型绑定的问题 视图转至控制器 相关链接(注:这一节请看相关连接暂不做学习)
类型模型允许为空应该在模型里设置int?类型或设置为string类
public int? Salary{get;set;}
自定义验证新类,然后在模型字段中加入[FirstNameValidation]
public class FirstNameValidation : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value == null) // Checking for Empty Value { return new ValidationResult("请输入你的名字"); } else { if (value.ToString().Contains("@")) { return new ValidationResult("名字不应该包含 @"); } } return ValidationResult.Success; } }
第四天保留值
新增一个类,用来存储临时值,类型全为STRING然后在输入有误的过程中设置返回
CreateEmployeeViewModel vm = new CreateEmployeeViewModel();//存储临时值类 vm.FirstName = e.FirstName; //赋值 vm.LastName = e.LastName; if (e.Salary.HasValue) { vm.Salary = e.Salary.ToString();//转换为字符存存临时值 } else { vm.Salary = ModelState["Salary"].Value.AttemptedValue;//显示原始值 } return View("CreateEmployee", vm); // Day 4 Change - Passing e here
客户端验证(注:客户端认证与服务端验证不冲突)
function IsFirstNameEmpty() { if (document.getElementById('TxtFName').value == "") { return '名字不应该是空的'; } else { return ""; } } function IsFirstNameInValid() { if (document.getElementById('TxtFName').value.indexOf("@") != -1) { return '姓不应该包含@'; } else { return ""; } } function IsLastNameInValid() { if (document.getElementById('TxtLName').value.length>=5) { return '姓不应该超过5个字符。'; } else { return ""; } } function IsSalaryEmpty() { if (document.getElementById('TxtSalary').value=="") { return '工资不应该是空的'; } else { return ""; } } function IsSalaryInValid() { if (isNaN(document.getElementById('TxtSalary').value)) { return '输入有效的工资'; } else { return ""; } } function IsValid() { var FirstNameEmptyMessage = IsFirstNameEmpty(); var FirstNameInValidMessage = IsFirstNameInValid(); var LastNameInValidMessage = IsLastNameInValid(); var SalaryEmptyMessage = IsSalaryEmpty(); var SalaryInvalidMessage = IsSalaryInValid(); var FinalErrorMessage = "错误:"; if (FirstNameEmptyMessage != "") FinalErrorMessage += "\n" + FirstNameEmptyMessage; if (FirstNameInValidMessage != "") FinalErrorMessage += "\n" + FirstNameInValidMessage; if (LastNameInValidMessage != "") FinalErrorMessage += "\n" + LastNameInValidMessage; if (SalaryEmptyMessage != "") FinalErrorMessage += "\n" + SalaryEmptyMessage; if (SalaryInvalidMessage != "") FinalErrorMessage += "\n" + SalaryInvalidMessage; if (FinalErrorMessage != "错误:") { alert(FinalErrorMessage); return false; } else { return true; } }
视图中加入
<script src="~/Scripts/Validations.js"></script> <input type="submit" name="BtnSubmit" value="Save Employee"onclick="return IsValid();" />
添加授权认证
新建控制器方法Login
在要控制的页面方法前加入[Authorize]属性
如果所有控制器页面都要认证的话,应在App_start的FilterConfig.cs文件中增加一行,注意是增加
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute());//Old line filters.Add(new AuthorizeAttribute());//New Line }
然后在AuthenticationController附加上AllowAnonymous属性
Web.confing:
<authentication mode="Forms"> <forms loginUrl="~/Authentication/Login"></forms> </authentication>
业务层加入:
public bool IsValidUser(UserDetails u) { if (u.UserName == "Admin" && u.Password == "Admin") { return true; } else { return false; } }
login视图层
@model MyWeb.Models.UserDetails @{ ViewBag.Title = "Login"; } <div> @using (Html.BeginForm("DoLogin", "Authentication", FormMethod.Post)) { @Html.LabelFor(c => c.UserName) @Html.TextBoxFor(x => x.UserName) <br /> @Html.LabelFor(c => c.Password) @Html.PasswordFor(x => x.Password) <br /> <input type="submit" name="BtnSubmit" value="Login" /> } </div>
然后控制器
[HttpPost] public ActionResult DoLogin(UserDetails u) { EmployeeBusinessLayer bal = new EmployeeBusinessLayer();//实例业务层 if (bal.IsValidUser(u))//判定 { FormsAuthentication.SetAuthCookie(u.UserName, false); //为用户提供验证的Cookie,非永久性的 return RedirectToAction("Index", "Employee"); } else { ModelState.AddModelError("CredentialError", "无效的用户名或密码"); return View("Login"); } }
7示例21 在Login页实现客户端的认证
在页面里添加
<script src="~/Scripts/jquery-1.10.2.js"></script> <script src="~/Scripts/jquery.validate.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
然后页面视图里加入@Html.ValidationMessageFor(x =>x.UserName)用于显示错误信息
第5天
在视图呈现分部视图在Shared文件夹添加Foorer,其视图加入了模板与模型,下面传入模型与调用方法
@{ Html.RenderPartial("Footer", Model.FooterData);//在区域内Razor,这个方法比Partial更快,Partial返回为MvcHtmlString非字符串 } @Html.Partial("Footer", Model.FooterData)
实现基于角色的安全性
枚举用户状态UserStatus.cs 并修改逻辑
public enum UserStatus { AuthenticatedAdmin, AuthentucatedUser, NonAuthenticatedUser }
业务层
public UserStatus GetUserValidity(UserDetails u) { if (u.UserName == "Admin" && u.Password == "Admin") { return UserStatus.AuthenticatedAdmin; } else if (u.UserName == "Sukesh" && u.Password == "Sukesh") { return UserStatus.AuthentucatedUser; } else { return UserStatus.NonAuthenticatedUser; } }
然后通过DoLogin的方法Session["IsAdmin"] = IsAdmin;设置当前会话的HttpSessionStateBase 对象
FormsAuthentication.SetAuthCookie(u.UserName, false); Session["IsAdmin"] = IsAdmin; return RedirectToAction("Index", "Employee");//重定向致...
在视图中调用,返回GetAddNewLink的方法结果
@{ Html.RenderAction("GetAddNewLink"); }
GetAddNewLink再通过判定HttpSessionStateBase 对象的bool值决定是否返回显示相关的add代码
public ActionResult GetAddNewLink() { if (Convert.ToBoolean(Session["IsAdmin"])) { return PartialView("AddNewLink"); } else { return new EmptyResult(); } }
过滤器
新建文件夹Filter下新建AdminFilter.cs继承ActionFilterAttribute并重写OnActionExecuting方法
public class AdminFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (!Convert.ToBoolean(filterContext.HttpContext.Session["IsAdmin"])) { filterContext.Result = new ContentResult() { Content = "未授权访问指定资源." }; } } }
在防用URL的方法前加入[AdminFilter]
在方法前加入[ChildActionOnly]该方法表示只能在View中通过Html.Action或Html.RenderAction来使用,不能通过控制器调用
24 处理CSRF攻击
链接 方法前插入[ValidateAntiForgeryToken]
用法:在View->Form表单中:<%:Html.AntiForgeryToken()%>
在Controller->Action动作上:[ValidateAntiForgeryToken]
原理:1、<%:Html.AntiForgeryToken()%>这个方法会生成一个隐藏域:<inputname="__RequestVerificationToken" type="hidden" value="7FTM...sdLr1" />并且会将一个以"__RequestVerificationToken“为KEY的COOKIE给控制层。
2、[ValidateAntiForgeryToken],根据传过来的令牌进行对比,如果相同,则允许访问,如果不同则拒绝访问。
关键:ValidateAntiForgeryToken只针对POST请求。
换句话说,[ValidateAntiForgeryToken]必须和[HttpPost]同时加在一个ACTION上才可以正常使用。
继承
新建BaseViewModel类
public class BaseViewModel { public string UserName { get; set; } public FooterViewModel FooterData { get; set; }//New Property }
从 EmployeeListViewModel 类中移除UserName 和FooterData 属性,然后让它继承BaseViewModel 类。
然后创建布局页
@using MyWeb.ViewModels @model BaseViewModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@RenderSection("TitleSection")</title> @RenderSection("HeaderSection", false) </head> <body> <div style="text-align:right"> 欢迎你, @Model.UserName <a href="/Authentication/Logout">Logout</a> </div> <hr /> <div> @RenderSection("ContentBody") </div> @Html.Partial("Footer", Model.FooterData) </body> </html>
更改视图页中的 (注意视图模型的基类也要更改哦)
@{
Layout = "~/Views/Shared/MyLayout.cshtml";
}
@section ContentBody{内容,false}表示里的内存是在布局页中 @RenderSection("ContentBody")中显示的(主要用于定位内容的位置),参数false表示不一定要提供,true表示一定要提供
而布局页中的@RenderBody() 表示显示不在指定部分中的页面内容
应用到所有视图_ ViewStart.cshtml文件里添加 Layout = "~/Views/Shared/_Layout.cshtml";
第五天上传
异步:
上传类:
public class FileUploadViewModel:BaseViewModel { public HttpPostedFileBase fileUpload { get; set; }//充当类的基类,这些类提供对客户端已上载的单独文件的访问。 }
视图中:pplication/x-www-form-urlencoded与multipart/form-data的区别是data能以二进制发送,发大文件数据时效率更高
@model FileUploadViewModel <form action="/BulkUpload/Upload" method="post" enctype="multipart/form-data"> Select File : <input type="file" name="fileUpload" value="" /> <input type="submit" name="name" value="Upload" /></form>
控制器:(异步处理上传文件方法)
public class BulkUploadController : AsyncController { [AdminFilter] public async Task<ActionResult> Upload(FileUploadViewModel model) { int t1 = Thread.CurrentThread.ManagedThreadId; List<Employee> employees = await Task.Factory.StartNew<List<Employee>>(() => GetEmployees(model)); // List<Employee> employees = GetEmployees(model); EmployeeBusinessLayer bal = new EmployeeBusinessLayer(); bal.UploadEmployees(employees); return RedirectToAction("Index", "Employee"); } /// <summary> /// 取得上传文件的雇员信息 /// </summary> /// <param name="model"></param> /// <returns></returns> private List<Employee> GetEmployees(FileUploadViewModel model) { List<Employee> employees = new List<Employee>(); StreamReader csvreader = new StreamReader(model.fileUpload.InputStream); csvreader.ReadLine(); // 假设第一行是头。 while (!csvreader.EndOfStream) { var line = csvreader.ReadLine(); var values = line.Split(',');//不同值之间采用逗号分隔 Employee e = new Employee(); e.FirstName = values[0]; e.LastName = values[1]; e.Salary = int.Parse(values[2]); employees.Add(e); } return employees; } }
异常信息显示
wed.confing中修改,这里修改后App_Start 文件夹下的FilterConfig.cs会自动添加Global 级别过滤器,并修改404错误信息
<system.web> <customErrors mode="On"><error statusCode="404" redirect="~/Error/Index"/></customErrors>
错误页:显示错误信息
@model HandleErrorInfo Error Message :@Model.Exception.Message<br /> Controller: @Model.ControllerName<br /> Action: @Model.ActionName
控制器:
[AllowAnonymous] //跳过权限控制器即跳过基于ActionFilterAttribute为基类的方法 public class ErrorController : Controller { // GET: Error public ActionResult Index() { Exception e = new Exception("Invalid Controller or/and Action Name"); HandleErrorInfo eInfo = new HandleErrorInfo(e, "Unknown", "Unknown"); return View("Error", eInfo); } }
不同的错误视图:
方法1:在视图中加入
- [HandleError(View="MyError")]
- [HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]
- [HandleError(View = "NotFiniteError", ExceptionType = typeof(NotFiniteNumberException))]
方法2:在过滤FilterConfig.cs里面加入
filters.Add(new HandleErrorAttribute() { ExceptionType = typeof(DivideByZeroException), View = "DivideError" }); filters.Add(new HandleErrorAttribute() { ExceptionType = typeof(NotFiniteNumberException), View = "NotFiniteError" }); filters.Add(new HandleErrorAttribute());
异常日志
新增FileLogger类,在LogException输出文本
public void LogException(Exception e) { File.WriteAllLines("C://Error//" + DateTime.Now.ToString("dd-MM-yyyy mm hh ss") + ".txt", new string[] {"Message:"+e.Message,"StackTrace:"+e.StackTrace}); }
新增EmployeeExceptionFilter类,并继承处理异常的HandleErrorAttribute类的方法,重写OnException,在重写OnException方法时,再去调用运行基类的OnException方法与LogException方法
public class EmployeeExceptionFilter:HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { FileLogger logger = new FileLogger(); logger.LogException(filterContext.Exception); base.OnException(filterContext);//调用基类的方法 } }
修改过滤器配置FilterConfig.cs
filters.Add(new EmployeeExceptionFilter());//移除HandleErrorAttribute异常的方法
注意:C盘Error的目录必须有相应IIS的权限
在OnException方法中添加filterContext.ExceptionHandled = trueg表示已处理异常,不须在呈现默认的错误页
路由
打开/Employee/BulkUpload网页(重复,不再说明)
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Upload", url: "Employee/BulkUpload", defaults: new { controller = "BulkUpload", action = "Index" } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
请打开的URL更友好如http://localhost:5832/Employee/变为http://localhost:5832/Employee/List
路由里添加routes.MapMvcAttributeRoutes();//添加在routes.IgnoreRoute后面
index方法前加入 [Route("Employee/List")]
方法参数限制
[Route("Employee/List/{id:int}")]
我们可以拥有如下限制。
1. {x:alpha} – 字符串认证
2. {x:bool} – 布尔认证
3. {x:datetime} – Date Time 认证
4. {x:decimal} – Decimal 认证
5. {x:double} – 64 位Float 认证
6. {x:float} – 32 位Float 认证
7. {x:guid} – GUID 认证
8. {x:length(6)} – 长度认证
9. {x:length(1,20)} – 最小和最大长度认证
10.{x:long} – 64 位Int 认证
11.{x:max(10)} – 最大 Integer 长度认证
12.{x:maxlength(10)} – 最大长度认证
13.{x:min(10)} – 最小 Integer 长度认证
14.{x:minlength(10)} – 最小长度认证
15.{x:range(10,50)} – 整型 Range 认证
16.{x:regex(SomeRegularExpression)} – 正则表达式认证
第七天让项目有组织性
添加>新建解决方案(以下)与类库
View And Controller(存放视图与控制器)
- 迁移之前项目文件,移除解决方案的EntityFramework包.在Global.asax中移除Database.SetInitializer添加
using BusinessLayer; ... BundleConfig.RegisterBundles(BundleTable.Bundles); BusinessSettings.SetBusiness();
Model(模型)
- BusinessLayer(业务层) 引用(数据实体层)与(业务实体层) 添加BusinessSettings类,并添加EntityFramework包
public static void SetBusiness() { DatabaseSettings.SetDatabase(); }
- BusinessEntities(业务实体层) 引用System.ComponentModel.DataAnnotations
ViewModel(视图模型)
- ViewModel(视图模型层) 引用 System.Web
- 迁移项目文件中的视图模型到视图模型层
Data Access Layer(数据访问层)
- 类库DataAccessLayer(数据实体层)引用 (业务实体层) 与 (业务层)、并添加EntityFramework包, 注意:在数据实体的类中加入using System.Data.SqlClient;
- 新增类:DatabaseSettings其中包含方法:
public static void SetDatabase() { Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SalesERPDAL>()); }
应用JQuery UI页面展示,
添加:JQuery UI,然后在主视图中加入
<head> <meta name="viewport" content="width=device-width" /> <script src="~/Scripts/jquery-1.8.0.js"></script> <script src="~/Scripts/jquery-ui-1.11.4.js"></script> <script> function OpenAddNew() { $.get("/SPA/Main/AddNew").then ( function (r) { $("<div id='DivCreateEmployee'></div>").html(r). dialog({ width: 'auto', height: 'auto', modal: true, title: "Create New Employee", close: function () { $('#DivCreateEmployee').remove(); } }); } ); } </script> <title>Employee Single Page Application</title> <link href="~/Content/themes/base/all.css" rel="stylesheet" /> ....后面加相关内容
子视图
<a href="#" onclick="OpenAddNew();">Add New</a>