Thursday, 3 May 2012

Sharing common view model data in asp.net mvc with all the bells and whistles

In anything but the most trivial applications, there are common pieces of data you will want to share between your different views. Typical examples include the name of the signed-in user, pervasive summaries such as the last three items viewed, unread message counts or anything else that typically appears in a navigation element and is user-specific.

There are many techniques for doing this but they all fell short for us in way or another as we wanted to meet all the following requirements:

  • It should be strongly-typed (no using of viewbag/viewdata, thanks - in our opinion it makes it too difficult to refactor views later).
  • The views should be bound to the necessary models to enable intellisense.
  • It should support ctor dependency injection.
  • The common data should be available to controllers and views. This is especially useful when your application supports authentication and you need to show a property of the user in the view and need to use a property of the user in your controller action.
  • You should be able to opt-out if necessary.
  • Controller actions shouldn't need to change in any way - e.g. no calling of functions to populate the models.
The first step is to define a class that will represent this shared context. There isn't anything special about this class - it's a regular poco.

Here is an example that will store the current user and the number of unread messages in their inbox.
namespace Web.Models
{
    public class SharedContext
    {
        public User CurrentUser { get; set; }
        public int UnreadMessageCount { get;set; }
    }
}
Once you have created your shared context class you need to create a base view model. Again, it's just a poco but what is important is that it is able to hold an instance of the shared context which will be explained in more detail further below. Here is an example:
namespace Web.Models
{
    public class LayoutModel
    {
        public SharedContext Context { get; set; }
    }
}
This is the model you will bind to your _Layout file which takes care of the intellisense and "no loosely-typed view data" requirements. In your _Layout, if you would like to show the user's name, for example, you could access the property with @Model.Context.CurrentUser.Name (assuming you had a User class with a Name property, obviously).

The next step is to wire up these classes so they are populated automatically. We start by creating the interface for what I have called the view model factory.

An example of such an interface is as follows:
namespace Web.Mvc
{
    public interface IViewModelFactory
    {
        T Create<T>() where T : SharedContext, new();
        void Set<T>(T model) where T : SharedContext, new();
    }
}
The generic constraint ensures that we can access the context properties in the method implementations. Here is an example implementation of this interface:
namespace Web.Mvc
{
    public class ViewModelFactory : IViewModelFactory
    {
        private readonly IUserMessageService _userMessageService;
        private readonly IUserService _userService;

        public ViewModelFactory(IUserMessageService userMessageService,
            IUserService userService)
        {
            _userMessageService = userMessageService;
            _userService = userService;
        }

        public T Create<T>() where T : SharedContext, new()
        {
            var model = new T();
            Set(model);

            return model;
        }

        public void Set<T>(T model) where T : SharedContext, new()
        {
            var user = _userService.GetCurrent();

            model.User = user;
            model.UnreadMessageCount = _userMessageService.GetUnreadCount(user.Id);
        }
    }
}
Hopefully it's pretty straightforward. It's an implementation of the view model factory that is injected with several fictitious dependencies and generates a shared context. You will need to use your imagination here a bit.

At this point, you are going to want to register the view model factory in whatever DI container (I hope) you're using. In Unity, you might do something like:
container.RegisterType<IViewModelFactory, ViewModelFactory>(new PerCallContextLifeTimeManager());
Although usually not a fan of inheritance it works well for this scenario. You need a base class from which all your controllers will inherit (instead of from "Controller"). You might have done this already for various other reasons. Here is an example:
namespace Web.Mvc
{
    public class BaseController : Controller
    {
        public SharedContext Context { get; set; }
    }
}
In one of your action methods, you could access the current user via Context.CurrentUser.

We want our view model factory to be called automatically so our model is populated correctly. Here is the code for that attribute - you should be able to use this class as-is unless you've renamed the view model factory or shared context.
namespace Web.Mvc
{
    public class LayoutModelAttribute : ActionFilterAttribute
    {
        private readonly IViewModelFactory _viewModelFactory;
        
        public LayoutModelAttribute(IViewModelFactory viewModelFactory)
        {
            _viewModelFactory = viewModelFactory;
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var controller = filterContext.Controller as BaseController;
            if (controller != null)
            {
                (controller).Context = _viewModelFactory.Create<SharedContext>();
            }
        
            base.OnActionExecuting(filterContext);
        }

        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            viewModel = filterContext.Controller.ViewData.Model;
            var controller = filterContext.Controller as BaseController;

            var model = viewModel as LayoutModel;
            if (model != null)
            {
                (model).Context = controller != null && controller.Context != null
                    ? controller.Context
                    : _viewModelFactory.Create<SharedContext>();
            }

            base.OnResultExecuting(filterContext);
        }
    }
}
Taking a quick step back, this is what the attribute is doing:

We override OnActionExecuting and OnResultExecuting as these execute at different places within the asp.net mvc pipeline. To accomplish the requirement of being able to access the share context in a controller, the attribute needs to execute before the controller action; hence OnActionExecuting.

