详解ASP.NET MVC入门级实例教程
对于ASP.NET MVC,很多开发者持欢迎的态度。不过在吸引了更多人目光的同时,我们也应该看到有很多开发人还不太了解ASP.NET MVC。本文的作用在于给大家一个简单易懂的ASP.NET MVC教程。
ASP.NET MVC 1.0发布有段儿时间了,刚发布不久的时候试用了一下,做了一个简单的BBS,现在总结一些经验和小技巧和大家分享。网上关于ASP.NET MVC的系列教程有好几个,所以就不从头开始介绍了,结尾处给大家推荐了几个链接,需要的话可以从头系统的看看。
1、ASP.NET MVC介绍及与ASP.NET WebForm的区别
刚开始为了搞清楚ASP.NET MVC到底值不值得用,翻来覆去想了一个多礼拜,看了好多资料和评论,最后决定还是值得一用。MVC不是一个简单的设计模式,更像一种架构模式,或者一种思想,刚开始一听MVC想到的就是模板引擎,NVelocity,StringTempleate等,但感觉如果只是为了用模板这种独立的前台设计方式,没必要用ASP.NET MVC,大多数情况用Repeaterk控件和自定义控件儿就能做到,而且ASPX页面上本来就可以写c#代码,一些比较复杂的界面表现逻辑用普通的WebForm也能实现,其实ASP.NET MVC的VIEW部分默认用的还是aspx的解析器。ASP.NET MVC的View部分让你写一些大型的,布局复杂的网站更方便,更底层,更直接,很受对css,js很熟悉的开发者的欢迎。
当你理解了MVC的思想后,会发现ASP.NET MVC的好处真正在于Controller和Action,你写一段代码能很明确的知道是在处理什么请求,毕竟web程序处理的是一个一个的http请求,不像windows桌面程序,基于事件驱动更直观。ASP.NET MVC的Controller让你写一些web api或者rest风格的接口很方便(以前可能要用HttpHandler来做),这些Controller只负责提供数据(具体的ActionResult类,如JsonResult,JavascriptResult等)给使用者,比如一个Ajax调用,或者View层。
至于Model层,我看网上大多数人是用LINQ TO SQL实现的,毕竟使用起来很简单,设计好表,用LINQ 设计器往vs.net里一拖就能用了。而且本身就是强类型的,再在自动生成的代码上加一些分部方法,就可以实现数据的有效性验证等。还有就是对LINQ做的Model进行数据持久化和查询的时候更方便,直接用DbContext一个类,增删改查全能搞定。
有得就有舍,ASP.NET MVC虽然提供了先进的思想和一些便利,但ASP.NET以前的一些东西不能用了,比如以前自己写的一些服务器控件儿不能用了,WebPart,皮肤,各种数据绑定控件等都不能用了,但Master页还能用,Asp.net Ajax control toolkit(服务端)也不能用了,但asp.net ajax library(客户端js库)还能继续使用,基于页面和目录的授权不能用了(因为现在没页面,只有view了),但MemberShip和Forms身份验证还是支持的。标准WebForm的生命周期变了,好些事件没了,现在你可以写一些拦截器(Action拦截器、Result拦截器和Exception拦截器)来影响请求的处理过程,还有一些区别,总之失去的东西,都有变通的方法能找吧回来。
2、linq to sql如何获取插入语句产生的标识列的值?
其实很简单,把对象插入数据库后,直接取值就行了,如下BBSPost是一个实体类,其中PostID在数据库里是自增列。
var db = new BBSDbContext(connstr); BBSPost post = new BBSPost() post.PostUser = User.Identity.Name; post.PostTime = DateTime.Now; db.BBSPosts.InsertOnSubmit(post); db.SubmitChanges(); int postid = post.PostID; //这里就能取到标识列的值。
3、ASP.NET MVC里在请求提交后如何后维持滚动条位置?
在WebForm里再简单不过了,在web.config里配置MaintainScrollPositionOnPostBack=true就搞定了,但在MVC里就不行了。我们知道了原理后,可以自己实现,其实就是在提交表单或者滚动条滚动的事件里捕获当前滚动条的位置,把数值放在一个隐藏域里,提交给服务端,服务端应答后,从隐藏域里取出滚动条的位置,用js操纵滚动条滚动到上次的位置。
我们先在View里写一个隐藏域,如下
%= Html.Hidden("scroll", ViewData["scrool"])%>
然后在处理客户端请求的action里给ViewData里存储一下提交上来的值(从FormCollection里取)。
public ActionResult reply(BBSPost post, FormCollection coll) { ... ViewData["scroll"] = coll["scroll"]; ... return View("show_post",posts); }
这样页面提交后隐藏域里就会保存着提交前滚动条的位置,然后我们在用JQuery写一些逻辑实现最终的效果。
script type="text/javascript"> $(function() { $(document).scroll(function() { //在滚动条滚动的时候更新隐藏域里滚动条的位置值,经测试不支持IE8 $("#scroll").val(document.documentElement.scrollTop); }); $("form").submit(function() { //在表单提交的时候更新隐藏域里滚动条的位置值 $("#scroll").val(document.documentElement.scrollTop); return true; }); //在document.load事件里取出隐藏域的值,并设置滚动条的位置 document.documentElement.scrollTop = $("#scroll").val(); }); script>
4、验证用户输入
数据有效性的验证基本上哪个程序都躲不了,LINQ 和ASP.NET MVC的配合,让数据验证的实现也很方便。
LINQ TO SQL设计器自动生成的类是一个分部类,就是半块儿的类,你可以写一个分步类,在自动生成的类上加一些扩展的方法,如下我们在LINQ实体类BBSPost上加了一个GetRuleViolations方法,一个IsValid属性,其中GetRuleViolations方法验证给实体类赋的值的有效性,用yield关键字返回一个枚举器,这里可以写你自己的数据有效性验证逻辑。
IsValid属性内部调用GetRuleViolations方法,如果返回的枚举器的Count不是0的话,表示数据有效性验证不通过。
另外为了方式LINQ TO SQL往数据库里写入无效数据,我们给OnValidate分布方法加了两行代码,在数据有效性验证不通过的情况下写数据库之前抛出异常。
public partial class BBSPost { public bool IsValid { get { return (GetRuleViolations().Count() == 0); } } public IEnumerable GetRuleViolations() { if (String.IsNullOrEmpty(Title)) yield return new RuleViolation("标题必须输入", "Title"); if (String.IsNullOrEmpty(Content)) yield return new RuleViolation("内容必须输入", "Content"); yield break; } partial void OnValidate(ChangeAction action) { if (!IsValid) throw new ApplicationException("Rule violations prevent saving"); } }
RuleViolation是一个辅助类,很简单。
public class RuleViolation { public string ErrorMessage { get; private set; } public string PropertyName { get; private set; } public RuleViolation(string errorMessage) { ErrorMessage = errorMessage; } public RuleViolation(string errorMessage, string propertyName) { ErrorMessage = errorMessage; PropertyName = propertyName; } }
在写action的时候,捕获SubmitChanges操作的异常,然后给ModelState里添加自定义验证逻辑的异常,ModelState会把添加进去的异常传递给View层,供View层使用。
try { var db = new BBSDbContext(GlobalHelper.Conn); post.PostUser = User.Identity.Name; //其它赋值操作 db.BBSPosts.InsertOnSubmit(post); db.SubmitChanges(); ModelState.Clear(); } catch (Exception ex) { ModelState.AddModelErrors(post.GetRuleViolations()); ModelState.AddModelError("exception", ex); }
默认的ModelState没有AddModelErrors方法,只有AddModelError方法,我们是后来给他加了一个扩展方法,如下
public static class ModelStateHelpers { public static void AddModelErrors(this ModelStateDictionary modelState, IEnumerable errors) { foreach (RuleViolation issue in errors) { modelState.AddModelError(issue.PropertyName, issue.ErrorMessage); } } }
在View层使用了Html.ValidationMessage方法在合适的位置输出错误描述,如果View呈现的时候ModelState里有错误的话,会自动显示相应的错误描述,代码示例如下。
p> label for="Title"> 标题:label> %= Html.TextBox("Title", null, new { style = "width:700px;" })%> %= Html.ValidationMessage("Title") %> p> p> label for="Content"> 内容:label> %= Html.TextArea("Content", null, new { style = "width:700px;height:100px;" })%> %= Html.ValidationMessage("Content")%> p>
5、LINGQ TO SQL的分页
SQLSERVER 2005有很强悍的分页函数,LINQ TO SQL对其有很好的支持,IQueryable的Skip和Take方法最终就生成分页的SQL,先写如下的一个帮助类(取自NerdDinner),这个类的属性很简单,见名知意,就不介绍了。
public class PaginatedList : List { public int PageIndex { get; private set; } public int PageSize { get; private set; } public int TotalCount { get; private set; } public int TotalPages { get; private set; } public PaginatedList(IQueryable source, int pageIndex, int pageSize) { PageIndex = pageIndex; PageSize = pageSize; TotalCount = source.Count(); TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize); this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize)); //这句会停止延迟加载,把数据加载到内存里 } public bool HasPreviousPage { get { return (PageIndex > 0); } } public bool HasNextPage { get { return (PageIndex+1 } } }
使用起来很简单,用LINQ TO SQL得到一个IQueryable后,再用其New一个PaginatedList就表示一个已分页的数据集了
var posts = from post in db.BBSPosts where post.CategoryID == id && post.ParentID == 0 orderby post.PostID descending select post; const int pageSize = 10; var pagePosts = new PaginatedList(posts, page ?? 0, pageSize); return View(pagePosts);
posts是用linq to sql生成的一个IQueryable对象,这时候SQL语句并没有执行,会延迟执行,再new一个PaginatedList的时候会对其生成的SQL语句进行修改,最后把pagePosts传递给view层用就行了,View层我们使用了强类型的View,如下
%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage>" %>
页面上要显示上一页,下一页的链接,写起来也很简单
div class="pagination"> % if (Model.HasPreviousPage) { %> %= Html.RouteLink("上一页", "Default", new { page=(Model.PageIndex-1) }) %> % } %> % if (Model.HasNextPage) { %> %= Html.RouteLink("下一页", "Default", new { page = (Model.PageIndex + 1) })%> % } %> div>
6、查看LINQ TO SQL生成的SQL语句?
有人怀疑LINQ TO SQL的性能问题,认为它生成的语句不靠谱,其实它生成的语句都是参数化查询,一般的基于主键或者索引列的查询及大多数更新操作性能应该不会比手写SQL差,如果还是不放心的话,可以把LINQ TO SQL生成的SQL打印出来,以避免性能查的语句产生。
如下代码
var db = new BBSDbContext(conn); var posts = from post in db.BBSPosts where post.CategoryID == 1 && post.ParentID == 0 orderby post.PostID descending select new {post.PostID, post.Title, post.Content}; db.Log = Response.Output; //跟踪自动生成的SQL语句 rpt1.DataSource = posts; rpt1.DataBind(); //只有真正执行使用数据的语句时,SQL查询才会执行,在这之前语句只是语句,自动延迟执行的。
会在页面上看到LINQ TO SQL生成的SQL语句
SELECT [t0].[PostID], [t0].[Title], [t0].[Content] FROM [dbo].[bbs_Post] AS [t0] WHERE ([t0].[CategoryID] = @p0) AND ([t0].[ParentID] = @p1) ORDER BY [t0].[PostID] DESC -- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1] -- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [0] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1
如果改成如下分页方式
var db = new BBSDbContext(conn); var posts = from post in db.BBSPosts where post.CategoryID == 1 && post.ParentID == 0 orderby post.PostID descending select post; db.Log = Response.Output; rpt1.DataSource = posts.Skip(1 * 5).Take(5); rpt1.DataBind();
会输出如下SQL
SELECT [t1].[CategoryID], [t1].[PostID], [t1].[ParentID], [t1].[Title], [t1].[Content], [t1].[PostUser], [t1].[PostTime] FROM ( SELECT ROW_NUMBER() OVER (ORDER BY [t0].[PostID] DESC) AS [ROW_NUMBER], [t0].[CategoryID], [t0].[PostID], [t0].[ParentID], [t0].[Title], [t0].[Content], [t0].[PostUser], [t0].[PostTime] FROM [dbo].[bbs_Post] AS [t0] WHERE ([t0].[CategoryID] = @p0) AND ([t0].[ParentID] = @p1) ) AS [t1] WHERE [t1].[ROW_NUMBER] BETWEEN @p2 + 1 AND @p2 + @p3 ORDER BY [t1].[ROW_NUMBER] -- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1] -- @p1: Input Int (Size = 0; Prec = 0; Scale = 0) [0] -- @p2: Input Int (Size = 0; Prec = 0; Scale = 0) [5] -- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [5] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1
可以看到这些查询用的都是参数化查询,不是拼SQL,而且还用了ROW_NUMBER函数,LINQ TO SQL还是比较了解SQLSERVER的。
7、设置某个Action需要身份认证?
因为基于页面的授权不能使用了,我们只好对某个Action进行授权,比如要回复帖子的话需要进行登录,那么就在reply的action上加上需要身份验证的属性修饰,如下
[AcceptVerbs(HttpVerbs.Post), Authorize] ?public ActionResult reply(BBSPost post, FormCollection coll) {
这种方式是以AOP注入方式实现的,更多的拦截器示例,或者想写自己的拦截器可以google些资料看看。
8、如何把用户提交的表单数据转成强类型
我们都知道网页上提交的数据包括Form里和QueryString,在服务端取出来都是string类型的,在asp时代,我们需要一个一个的处理参数,在ASP.NET MVC里就很方便了,比如你有一个BBSPost类,有Title和Content和CategoryId 3个属性,而表单上有两个文本框Title和Content,地址栏参数里有一个CategoryId,你可以直接在action里取到一个BBSPost类,而且属性都给你填充好了,不用你取出一个一个的string再new一个BBSPost类,再转类型赋值等一系列操作了,如下
public ActionResult reply(BBSPost post, FormCollection coll) {}
第一个参数会自动填充成强类型,第二个参数可以取出原始的表单提交的数据。如果你想了解更多的表单数据和强类型数据的绑定,细节,可以查查DefaultModelBinder是如何工作的。
9、给HTMLHelper加扩展方法。
ASP.NET MVC里的一个最佳实践就是给HTMLHelper加一些常用的扩展方法以供View层方便使用,不要到处写帮助类,比如在显示帖子的时候要格式化帖子成HTML格式,我们写了如下的扩展方法
public static class HtmlHelperExtension { public static string Text2Html(this HtmlHelper helper, string input) { input = input.Replace(" ", " "); input = input.Replace("\r\n", " "); input = input.Replace("\t", " "); return input; } }
在view上先引用扩展方法所在的命名空间
%@ Import Namespace="SimpleBBS.Helpers" %>
然后扩展方法就能使用了,如下
%= Html.Text2Html(Html.Encode(item.Content)) %>
10、如何定位脚本和CSS的位置
如果我们目录级别特别多,把脚本,样式表等放在一个固定的目录后,在特定的子目录访问这些资源路径可能不一致,在WebForm的时候只有服务端控件才能使用~语法,无论是部署在站点根目录还是虚拟目录,~都能表示应用的根目录,在ASP.NET MVC里我们可以用Url.Content来使用~,如下
script src="~/Scripts/jquery-1.3.2.min.js")%>" type="text/javascript">script>