Vous êtes sur la page 1sur 22

12 ASP.

NET MVC Best Practices

Controller’s best practices

1 – Delete the AccountController

You will never use it and it’s a super-bad practice to keep demo code in your applications.

2 – Isolate Controllers from the outside World

Dependencies on the HttpContext, on data access classes, configuration, logging, clock, etc…
make the application difficult (if not impossible) to test, to evolve and modify.

3 – Use an IoC Container

To make it easy to adhere to Best Practice #2, use an IoC Container to manage all that external
dependencies. I use Ninject v2, but there are many around, and it’s easy to build your own if
needed.

4 – Say NO to “magic strings”

Never use ViewData[“key”], but always create a ViewModel per each View, and use strongly-
typed views ViewPage<ViewModel>.

Magic strings are evil because they will never tell you whether your view is failing due to a
misspelling error, while using a strongly-typed model you will get a compile-time error when
there is a problem. And as bonus you get Intellisense.

5 – Build your own “personal conventions”

Use ASP.NET MVC as a base for your (or your company’s) reference architecture. Enforce your
own conventions having controllers and maybe views inherit from your own base classes rather
then the default ones.

6 – Pay attention to the Verbs

1
Even without going REST (just RESTful) use the best Http Verb for each action. Adopt the PRG
Pattern (Post-Redirect-Get): show data with GET, modify data with POST.

Model’s Best Practices

7 – DomainModel != ViewModel

The DomainModel represents the domain, while the ViewModel is designed around the needs of
the View, and these two worlds might be (and usually are) different. Furthermore the
DomainModel is data plus behaviours, is hierarchical and is made of complex types, while the
ViewModel is just a DTO, flat, and made of strings. To remove the tedious and error-prone
object-mapping code, you can use AutoMapper. For a nice overview of the various options I
recommend you read: ASP.NET MVC View Model Patterns.

8 – Use ActionFilters for “shared” data

This is my solution for the componentization story of ASP.NET MVC, and might need a future
post of its own. You don’t want your controllers to retrieve data that is shared among different
views. My approach is to use the Action Filters to retrieve the data that needs to be shared across
many views, and use partial view to display them.
View’s Best Practices

9 – Do NEVER user code-behind

NEVER

10 – Write HTML each time you can

I have the option that web developers have to be comfortable writing HTML (and CSS and
JavaScript). So they should never use the HtmlHelpers whose only reason of living is hiding the
HTML away (like Html.Submit or Html.Button). Again, this is something that might become a
future post.

11 - If there is an if, write an HtmlHelper

Views must be dumb (and Controllers skinny and Models fat). If you find yourself writing an
“if”, then consider writing an HtmlHelper to hide the conditional statement.

12 – Choose your view engine carefully

2
The default view engine is the WebFormViewEngine, but IMHO it’s NOT the best one. I prefer
to use the Spark ViewEngine, since it seems to me like it’s more suited for an MVC view. What
I like about it is that the HTML “dominates the flow and that code should fit seamlessly” and the
foreach loops and if statements are defined with “HTML attributes”.

ASP.NET MVC Best Practices


In this post, I will share some of the best practices/guideline in developing ASP.NET MVC
applications which I have learned in the hard way. I will not tell you to use DI or Unit Test
instead I will assume you are already doing it and you prefer craftsmanship over anything.

1. Create Extension methods of UrlHelper to generate your url from Route

Avoid passing the controller, action or route name as string, create extension methods of
UrlHelper which encapsulates it, for example:

01.public static class UrlHelperExtension


02.{
03.    public static string Home(this UrlHelper helper)
04.    {
05.        return helper.Content("~/");
06.    }
07. 
08.    public static string SignUp(this UrlHelper helper)
09.    {
10.        return helper.RouteUrl("Signup");
11.    }
12. 
13.    public static string Dashboard(this UrlHelper helper)
14.    {
15.        return Dashboard(helper, StoryListTab.Unread);
16.    }
17. 
18.    public static string Dashboard(this UrlHelper helper, StoryListTab tab)
19.    {
20.        return Dashboard(helper, tab, OrderBy.CreatedAtDescending, 1);
21.    }
22. 
23.    public static string Dashboard(this UrlHelper helper, StoryListTab tab,
OrderBy orderBy, int page)
24.    {
25.        return helper.RouteUrl("Dashboard", new { tab = tab.ToString(),
orderBy = orderBy.ToString(), page });
26.    }
27. 
28.    public static string Update(this UrlHelper helper)
29.    {
30.        return helper.RouteUrl("Update");

3
31.    }
32. 
33.    public static string Submit(this UrlHelper helper)
34.    {
35.        return helper.RouteUrl("Submit");
36.    }
37.}