To intercept the model returned from the action and populate the required properties, we override OnResultExecuting which executes after the action has complete but before the view is rendered.

There are two different base-class checks here that allow us to opt-out of the shared context population. If the base class of your controller does not inherit from your new BaseController class, the view model factory will not be invoked before the action executes.

The other check is to ensure that the view model you are returning inherits from the new LayoutModel class. If not, the view model factory is bypassed. This means you can also use the shared context in your non-layout views which can be useful.

The next step is to register this attribute so it executes for every controller. There are different ways to do this, but I generally use the following as part of my site's bootstrapper (where container is our DI container):
GlobalFilters.Filters.Add(container.Resolve<LayoutModelAttribute>(), 1);
 

The last parameter (1 in this case) is there because I have an authentication filter higher up that should be checked before the new attribute is executed. You are likely to have different requirements in your own application.

Now that the infrastructure is complete, we can get on with building the application. Here is a sample view model that you might use on the homepage of your site:
namespace Web.Models
{
    public class HomeModel : LayoutModel
    {
        public string Content { get;set; }
    }
}
And here is the controller you might use:
namespace Web.Mvc
{
    public class HomeController : BaseController
    {
        public ActionResult Index()
        {
            return View(new HomeModel { Content = "Hello View Model Factory!" });
        }
    }
}

It might seem a bit complicated at first, but after several large applications this appears to provide the most maintainable and robust solution to this particular problem.

22 comments:

  1. Excellent article. This helped me out immensely.

    ReplyDelete
  2. Wow, this is exactly what the doctor ordered. One of the cleanest and least-fussy ways of dealing with shared context information between controllers and views. Thanks so much for the write-up!

    ReplyDelete
  3. What happens if you have a view making use of one of the Contextual properties, but an action that doesn't provide a model, or the model is null?

    ReplyDelete
    Replies
    1. That's a good point - at the moment, if you supply a model that is null or a model that doesn't inherit from LayoutModel, then none of the contextual properties will be populated. If you wanted this behaviour you would need to make sure any views you rendered from this action did not rely on any property within your shared model.

      Delete
  4. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. I tried to include Ninject specific modifications to the code provided above, but this comment system didn't like the use of greater than and less than characters, probably thought they were HTML, rendering my comments useless.

      Delete
    3. Hello Rick, could you please send me the Ninject specific modification of this code to my email address? gattish@gmail.com
      Thanks in advance.

      Delete
  5. Where does PerCallContextLifeTimeManager come from?

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Great post! Very helpful! How would I pass the controller's HttpContext to the SharedContext Model for creating some shared properties on the SharedContext Model based on that information? I'm using Autofac as my DI Container if that helps.

    Thanks!

    ReplyDelete
    Replies
    1. Hi Michael,

      What I've done in the past is to create something like an IHttpContextFactory and then an implementation that has a single method - something like:

      public interface IHttpContextFactory
      {
      HttpContextWrapper GetContext();
      }

      and then implement the interface like so:

      public class HttpContextFactory : IHttpContextFactory
      {
      public HttpContextWrapper GetContext()
      {
      return new HttpContextWrapper(HttpContext.Current);
      }
      }

      Now in autofac you can wire up the dependency IHttpContextFactory to the implementation HttpContextFactory and inject that into your ViewModelFactory.

      Inside your ViewModelFactory you can then call GetContext() and use whatever you want from the request/response to set properties on your SharedContext model.

      I hope this helps.

      Gav

      Delete
  8. TechnoSoftwar having several years experience working with global customers, connecting our professionals to their business needs, as their IT Development & Support Partner. TechnoSoftwar having a team of dedicated and experienced softwares developers that works for your all business needs. Techno Softwares deals in web design and development, Customized ERPs, CRMs, Web & Mobile Applications, eCommerce platforms etc.

    ReplyDelete
  9. Nice blog..Sharing common view model data in asp.net mvc with all the bells and whistles is very easy to understand..Keep on blogging..
    PHP training in chennai

    ReplyDelete
  10. Great and useful article. Creating content regularly is very tough. Your points are motivated me to move on.


    SEO Company in Chennai

    ReplyDelete
  11. I can feel this is the right way, while I still cannot to finish a sample by myself. Could you give me a whole sample solution. my mail: wangjij@gmail.com

    ReplyDelete
  12. Wonderful post. I like your post. Keep sharing.

    ppc training in chennai

    ReplyDelete
  13. Wonderful blog.. Thanks for sharing informative blog.. its very useful to me..

    iOS Training in Chennai

    ReplyDelete
  14. This blog is having the general information. Got a creative work and this is very different one.We have to develop our creativity mind.This blog helps for this. Thank you for this blog. This is very interesting and useful.

    Mobile App Development Company in India

    ReplyDelete
  15. I am expecting more interesting topics from you. And this was nice content and definitely it will be useful for many people.

    Android App Development Company

    ReplyDelete