ASP.NET MVC from Basics to Tips and Tricks

by michael 11/16/2009 5:41:00 PM

I had a great time speaking at the Fort Smith .NET User Group last week.  ASP.NET MVC is a subject that I am very passionate about.  I recently had the pleasure of developing an e-commerce website for Wolff Wire – Office Organizers using this technology.  Since then I have been using it anytime I can.  The development feels so much cleaner, and the code is more organized than in ASP.NET WebForms.  In addition the HTML output is not cluttered with ViewState and ClientIDs.

Unfortunately I wasn’t able to get quite as far as I wanted with the presentation, so I figured that I would go ahead and hit the highlights of my presentation in this blog post.

I am not an expert, so if anyone has better ways of doing things please let me know.

The Basics

First off MVC stands for Model-View-Controller.  Below some of the basic components are listed.

  • Model = Data / State
  • View = Responsible only for rendering the HTML output (.aspx page)
  • Controller = Presentation Logic (class with action methods)
    • HTTP operations are routed here
    • Responsible for selecting the appropriate View
    • Provides the View with the proper Model
  • Routing = URL Processing Engine
    • Determines based on the URL what Action Methods to call on the Controller
    • Default URL Structure = Controller Prefix/Action/ID = ex. Product/Detail/2
    • Very Customizable
  • Html Helpers = Methods that generate html (used in View)
    • Partially equates to WebForms Controls
    • Encapsulates more advanced rendering logic outside of the View
    • Html.ActionLink is very important
      • ex. Html.ActionLink( DisplayText, Action, Controller, new {ID or other defined value as property of an anonymous type}, new {anchor tag html attribute defined as a property of an anonymous type})

 

