ASP.NET MVC from Basics to Tips and Tricks

 

A simple diagram depicting the relationship be...

First of all 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 athttp://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).

 

 

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s