[转载]ASP.NET MVC Best Practices (Part 2) – Kazi Manzur Rashid’s Blog.
ASP.NET MVC Best Practices (Part 2)
This is the second part of the series and may be the last, till I find some thing new. My plan was to start with routing, controller, controller to model, controller to view and last of all the view, but some how I missed one important thing in routing, so I will begin with that in this post.
If you are developing a pure ASP.NET MVC application, turn off existing file check of routes, it will eliminate unnecessary file system check. Once you do it there are few more things you have to consider. Remember when you are hosting application in IIS7 integrated mode, your ASP.NET application will intercept all kind of request, no matter what the file extension is. So you have to add few more things in the ignore list which your ASP.NET MVC application will not process. This might include static files like html, htm, text file specially robots.txt, favicon.ico, script, image and css etc. This is one of the reason, why I do not like the default directory structure (Contents and Scripts folder) mentioned in my #2 in the previous post. The following is somewhat my standard template for defining routes when hosting in IIS7:
04 |
_routes.RouteExistingFiles = true ; |
07 |
_routes.IgnoreRoute( "{file}.txt" ); |
08 |
_routes.IgnoreRoute( "{file}.htm" ); |
09 |
_routes.IgnoreRoute( "{file}.html" ); |
12 |
_routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" ); |
15 |
_routes.IgnoreRoute( "assets/{*pathInfo}" ); |
18 |
_routes.IgnoreRoute( "ErrorPages/{*pathInfo}" ); |
21 |
_routes.IgnoreRoute( "{*favicon}" , new { favicon = @"(.*/)?favicon.([iI][cC][oO]|[gG][iI][fF])(/.*)?" }); |
Next, few of my personal preference rather than guideline, by default ASP.NET MVC generates url like {controller}/{action} which is okay when you are developing multi-module application, for a small application, I usually prefer the action name without the controller name, so instead of www.yourdomain.com/Story/Dashboard, www.yourdomain.com/Membership/SignIn it will generate www.yourdomain.com/Dashboard, www.yourdomain.com/Signin. So I add few more routes:
01 |
_routes.MapRoute( "SignUp" , "SignUp" , new { controller = "Membership" , action = "SignUp" }); |
02 |
_routes.MapRoute( "SignIn" , "SignIn" , new { controller = "Membership" , action = "SignIn" }); |
03 |
_routes.MapRoute( "ForgotPassword" , "ForgotPassword" , new { controller = "Membership" , action = "ForgotPassword" }); |
04 |
_routes.MapRoute( "SignOut" , "SignOut" , new { controller = "Membership" , action = "SignOut" }); |
05 |
_routes.MapRoute( "Profile" , "Profile" , new { controller = "Membership" , action = "Profile" }); |
06 |
_routes.MapRoute( "ChangePassword" , "ChangePassword" , new { controller = "Membership" , action = "ChangePassword" }); |
08 |
_routes.MapRoute( "Dashboard" , "Dashboard/{tab}/{orderBy}/{page}" , new { controller = "Story" , action = "Dashboard" , tab = StoryListTab.Unread.ToString(), orderBy = OrderBy.CreatedAtDescending.ToString(), page = 1 }); |
09 |
_routes.MapRoute( "Update" , "Update" , new { controller = "Story" , action = "Update" }); |
10 |
_routes.MapRoute( "Submit" , "Submit" , new { controller = "Story" , action = "Submit" }); |
12 |
_routes.MapRoute( "Home" , "{controller}/{action}/{id}" , new { controller = "Home" , action = "Index" , id = string .Empty }); |
ASP.NET MVC has quite a number of ActionResult for different purposes, but still we might need new ActionResult. For example xml, rss, atom etc. In those cases, I would suggest instead of using the generic ContentResult, create new ActionResult. The MVCContrib has an XmlResult which you can use for returning xml but no support for feed. Yes it is obviously tricky to convert an unknown object into rss/atom, in those cases you can create model specific ActionResult. For example:
01 |
public class AtomResult : ActionResult |
03 |
public AtomResult( string siteTitle, string feedTitle, IEnumerable<IStory> stories) |
05 |
SiteTitle = siteTitle; |
06 |
FeedTitle = feedTitle; |
10 |
public string SiteTitle |
16 |
public string FeedTitle |
22 |
public IEnumerable<IStory> Stories |
28 |
public override void ExecuteResult(ControllerContext context) |
30 |
string xml = Build(context); |
32 |
HttpResponseBase response = context.HttpContext.Response; |
33 |
response.ContentType = "application/atom+xml" ; |
And in Controller:
1 |
[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Atom" )] |
2 |
public ActionResult Shared() |
4 |
IEnumerable<stories> stories = GetSharedStories(); |
6 |
return new AtomResult( "My Site" , "My shared stories in atom" , stories); |
Split your view into multiple ViewUserControl when it is getting bigger, it really does not matter whether the same UserControl is reused in another page, it makes the very view much more readable. Consider the following view:
01 |
< asp:Content ID = "Content1" ContentPlaceHolderID = "TitleContent" runat = "server" > |
02 |
My Secret App : Dashboard |
04 |
< asp:Content ID = "Content2" ContentPlaceHolderID = "MainContent" runat = "server" > |
05 |
< div id = "heading" ></ div > |
07 |
< div id = "main" class = "column" > |
08 |
< div id = "storyListTabs" class = "ui-tabs ui-widget ui-widget-content ui-corner-all" > |
09 |
<% Html.RenderPartial("TabHeader", Model);%> |
10 |
< div id = "tabContent" class = "ui-tabs-panel ui-widget-content ui-corner-bottom" > |
12 |
<% Html.RenderPartial("SortBar", Model);%> |
13 |
< div class = "clear" ></ div > |
14 |
<% Html.RenderPartial("NoLinkMessage", Model);%> |
15 |
< form id = "update" action="<%= Url.Update()%>" method="post"> |
16 |
<% Html.RenderPartial("List", Model);%> |
17 |
<% Html.RenderPartial("ActionBar", Model);%> |
18 |
<% Html.RenderPartial("Pager", Model);%> |
23 |
<%Html.RenderPartial("Submit", new StorySubmitViewModel());%> |
25 |
< div id = "sideBar" class = "column" ></ div > |
First, read this great post of Rob Conery and I completely agree, you should create helper methods for each condition and I would also suggest to create helper methods for reusable UI elements like the ASP.NET MVC team did, but I have a different suggestion about the placing of these methods that we are currently practicing.
Application Developer
Do only create extension methods of HtmlHelper if you are using it in more than one view. Otherwise create a view specific helper and create an extension method of the HtmlHelper which returns the view specific helper. for example:
01 |
public class DashboardHtmlHelper |
03 |
private readonly HtmlHelper _htmlHelper; |
05 |
public DashboardHtmlHelper(HtmlHelper htmlHelper) |
07 |
_htmlHelper = htmlHelper; |
10 |
public string DoThis() |
15 |
public string DoThat() |
21 |
public static class HtmlHelperExtension |
23 |
public static DashboardHtmlHelper Dashboard( this HtmlHelper htmlHelper) |
25 |
return new DashboardHtmlHelper(htmlHelper); |
Now, you will able to use it in the view like:
1 |
<%= Html.Dashboard().DoThis() %> |
UI Component Developer
If you are developing some family of UI components that will be reusable across different ASP.NET MVC application, create a helper with your component family name like the above, if you are a commercial vendor maybe your company name then add those methods in that helper. Otherwise there is a very good chance of method name collision.
The same is also applied if you are extending the IViewDataContainer like the MVCContrib.org.
Whatever you receive from the User always use Html.Encode(“User Input”) for textNode and Html.AttributeEncode(“User Input”) for html element attribute.
Do not intermix your JavaScript with the html, create separate js files and put your java script in those files. Some time, you might need to pass your view data in your java script codes, in those cases only put your initialization in the view. For example, consider you are developing Web 2.0 style app where you want to pass ajax method url, and some other model data in the java script codes, in those cases you can use some thing like the following:
The View:
1 |
< div id = "sideBar" class = "column" ></ div > |
3 |
$(document).ready(function(){ |
4 |
Story.init('<%= Model.UrlFormat %>', '<%= Url.NoIcon() %>', <%= Model.PageCount %>, <%= Model.StoryPerPage %>, <%= Model.CurrentPage %>, '<%= Model.SelectedTab %>', '<%= Model.SelectedOrderBy %>'); |
And JavaScript:
11 |
init: function (urlFormat, noIconUrl, pageCount, storyPerPage, currentPage, currentTab, currentOrderBy) |
13 |
Story._urlFormat = urlFormat; |
14 |
Story._noIconUrl = noIconUrl; |
15 |
Story._pageCount = pageCount; |
16 |
Story._storyPerPage = storyPerPage; |
17 |
Story._currentPage = currentPage; |
18 |
Story._currentTab = currentTab; |
19 |
Story._currentOrderBy = currentOrderBy; |
And those who are not familiar with the above JavaScript code, it is an example of creating static class in JavaScript. And one more thing before I forgot to mention, never hard code your ajax method url in your javascript file, no matter Rob Conery or Phil Haack does it in their demo. It is simply a bad practice and demolish the elegance of ASP.NET Routing.
Use JQuery and jQuery UI, nothing can beats it and use Google CDN to load these libraries.
Or much better:
3 |
< script type = "text/javascript" > |
4 |
google.load("jQuery", "1.3.2"); |
5 |
google.load("jQueryui", "1.7.1"); |
And that’s it for the time being.
At the end, I just want congratulate the ASP.NET MVC Team for developing such an excellent framework and specially the way they took the feedback from the community and I look forward to develop few more killer apps in this framework.