Tips and Tricks / Best Practices

  1. Use Html.ActionLink 
    Do not manually create anchor tags, because if the routing configuration is changed your links will be broken.  Html.ActionLink automatically renders appropriate URLs based on the current routing configuration.
  2. Use Descriptive Keyword Rich Names Instead of Database Table IDs (SEO) In URLs
    This is particularly useful for public facing websites such as blogs or e-commerce sites.  Google and other search engines index keywords in URLs, and ID numbers yield no benefit.  Again the default URL structure in ASP.NET MVC is “Controller Prefix/Action/ID” (Product/Detail/2).  There is nothing stating that “ID” has to be an integer.  You could have something like “Product/Detail/paper-tray”.  Just be sure that the controller action methods “ID” parameter is typed as a string.  I generally keep an indexed column in my database for this.  It is a lower case variation of the display name with dashes in place of spaces.  I suppose you could also use a lookup dictionary that maps to the table ID instead.
  3. Configure Routing to Optimize URLs
    Don’t feel bound to the default URL routing configuration.  The routing is very customizable as seen below.
    routes.MapRouteLowercase(
        "Catalog",                                             
        "workspace-organizers",                          
        new { controller = "Category", action = "Index" } 
    );
    //ex. /workspace-organizers
    
    routes.MapRouteLowercase(
        "ProductCategory",                                             
        "{urlname}/cat",                          
        new { controller = "Product", action = "Index", urlname = "" } 
    );
    //ex. /desktop-accessories/cat
    
    routes.MapRouteLowercase(
        "ProductDetail",                                              
        "{urlname}/prod",                         
        new { controller = "Product", action = "Details", urlname = "" }  
    );
    //ex. /cd-holder/prod
  4. Use Strongly Typed Models
    Avoid using hard coded “Magic” strings whenever possible.  One way to send model data to the view is through the use of the ViewData dictionary object.
    public ActionResult Index()
    {
        ViewData["Message"] = "Welcome to ASP.NET MVC!";
        return View();
    }
    <h2><%=
    Html.Encode(ViewData["Message"]) 
    %></h2>
    This is problematic for a number of reasons.  Errors from typos and broken references when refactoring may not show up until runtime.  Also complex objects stored in the dictionary will have to be typed in the view in order to access their properties.  It is best to give your view a specified model type as shown below.
    public ActionResult Index()
    {
        string strMessage = "Welcome to ASP.NET MVC!";
        return View(strMessage);
    }
    Next in the view give the page declaration’s inherits attribute a type.
    Inherits="System.Web.Mvc.ViewPage<string>"
    Then you can utilize the “Model”, which is the instance of the specified type.
    <h2><%=
    Html.Encode(Model) 
    %></h2>

  5. Use ViewModels 
    Often it is necessary to have more than one type represented as model data.  I prefer to have a model per view that consolidates all the required types.
    public class ProductIndexViewModel : ViewModelBase
    {
        public string CategoryName { get; set; }
        public IEnumerable<CatalogListItem> CatalogItems { get; set; }
    }
    public ActionResult Index(string id)
    {
        var viewmodel = new Models.ProductIndexViewModel();
        viewmodel.CatalogItems = productRepository.GetCatalogItems(id);
        viewmodel.CategoryName = categoryRepository.GetCategoryName(id);
        return View(viewmodel);
    }
  6. Use a Master ViewModel
    How do you get data to a master page so it can be used across multiple pages?  A shopping cart summary is one example of where this is needed.

    One option is to have a master controller that all relevant controllers would inherit.  The master controller could set a value in the ViewData dictionary.  Again I don’t like this because it isn’t strongly typed.

    Another option is to use RenderAction to simulate an http request and return a partial view rendered to html.  This has some advantages in that sections of a page can be cached.

    The technique I like to use is to create a master viewmodel that the other models inherit.  Master pages can have a specified model type just like normal views, so I set the master page’s type to the view model base.  This will work as long as every view that implements the master page receives a viewmodel that inherits from the master view model.
    public class ViewModelBase
    {
        public ViewModelBase()
        {
            SiteHeaderText = "MVC Outdoor Catalog";
        }
    
        public string SiteHeaderText { get; set; }
    }
    <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage<MvcCatalog.Models.ViewModelBase>" %>
    <h1><%= Html.Encode(Model.SiteHeaderText) %></h1>
  7. Use Custom HTML Helper Extensions
    Use html helper extension methods for complex rendering logic.  I first started using this technique when I needed to add some functionality to navigation menu items on a master page.  Initially it was an unordered list containing links generated using Html.ActionLink.  The current page’s menu item needed to have a different CSS class.  Custom HTML Helper to the rescue.  (Originally I found a variation of this at http://www.asp.net/learn/mvc/tutorial-27-cs.aspx  )
    public static class MenuItemHelper
    {
        public static string MenuItem(this HtmlHelper helper, string linkText, string actionName, string controllerName)
        {
            string currentControllerName = (string)helper.ViewContext.RouteData.Values["controller"];
            string currentActionName = (string)helper.ViewContext.RouteData.Values["action"];
    
            // Add selected class
            if (currentControllerName.Equals(controllerName, StringComparison.CurrentCultureIgnoreCase) && currentActionName.Equals(actionName, StringComparison.CurrentCultureIgnoreCase))
                return string.Concat("<li class=\"selected\">", helper.ActionLink(linkText, actionName, controllerName), "</li>");
    
            // Add link
            return string.Concat("<li>", helper.ActionLink(linkText, actionName, controllerName), "</li>");
        }
    }
    <ul id="menu">              
        <%= Html.MenuItem("Home", "Index", "Home")%>
        <%= Html.MenuItem("Catalog", "Index", "Category")%>
        <%= Html.MenuItem("About", "About", "Home")%>
    </ul>
  8. Use Custom Routing Extensions
    I like all my URLs to be lower case, but I don’t want to change my controller and action methods to be lower case.  The use of a RoutingCollection extension method easily solves this.  Found this nifty extension method at: http://goneale.com/2008/12/19/lowercase-route-urls-in-aspnet-mvc/ 

    routes.MapRoute(
        "Default",                                            
        "{controller}/{action}/{id}",                         
        new { controller = "Home", action = "Index", id = "" }
    );
    Using the normal “MapRoute” method as in the snippet above would create a URL structure like “Product/Detail/binder-holder”. 
    routes.MapRouteLowercase(
        "Default",                                            
        "{controller}/{action}/{id}",                         
        new { controller = "Home", action = "Index", id = "" }
    );
    Using the modified “MapRouteLowerCase” extension method creates a URL like “product/detail/binder-holder”.
  9. Separate Data Access Logic and Business Logic from the Controller
    The controller is really just for presentation layer management logic.  It should decide what view gets rendered and hand that view the appropriate model data.  I prefer to use the repository pattern to separate the data access logic from the controller.  I also like to use a repository interface so that it can be swapped easily with a different data access method without affecting the controller.  In this case I used manual dependency injection, but an IOC framework could be used.
    public interface ICategoryRepository
        {
            void Add(MvcCatalog.Models.Category category);
            System.Collections.Generic.IEnumerable<MvcCatalog.Models.Category> GetCategories();
            string GetCategoryName(string id);
            void Save();
        }
    ICategoryRepository categoryRepository = new CategoryRepositoryLinqToSQL();
    
    public ActionResult Index()
    {
        var viewmodel = new Models.CategoryIndexViewModel();
        viewmodel.Categories = categoryRepository.GetCategories();
        return View(viewmodel);
    }
  10. Cache Your Data  
    A substantial performance gain can be made by not hitting your database for every request.  Again here is another good reason to use the repository pattern.
    public class CategoryRepositoryCached : ICategoryRepository
    {
        private const string cacheName = "Categories";
        ICategoryRepository _repository;
    
        public CategoryRepositoryCached() 
            : this(new CategoryRepositoryLinqToSQL())
        {}
    
        public CategoryRepositoryCached(ICategoryRepository repository)
        {
            _repository = repository;
        }
    
        #region ICategoryRepository Members
    
        public void Add(Category category)
        {
            _repository.Add(category);
        }
    
        public IEnumerable<Category> GetCategories()
        {
            var categories = (IEnumerable<Category>) HttpContext.Current.Cache[cacheName];
            if (categories == null)
            {
                categories = _repository.GetCategories();
                HttpContext.Current.Cache[cacheName] = categories;
            }
    
            return categories;
        }
    
        public string GetCategoryName(string id)
        {
            return _repository.GetCategoryName(id);
        }
    
        public void Save()
        {
            _repository.Save();
            HttpContext.Current.Cache.Remove(cacheName);
        }
    
        #endregion
    }
  11. jQuery + JSON Action Methods = Cool
    It is easy to return a JSON object instead of a view.
    public JsonResult Create(string CategoryName)
    {
        var category = new Models.Category();
        category.Name = CategoryName;
        category.URLName = CategoryName.ToLower().Replace(" ", "-");
        categoryRepository.Add(category);
        categoryRepository.Save();
    
        return Json(category);
    }
    <script type="text/javascript" language="javascript">
        $("#CreateNewCategory").click(function() {
            $.getJSON("/category/create/",
                      { "CategoryName": $("#NewCategoryName").val() },
                      CategoryAdded);
                  });
    
                  function CategoryAdded(category) {
                      $("#CategoryList").append("<li><a href=\"" + category.URLName + "/cat\">" + category.Name + "</a></li>");
                  }
    </script>
  12. (When using IIS 6) Use httphandlers and httpmodules for http compression and client side static file caching
    The problem with using wildcard mapping under IIS6 is that you loose a lot of IIS functionality like http compression and client side static file caching.  I use this nifty work around: http://code.msdn.microsoft.com/fastmvc
  13. Cache Appropriate Actions
    It can be useful to cache the rendered action results of pages that do not change very often.
    public class HomeController : Controller
    {
        [OutputCache(Duration=86400, VaryByParam="none")]
        public ActionResult Index()
        {
  14. Restricting Post Data Binding / UpdateModel
    Be careful when using UpdateModel to bind posted form values to an object.  Let’s say that you want to allow a user to edit a product while restricting them from changing the price.  Even if you don’t include an input box on your form someone could fake a form post.  There are several ways to protect against this.

    One way is to give a property exclude list to the UpdateModel method.  Here are those pesky magic strings again. 
    UpdateModel(prod, null, null, new[] { "Price" });
    I prefer the strongly typed method of defining an interface that only has the properties that should be bound.
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection form)
    {
        var prod = productRepository.GetProductByID(id);
        try
        {
            UpdateModel<Models.IProductEdit>(prod);
            productRepository.Save();
  15. Note on Html Helper Magic Strings
    Again I think that magic strings should be avoided, but the default Html Helpers are full of them.  One approach is to use constants to contain the strings to one spot.  Another approach that is available in MVC Futures uses lambda expressions.  You could have something like this. 
    <%= Html.ActionLink<HomeController>(c => c.Index()) %>
    Instead of.
    <%= Html.ActionLink("Home", "Index", "Home") %>
    The only problem is that this approach uses compiled lambda expressions which can have performance/scaling issues.  It is my understanding that these issues have been fixed in ASP.NET MVC v2 (thanks for the update Elijah Manor).

 

Download

Keep in mind that my sample project is not 100% best practices.  I was trying to start simple then refactoring toward better practices.  You can download the sample below (the sample product pictures come from the Microsoft MVC StoreFront sample).

ASP.NET MVC Sample Download

Currently rated 3.3 by 111 people

  • Currently 3.297298/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

Michael Johnson Michael Johnson
Developer and Technologist.

E-mail me Send mail

Pages

    Recent comments

    Categories

    None


    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2014

    Sign in