Now, You can use the following in your view:

1.<a href="<%= Url.Dashboard() %>">Dashboard</a>


2.<a href="<%= Url.Profile() %>">Profile</a>

Instead of:

1.<%= Html.ActionLink("Dashboard", "Dashboard", "Story") %>


2.<a href="<%= Url.RouteUrl("Profile")%>">Profile</a>

And in Controller I can use:

1.return Redirect(Url.Dashboard(StoryListTab.Favorite,
OrderBy.CreatedAtAscending, 1))

Instead of:

1.return RedirectToAction("Dashboard", "Story", new { tab =


StoryListTab.Favorite, orderBy = OrderBy.CreatedAtAscending, page = 1 });

Of course you can use the strongly typed version which takes the controller, method and the
parameters of the future assembly or create your own to avoid the future refactoring pain, but
remember it is not officially supported and might change in the future. You can also use the
above with the strongly typed version, certainly “adding another layer of indirection” (Scott Ha
favorite quote) has some benefits. Another benefit when writing Unit Test you will only deal
with the RedirectResult rather than RediretResult and
RedirectToRouteResult.

2. Create Extension Method of UrlHelper to map your JavaScript, Stylesheet


and Image Folder

By default ASP.NET MVC creates Content, Scripts folder for these things, which I do not
like, Instead I like the following folder structure so that I can only set static file caching  on the
Assets folder in IIS instead of going to multiple folders:

Assets
+images
+scripts
+stylesheets

4
No matter what the structure is, create some extension method of UrlHelper to map these
folders, so that you can easily refer it in your view and later on if you need to change the
structure, you do not have to do massive find/replace. I would also recommend to create
extension methods for those assets which are often refereed in your views. For example:

01.public static string Image(this UrlHelper helper, string fileName)


02.{
03.    return helper.Content("~/assets/images/{0}".FormatWith(fileName));
04.}
05. 
06.public static string Stylesheet(this UrlHelper helper, string fileName)
07.{
08.    return helper.Content("~/assets/stylesheets/{0}".FormatWith(fileName));
09.}
10. 
11.public static string NoIcon(this UrlHelper helper)
12.{
13.    return Image(helper, "noIcon.png");
14.}

And when referring the assets you can use:

1.<link href="<%= Url.Stylesheet("site.css")%>" rel="stylesheet"


type="text/css"/>

Instead of the default:

1.<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />

3. Use Bootstrapper in Global.asax

I have already covered it in the past, basically if you are doing a lot things in
Application_Start of global.asax e.g. Registering Routes, Registering Controller
Factory, Model Binders, View Engine, starting application specific background services, create
individual task for specific part, then do use Bootstrapper to execute those. This make your
code lot more clean and testable. This will be also helpful when developing a portal kind of app
in asp.net mvc where each module can have some startup initialization without affecting others.
But if you are developing a small app where the above things will never be an issue you can
surly go ahead with the default global.asax.

4. Do not make any hard dependency on the DI Container, use Common Service
Locator

Do not clutter your code with any specific DI reference, instead use the Common Service
Locator, it is an abstraction over the underlying DI and it has the support for all of the popular DI
containers, so that you can replace the underlying DI without modifying your application code as
each DI Container has some unique features over the others. Tim Barcz recently wrote a
excellent post on this subject, how much obsessed we are with our favorite DI Container but I am

5
not sure why he did not mention about it. The Common Service Locator has the support for most
of the regular scenarios, but for specific case for example injecting dependency in already
instantiated object which as per my knowledge StructureMap, Ninject and Unity supports you
can call the static ServiceLocator.Current.GetInstance in the constructor instead of
calling the underlying DI. And for those who do know, Common Service Locator is a joint effort
of the DI Containers creators initiated by Jeremy D Miller.

Creating Controller Factory with Common Service Locator is very easy:

1.public class CommonServiceLocatorControllerFactory : DefaultControllerFactory


