C# – MVC – Sharing Contextual Information between views

asp.net-mvccmvc

Please excuse the long post. There is a question, just bear with me.

A Little Context

We have a site which is required to adapt considerably based on a variety of user settings, the group the user belongs to, where they come from and other things. We used to include the relevant bits on the model for the page, so if the page had a table which would show if the user was over a certain age, then on the model we would do something like:

//model
public PageModel
{
    public bool ShowTable {get;set;}
}

//controller
public PageController
{
    public ActionResult ShowPage()
    {
        var model = new PageModel() {
            ShowTable = User.Age > 21
        };
        return View(model);
    }
}

//view
@if(Model.ShowTable)
{ 
    <table>Some Html here</table>
}

This quickly became very complicated to know what we should be showing which users. To try and address this problem, we centralised all the logic about when a particular thing should be shown or hidden. We called this class UserConfiguration and it (mostly) just contained a series of functions returning booleans indicating what should be shown. This allowed us to set up a series of Specs and Tests of what a user should be shown. This UserConfigratuion was then put on a base class, which all page models were required to inherit from, so what we have currently is something like this:

//UserConfiguration 
public UserConfiguration
{
    private readonly User _user;

    public UserConfiguration(User user) {
        _user = user
    }

    public bool ShowTable() {
        return _user.Age > 21;
    }
}

//model base
public ModelBase
{
    public UserConfiguration {get;set;}
}

//model
public PageModel : ModelBase
{
    // whatever data is needed for the page
}

//controller
public PageController
{
    public ActionResult ShowPage()
    {
        var userConfiguration = new UserConfiguration(User);
        var model = new PageModel {
            UserConfiguration = userConfiguration
        };
        return View(model);
    }
}

//view
@if(Model.UserConfiguration.ShowTable())
{ 
    <table>Some Html here</table>
}

This has helped, mostly because it allowed us to create a series of tests of what a user should and should not see etc. However, it is not a very clean solution, having to put together this additional class and include it on the model. It also has ramifications for rendering Partial Views. If the model has a property IEnumerable<Foo> Foos on it, that we want to render in a partial, but that partial also relies on the user configuration, we have a problem. You can't just pass the foos to the Partial as the model, because then the partial doesn't have access to the UserConfiguration. So what would be the best way to access this information. The way I see it, in the context of asp.net MVC there are 4 ways available:

1) Have a new model for the partial e.g.

// parent view
@{
    var foosModel = new foosModel {
        Foos = Model.Foos,
        UserConfiguration = Model.UserConfiguration
    }
}

@Html.RenderPartial("FooList", foosModel)

// child partial view
@if(Model.UserConfiguration.ShowTable) {
    foreach(var foo in Model.Foos) {
        //Some HTML
    }
}

This is the probably the "most pure" solution, adhering best to the principles of MVC, but involves a lot of (arguably unnecessary) models, causing project bloat.

2) Expose the UserConfiguration through the ViewData. e.g :

// parent view
@Html.RenderPartial("FooList", Model.Foos, new ViewDataDictionary { { "UserConfiguration", Model.UserConfiguration } })

// child partial view
@{ 
    var userConfig = (UserConfiguration)ViewData["UserConfiguration"];
}
@if(userConfig.ShowTable) {
    foreach(var foo in Model) {
        //Some HTML
    }
}

I don't really like this because it isn't type safe and relies on magic strings to get it from the ViewData.

3) Put the UserConfiguration in the ViewBag. Same issues as above really

4) Modify the page model, and expose the UserConfiguration via a property of the page itself, as per http://haacked.com/archive/2011/02/21/changing-base-type-of-a-razor-view.aspx/

I feel since the UserConfiguration is ambient contextual information it makes sense to expose it via the class as in option 4 above. Is there generally accepted best practice in MVC for exposing this kind of data? Has anyone tried anything like option 4 in the past and were there any 'gotcha's'

tl;dr: What is the best way of exposing contextual information to the views on your site, in MVC in general or asp.net MVC in particular?

Best Answer

You should go with #5: None of the above.

I've started creating extension methods for the IPrincipal interface, which gives me strongly typed declarations of what the current user can do. You could even create a UserConfiguration DTO that you stuff in the Session for use by these extension methods.

First, the extension methods:

namespace YourApplication.Helpers
{
    public static class UserConfigurationExtensions
    {
        private HttpContext CurrentContext
        {
            get
            {
                return System.Web.HttpContext.Current;
            }
        }

        private static UserConfiguration Config
        {
            get
            {
                if (CurrentContext == null)
                    return null;

                return CurrentContext.Session["UserConfiguration"] as UserConfiguration;
            }
        }

        public static bool CanViewTable(this IPrincipal user)
        {
            return Config.ShowTable;
        }
    }
}

Now when the user has successfully logged in, create the instance of UserConfiguration and stash it in the Session:

public class AccountController : Controller
{
    [HttpPost]
    public ActionResult Login(LoginFormModel model)
    {
        if (ModelState.IsValid)
        {
            // Log in
            Session["UserConfiguration"] = new UserConfiguration(...);
        }

        return RedirectToAction("Index", "Home");
    }
}

Next, add the namespace in which the extension methods exist to your default namespaces in the Razor templates.

YourApplication/Views/Web.config

<?xml version="1.0"?>

<configuration>
  <!-- ... -->

  <system.web.webPages.razor>
    <namespaces>
      <add namespace="YourApplication.Helpers"/>
    </namespaces>
  </system.web.webPages.razor>

  <!-- ... -->
</configuration>

Now close and reopen the Visual Studio solution. Then your Razor templates have new methods available:

@if (User.CanViewTable())
{
    foreach(var foo in Model)
    {
        //Some HTML
    }
}
Related Topic