8

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?

2

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
    }
}
0

I would go with your first option, making the necessary modifications to your model classes. It seems that your option 4, modifying the page model, would be trading Model references for helper references in your razor views. Option 1 seems easier to maintain than option 4 because it would seemingly require less code and because more MVC developers would understand it.

0

My opinion is, contextual data with ViewBag/ViewData configured by contextual data service. I got annoyed with no or very bad flexibility of having "BaseController" which sets all stuff from "GodModel" becaues every view needs to have it unless I need to add some new thin view where I do not need all that stuff. Where of course "GodModel" was also a base class for models in views.

It is hard to tell up front if something is really needed by "all" views and making a lot of stuff mandatory makes it for me much harder than making stuff optional and when once in a while I forget to set it up because it is dynamic.

Of course all view specific and really mandatory things should go to model and be strongly typed and have validation. But general stuff that can bog down performance because someone thought that "everything has to be strongly typed" is not nice.

0

it sounds like MOST of the information you need to inspect is user-centric and not action-based. why not store your UserConfiguration in the session? another approach, depending on how you are doing user auth/management, is to keep all the information you need in the ClaimsPrincipal (sample below)...

    private ClaimsPrincipal CurrentClaimsPrincipal
    {
        get { return System.Security.Claims.ClaimsPrincipal.Current; }
    }

    public string Firstname
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.GIVEN_NAME_KEY)?.Value : string.Empty; }
    }

    public string Lastname
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.FAMILY_NAME_KEY)?.Value : string.Empty; }
    }

    public string AccessLevel
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.ACCESS_LEVEL_KEY)?.Value : string.Empty; }
    }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.