2.{
3.    protected override IController GetControllerInstance(Type controllerType)
4.    {
5.        return (controllerType == null) ?
base.GetControllerInstance(controllerType) :
ServiceLocator.Current.GetInstance(controllerType) as IController;
6.    }
7.}

I hope the MVCContrib guys will follow the same instead of creating separate Controller Factory
for each Container.

5. Decorate your Action Methods with Proper AcceptVerbs Attribute

ASP.NET MVC is much more vulnerable comparing to Web Forms. Make sure the action
methods that modifies the data only accepts HttpVerbs.Post. If security is too much
concern use the ValidateAntiForgeryToken or you can use Captcha. Derik Whittaker
has an excellent post as well as a screen cast on how to integrate reCaptcha with ASP.NET MVC
application, which I highly recommend. (Side Note: Do not miss a single episode of
DimeCasts.net, I have learnt a lot form those short screen casts). My rule of thumb is use
HttpVerbs.Post for all data modification actions and HttpVerbs.Get for data reading
operations.

6. Decorate your most frequent Action Methods with OutputCache Attribute

Use OutputCache attribute when you are returning the less frequent updated data, prime
candidate may be your home page, feed etc etc. You can use it for both Html and Json data types.
When using it, only specify the Cache Profile name, do not not specify any other thing, use the
web.config output cache section to fine tune it. For example:

1.[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard")]


2.public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy
orderBy, int? page)
3.{
4.}

And in web.config:

6
01.<system.web>
02.        <caching>
03.            <outputCacheSettings>
04.                <outputCacheProfiles>
05.                    <clear/>
06.                    <!-- 15 Seconds -->
07.                    <add
08.                        name="Dashboard"
09.                        duration="15"
10.                        varyByParam="*"
11.                        location="Client"
12.                    />
13.                </outputCacheProfiles>
14.            </outputCacheSettings>
15.        </caching>
16.</system.web>

7. Keep your controller free from HttpContext and its tail

Make sure your controller does not have to refer the HttpContext and its tail. it will make
your life easier when unit testing your Controller. If you need to access anything from
HttpContext like User, QueryString, Cookie etc use custom action filter or create some
interface and wrapper and pass it in the constructor. For example, for the following Route:

1._routes.MapRoute("Dashboard", "Dashboard/{tab}/{orderBy}/{page}", new


{ controller = "Story", action = "Dashboard", tab =
StoryListTab.Unread.ToString(), orderBy =
OrderBy.CreatedAtDescending.ToString(), page = 1 });

But the controller action methods is declared as:

1.[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"),


UserNameFilter]
2.public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy
orderBy, int? page)
3.{
4.}

The UserNameFilter is responsible for passing the UserName:

01.public class UserNameFilter : ActionFilterAttribute


02.{
03.    public override void OnActionExecuting(ActionExecutingContext
filterContext)
04.    {
05.        const string Key = "userName";
06. 
07.        if (filterContext.ActionParameters.ContainsKey(Key))
08.        {
09.            if (filterContext.HttpContext.User.Identity.IsAuthenticated)
10.            {
11.                filterContext.ActionParameters[Key] =
filterContext.HttpContext.User.Identity.Name;

7
12.            }
13.        }
14. 
15.        base.OnActionExecuting(filterContext);
16.    }
17.}

[Update: Make sure you have decorate either the Action or the Controller with
Authorize attribute, check the comments]

8. Use Action Filter to Convert to compatible Action Methods parameters

Use Action Filter to convert incoming values to your controller action method parameters, again
consider the Dashboard method, we are accepting tab and orderBy as Enum.

1.[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"),


StoryListFilter]
2.public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy
orderBy, int? page)
3.{
4.}

The StoryListFilter will be responsible to convert it to proper data type from route
values/querystrings.

01.public class StoryListFilter : ActionFilterAttribute


