Please, share your ideas which could serve as best practices or guidelines for creating ASP.NET MVC web applications. These ideas and/or coding samples should be relevant to ASP.NET MVC application creation itself and not to TDD or similar practices.
Here are a few of my tips. And I'd like to hear yours.
public static class UrlHelperExtensions
{
// for regular pages:
public static string Home(this UrlHelper helper)
{
return helper.Content("~/");
}
public static string SignUp(this UrlHelper helper)
{
return helper.RouteUrl("SignUp");
}
// for media and css files:
public static string Image(this UrlHelper helper, string fileName)
{
return helper.Content("~/Content/Images/{0}".
FormatWith(fileName));
}
public static string Style(this UrlHelper helper, string fileName)
{
return helper.Content("~/Content/CSS/{0}".
FormatWith(fileName));
}
public static string NoAvatar(this UrlHelper helper)
{
return Image(helper, "NoAvatar.png");
}
}
If you're doing lots of things in Application_Start, it's better to encapsulate that logic using bootstrapper design pattern.
AcceptVerbs
(and CacheControl
for some actions) attributes[HttpPost, OutputCache(CacheProfile = "Products")]
public ActionResult Products(string category)
{
// implementation goes here
}
This will make your application loosely coupled and more testable.
Avoid passing data to views via ViewData, create custom models instead for your data and typed views.
Here is an example:
public Func<ActionResult> Foo(int id) {
AsyncManager.RegisterTask(
callback => BeginGetUserByID(id, callback, null),
a => {
ViewData["User"] = EndGetUserByID(a);
});
AsyncManager.RegisterTask(
callback => BeginGetTotalUsersOnline(callback, null),
a => {
ViewData["OnlineUsersCount"] = EndGetTotalUsersOnline(a);
});
return View;
}
What is happening here is that the MVC system executes both BeginGetUserByID and BeginGetTotalUsersOnline in parallel and when both complete, it will then process the View (display the web page). Simple, clean, efficient.
Don't forget to set [AcceptVerbs(HttpVerbs.Post)], [AntiForgeryToken] and [Authorize] attributes for your submit action methods. Example:
[Authorize("Clients"), HttpPost, ValidateAntiForgeryToken]
public ActionResult SendMoney(int accountID, int amount) {
// your code here
}
And also put this into your form view:
<%= Html.AntiForgeryToken() %>
Consider using this approach: http://maproutes.codeplex.com/
[Url("store/{?category}")]
public ActionResult Products(string category = null)
{
return View();
}
Use the Post-Redirect-Get [1] pattern when designing Controller actions.
To apply this pattern to MVC actions:
You have a particular view (e.g. Search).
[HttpGet]
public ViewResult Search()
{
return View("Search");
}
The user will enter some data into the rendered form, and submit it. Give the 'Post' action the same name. If no validation is required, or validation is successful, then redirect to another action which will be responsible for displaying the result.
If validation fails, return the View with the invalid model.
[HttpPost]
public ActionResult Search(string searchTerm)
{
//if validation fails
if (ValidateSearchTerm(searchTerm)==false)
{
return View("Search", searchTerm);
}
//if validation was successful, then redirect
return RedirectToAction("SearchResults");
}
The 'Get' portion of the Post-Redirect-Get comes from the fact the when a RedirectToAction result is returned, a HTTP redirect is returned to the client-browser, which will then issue a 'get' for the next action.
[HttpGet]
public ViewResult SearchResults()
{
return View("SearchResults");
}
Using this pattern brings at least following benefits:
Create Helpers to generate URLs for the stylesheets, javascript files and any other resources (via Storefront). This is very important since you might end up changing the structure of your resources directories a lot.
public static class UrlHelperExtensions
{
public static string CssResource(this UrlHelper helper, string filename)
{
return helper.Content(string.Format("~/Contents/CSS/{0}", filename));
}
public static string ImageResource(this UrlHelper helper, string filename)
{
return helper.Content(string.Format("~/Contents/Images/{0}", filename));
}
}
Hope that helps.
Checkout Kazi Manzur Rashid's Blog post on this: http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx
The 2nd part too: http://weblogs.asp.net/rashid/archive/2009/04/03/asp-net-mvc-best-practices-part-2.aspx
Karl Seguin has some entension methods [1] for cache busting.
[1] http://codebetter.com/blogs/karlseguin/archive/2010/01/11/asp-net-performance-part-3-cache-busting.aspxGood thoughts on both architecture and the details:
Jimmy Bogard: How we do MVC
http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/04/24/how-we-do-mvc.aspx [1]
Jeremy D. Miller: Our “Opinions” on the ASP.NET MVC (Introducing the Thunderdome Principle)
[1] http://www.lostechies.com/blogs/jimmy%5Fbogard/archive/2009/04/24/how-we-do-mvc.aspxDon't bother with extending UrlHelper, just use Html.BuildUrlFromExpression.
use ValueInjecter [1] for mapping ViewModels to Entities and back (you could also map viewdata to entities if you need that or request, or formcollection)
[1] http://valueinjecter.codeplex.com/This is the best place you can start learning ASP.NET MVC.
Here's a simple one:
Do not reference the models namespace from any of the views. I've seen in numerous examples where the controller fetches a list of objects from the model and this is passed directly into the view data rather than it being transformed by the controller into something appropriate to that view. This creates a direct dependency between the model structure and the view.
So in short, avoid code like this in your markup:
<%@ Import Namespace="MyProject.Models" %>
Make folder "App_Code", and add eg. Content.cshtml there. make methods for resources there like:
@helper Script(string scriptName, UrlHelper url)
{
<script src="@scriptName" type="text/javascript"></script>
}
@helper Css(string css, UrlHelper url)
{
<link href="@css" rel="stylesheet" type="text/css" />
}
so You can reference script/css in page like this:
@Content.Css("/Content/style.css", Url)
T4MVC is very useful, especially for larger projects because you can have strongly typed views, actions...
@Content.Css(Links.Content.style_css, Url)
@Html.ActionLink("Home", MVC.Home.Index())
You can then unload your web project, then edit project.csproj, and set MvcBuildView to true, then your views will be compiled before build so errors will be shown immediately.
I've always had problems with javascripts when deploying, virtual directory gives me another level on app, so my referenced actions in scripts didn't work. So, in my layout.cshtml I made this script:
@Html.Hidden("HiddenCurrentUrl", Url.Action("DummyAction"))
<script type="text/javascript">
var baseUrl = "";
baseUrl = $("#HiddenCurrentUrl").val();
baseUrl = baseUrl.substring(0, baseUrl.indexOf("DummyAction"));
</script>
//and in my scripts i call actions like this:
$.post(**baseUrl + "ActionName"**, data, function success)
//baseUrl is on your controller, so if you want level below, just type ../
and now it is simple to have style on menu links(or anything else) for current controller. Just put this in _layout.cshtml
$(document).ready(function () {
$('#menu li:has(a[href="' + baseUrl.substring(0, baseUrl.length - 1) + '"]) a').attr('class', 'current');
});
In your Views\Shared make folder EditorTemplates, and there you can make cshtml file, that will be template for some data type. Example: String.cshtml
@model System.String
//Template for string input, label + textbox + validation
<div class="editor-label">
@Html.LabelFor(model => model)
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model, ViewData)
</div>
<div>
@Html.ValidationMessageFor(model => model)
</div>
and in View you just call
@Html.EditorFor(model => model.Text)
and you get textbox with label and validation with less code.
//I'll put more when i get time.
How create lowercase routes in ASP.NET MVC. [1]
But I want to make an addendum to the.
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData path = base.GetVirtualPath(requestContext, values);
if (path != null && path.VirtualPath.Length>0)
{
var strings = path.VirtualPath.Split('/');
path.VirtualPath = "";
for (int i = 0; i < strings.Length; i++)
{
strings[i] = Char.ToLowerInvariant(strings[i][0]) + strings[i].Substring(1);
}
path.VirtualPath = String.Join("/", strings, 0, strings.Length);
}
return path;
}
Example work this code. If you have path "Home/CreateNews" code makes it more readable your url path "home/createNews"
[1] http://coderjournal.com/2008/03/force-mvc-route-url-lowercase/Redirect with http code [1] 301.
public class PermanentlyRedirectResult : ActionResult
{
public string Url { get; set; }
public PermanentlyRedirectResult(string url)
{
if (string.IsNullOrEmpty(url))
{
throw new ArgumentException("Url is null or empty", "url");
}
this.Url = url;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.HttpContext.Response.StatusCode = 301;
context.HttpContext.Response.RedirectLocation = Url;
context.HttpContext.Response.End();
}
}
use: new PermanentlyRedirectResult(Url.Action("Index"));