02.{
03.    public override void OnActionExecuting(ActionExecutingContext
filterContext)
04.    {
05.        const string TabKey = "tab";
06.        const string OrderByKey = "orderBy";
07. 
08.        NameValueCollection queryString =
filterContext.HttpContext.Request.QueryString;
09. 
10.        StoryListTab tab =  string.IsNullOrEmpty(queryString[TabKey]) ?
11.                            filterContext.RouteData.Values[TabKey].ToString
().ToEnum(StoryListTab.Unread) :
12.                            queryString[TabKey].ToEnum(StoryListTab.Unread)
;
13. 
14.        filterContext.ActionParameters[TabKey] = tab;
15. 
16.        OrderBy orderBy =   string.IsNullOrEmpty(queryString[OrderByKey]) ?
17.                            filterContext.RouteData.Values[OrderByKey].ToSt
ring().ToEnum(OrderBy.CreatedAtDescending) :
18.                            queryString[OrderByKey].ToEnum(OrderBy.CreatedA
tDescending);
19. 
20.        filterContext.ActionParameters[OrderByKey] = orderBy;
21. 

8
22.        base.OnActionExecuting(filterContext);
23.    }
24.}

You can also use the custom Model Binder for the same purpose. In that case you will have to
create two custom Model Binders for each Enum instead of one action filter. Another issue with
the Model Binder is once it is registered for a type it will always come into action, but action
filter can be selectively applied.

9. Action Filter Location

If you need the same action filter to all of your controller action methods,  put it in the controller
rather than each action method. If you want to apply the same action filter to all of your
controller create a base controller and inherit from that base controller, for example the story
controller should be only used when user is logged in and we need to pass the current user name
in its methods, also the StoryController should compress the data when returning:

01.[Authorize, UserNameFilter]
02.public class StoryController : BaseController
03.{
04.}
05. 
06.[CompressFilter]
07.public class BaseController : Controller
08.{
09.}

But if the inheritance hierarchy is going more than 2 /3 level deep, find another way to apply the
filters. The latest Oxite code has some excellent technique applying filters dynamically which I
highly recommend to check.

10. Use UpdateModel Carefully

I do not want to repeat what Justin Etheredge has mentioned in his post, be careful and do not
fall into that trap.

11.Controller will not contain any Domain logic

Controller should be only responsible for:

 Validating Input
 Calling Model to prepare the view
 Return the view or redirect to another action

If you are doing any other thing you are doing it in a wrong place, it is rather the Model
responsibility which you are doing in Controller. If you follow this rule your action method will

9
not be more than 20 – 25 lines of code. Ian Cooper has an excellent post Skinny Controller Fat
Model, do read it.

12. Avoid ViewData, use ViewData.Model

Depending upon the dictionary key will not only make your code hard to refactor, also you will
have to write the casting code in your view. It is completely okay even you end up per class for
each action method of your controller.  If you think, creating these kind of classes is a tedious
job, you can use the ViewDataExtensions of the MVCContrib project, it has some nice extension
for returning strongly typed objects, though you still have to depend upon the string key if you
have more than one data type in ViewData Dictionary.

13. Use PRG Pattern for Data Modification

One of the issue with this pattern is when a validation fails or any  exception occurs you have to
copy the ModelState into TempData. If you are doing it manually, please stop it, you can do
this automatically with Action Filters, like the following:

01.[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"),


StoryListFilter, ImportModelStateFromTempData]
02.public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy
orderBy, int? page)
03.{
04.    //Other Codes
05.    return View();
06.}
07. 
08.[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData]
09.public ActionResult Submit(string userName, string url)
10.{
11.    if (ValidateSubmit(url))
12.    {
13.        try
14.        {
15.            _storyService.Submit(userName, url);
16.        }
17.        catch (Exception e)
18.        {
19.            ModelState.AddModelError(ModelStateException, e);
20.        }
21.    }
22. 
23.    return Redirect(Url.Dashboard());
24.}

And the Action Filers

01.public abstract class ModelStateTempDataTransfer : ActionFilterAttribute


02.{
03.    protected static readonly string Key =
typeof(ModelStateTempDataTransfer).FullName;

10
04.}
05. 
06.public class ExportModelStateToTempData : ModelStateTempDataTransfer
07.{
08.    public override void OnActionExecuted(ActionExecutedContext
filterContext)
09.    {
10.        //Only export when ModelState is not valid
11.        if (!filterContext.Controller.ViewData.ModelState.IsValid)
12.        {
13.            //Export if we are redirecting
14.            if ((filterContext.Result is RedirectResult) ||
(filterContext.Result is RedirectToRouteResult))
15.            {
16.                filterContext.Controller.TempData[Key] =
filterContext.Controller.ViewData.ModelState;
17.            }
18.        }
19. 
20.        base.OnActionExecuted(filterContext);
21.    }
22.}
23. 
24.public class ImportModelStateFromTempData : ModelStateTempDataTransfer
25.{
26.    public override void OnActionExecuted(ActionExecutedContext
filterContext)
27.    {
28.        ModelStateDictionary modelState =
filterContext.Controller.TempData[Key] as ModelStateDictionary;
29. 
30.        if (modelState != null)
31.        {
32.            //Only Import if we are viewing
33.            if (filterContext.Result is ViewResult)
34.            {
35.                filterContext.Controller.ViewData.ModelState.Merge(modelSta
te);
36.            }
37.            else
38.            {
39.                //Otherwise remove it.
40.                filterContext.Controller.TempData.Remove(Key);
41.            }
42.        }
43. 
44.        base.OnActionExecuted(filterContext);
45.    }
46.}

The MVCContrib project also has this feature but they are doing it in a single class which I do
not like, I would like to have more control which method to export and which to import.

11
14. Create Layer Super Type for your ViewModel and Use Action Filter to
populate common parts.

Create a layer super type for your view model classes and use action filter to populate common
things into it . For example the tiny little application that I am developing I need to know the
User Name and whether the User is authenticated.

01.public class ViewModel


02.{
03.    public bool IsUserAuthenticated
04.    {
05.        get;
06.        set;
07.    }
08. 
09.    public string UserName
10.    {
11.        get;
12.        set;
13.    }
14.}

and the action filter:

01.public class ViewModelUserFilter : ActionFilterAttribute


02.{
03.    public override void OnActionExecuted(ActionExecutedContext
filterContext)
04.    {
05.        ViewModel model;
06. 
07.        if (filterContext.Controller.ViewData.Model == null)
08.        {
09.            model = new ViewModel();
10.            filterContext.Controller.ViewData.Model = model;
11.        }
12.        else
13.        {
14.            model = filterContext.Controller.ViewData.Model as ViewModel;
15.        }
16. 
17.        if (model != null)
18.        {
19.            model.IsUserAuthenticated =
filterContext.HttpContext.User.Identity.IsAuthenticated;
20. 
21.            if (model.IsUserAuthenticated)
22.            {
23.                model.UserName =
filterContext.HttpContext.User.Identity.Name;
24.            }
25.        }
26. 

12
27.        base.OnActionExecuted(filterContext);
28.    }
29.}

As you can see that it not replacing the model, if it is previously set in the controller, rather it
populates the common part if it finds it compatible. Other benefit is, the views that only depends
the layer super type you can simply return View() instead of creating the model.

That's it for today, I will post rest of the items tomorrow.

15. Routing consideration

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:

01._routes.Clear();

02. 

03.// Turns off the unnecessary file exists check

04._routes.RouteExistingFiles = true;

05. 

06.// Ignore text, html, files.

07._routes.IgnoreRoute("{file}.txt");

08._routes.IgnoreRoute("{file}.htm");

09._routes.IgnoreRoute("{file}.html");

10. 

11.// Ignore axd files such as assest, image, sitemap etc

12._routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

13. 

14.// Ignore the assets directory which contains images, js, css & html

15._routes.IgnoreRoute("assets/{*pathInfo}");

13
16. 

17.// Ignore the error directory which contains error pages

18._routes.IgnoreRoute("ErrorPages/{*pathInfo}");

19. 

20.//Exclude favicon (google toolbar request gif file as fav icon which is
weird)

21._routes.IgnoreRoute("{*favicon}", new { favicon = @"(.*/)?favicon.([iI][cC]


[oO]|[gG][iI][fF])(/.*)?" });

22. 

23.//Actual routes of my application

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" });

07. 

08._routes.MapRoute("Dashboard", "Dashboard/{tab}/{orderBy}/{page}", new


{ controller = "Story", action = "Dashboard", tab =
StoryListTab.Unread.ToString(), orderBy =
OrderBy.CreatedAtDescending.ToString(), page = 1 });

14
09._routes.MapRoute("Update", "Update", new { controller = "Story", action =
"Update" });

10._routes.MapRoute("Submit", "Submit", new { controller = "Story", action =


"Submit" });

11. 

12._routes.MapRoute("Home", "{controller}/{action}/{id}", new { controller =


"Home", action = "Index", id = string.Empty });

16. Create new ActionResult if required

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

02.{

03.    public AtomResult(string siteTitle, string feedTitle,


IEnumerable<IStory> stories)

04.    {

05.        SiteTitle = siteTitle;

06.        FeedTitle = feedTitle;

07.        Stories = stories;

08.    }

09. 

10.    public string SiteTitle

11.    {

12.        get;

13.        private set;

14.    }

15. 

16.    public string FeedTitle

15
17.    {

18.        get;

19.        private set;

20.    }

21. 

22.    public IEnumerable<IStory> Stories

23.    {

24.        get;

25.        private set;

26.    }

27. 

28.    public override void ExecuteResult(ControllerContext context)

29.    {

30.        string xml = Build(context);

31. 

32.        HttpResponseBase response = context.HttpContext.Response;

33.        response.ContentType = "application/atom+xml";

34.        response.Write(xml);

35.    }

36.}

And in Controller:

1.[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Atom")]

2.public ActionResult Shared()

3.{

4.    IEnumerable<stories> stories = GetSharedStories();

5. 

6.    return new AtomResult("My Site", "My shared stories in atom", stories);

16
7.}

17. Split your View into multiple ViewUserControl

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

03.</asp:Content>

04.<asp:Content ID="Content2" ContentPlaceHolderID="MainContent"


runat="server">

05.    <div id="heading"></div>

06.    <div class="columns">

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">

11.                    <div id="storyList">

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);%>

19.                        </form>

20.                    </div>

17
21.                </div>

22.            </div>

23.            <%Html.RenderPartial("Submit", new StorySubmitViewModel());%>

24.        </div>

25.        <div id="sideBar" class="column"></div>

26.    </div>

27.</asp:Content>

18. HtmlHelper extension

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

02.{

03.    private readonly HtmlHelper _htmlHelper;

04. 

05.    public DashboardHtmlHelper(HtmlHelper htmlHelper)

06.    {

07.        _htmlHelper = htmlHelper;

08.    }

09. 

10.    public string DoThis()

11.    {

12.        //Your Code

13.    }

18
14. 

15.    public string DoThat()

16.    {

17.        //Your Code

18.    }

19.}

20. 

21.public static class HtmlHelperExtension

22.{

23.    public static DashboardHtmlHelper Dashboard(this HtmlHelper htmlHelper)

24.    {

25.        return new DashboardHtmlHelper(htmlHelper);

26.    }

27.}

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.

19. Encode

Whatever you receive from the User always use Html.Encode(“User Input”) for
textNode and Html.AttributeEncode(“User Input”) for html element attribute.

20. Do not put your JavaScript codes in your View

19
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>

2.        <script type="text/javascript">

3.            $(document).ready(function(){

4.                Story.init('<%= Model.UrlFormat %>', '<%= Url.NoIcon() %>',


<%= Model.PageCount %>, <%= Model.StoryPerPage %>, <%= Model.CurrentPage %>,
'<%= Model.SelectedTab %>', '<%= Model.SelectedOrderBy %>');

5.            });

6.        </script>

7.    </div>

8.</asp:Content>

And JavaScript:

01.var Story =

02.{

03.    _urlFormat: '',

04.    _noIconUrl: '',

05.    _pageCount: 0,

06.    _storyPerPage: 0,

07.    _currentPage: 0,

08.    _currentTab: '',

09.    _currentOrderBy: '',

10. 

11.    init: function(urlFormat, noIconUrl, pageCount, storyPerPage,


currentPage, currentTab, currentOrderBy)

12.    {

20
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;

20. 

21.        //More Codes

22.    }

23.}

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.

21. Use jQuery and jQuery UI

Use jQuery and jQuery UI, nothing can beats it and use Google CDN to load these libraries.

1.<link type="text/css"
href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/themes/{YOUR
Prefered Theme}/jquery-ui.css" rel="stylesheet"/>

2.<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script
>

3.<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-
ui.js"></script>

Or much better:

1.<link type="text/css"
href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/themes/{YOUR
Prefered Theme}/jquery-ui.css" rel="stylesheet"/>

2.<script type="text/javascript" src="http://www.google.com/jsapi"></script>

21
3.<script type="text/javascript">

4.    google.load("jquery", "1.3.2");

5.    google.load("jqueryui", "1.7.1");

6.</script>

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.

22

Vous aimerez peut-être aussi