I'm working on an ASP.Net MVC 3 site. The _Layout master view contains a menu and I want to hide some of the items in the menu based on if you are logged in and what roles you are in.
This currently works using code like this
@if (HttpContext.Current.User.Identity.IsAuthenticated)
{
<li id="MyLearningTab">@Html.ActionLink("My Learning", "MyLearning", "Learning")</li>
if (HttpContext.Current.User.IsInRole("Reporters"))
{
<li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li>
}
if (HttpContext.Current.User.IsInRole("Administrators"))
{
<li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>
<li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li>
}
}
I'd like to refactor this in to something more readable and came up with something like this
@if ((bool)ViewData["MenuMyLearning"]){<li id="MyLearningTab">@Html.ActionLink("My Learning", "MyLearning", "Learning")</li> }
@if((bool)ViewData["MenuReports"]){<li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li>}
@if ((bool)ViewData["MenuDashboard"]){<li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li>}
@if ((bool)ViewData["MenuAdmin"]){<li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li>}
I originally added the following to my base controller constructor thinking I could setup the ViewData for these properties there
ViewData["MenuDashboard"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuAdmin"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Administrators");
ViewData["MenuReports"] = User != null && User.Identity.IsAuthenticated && User.IsInRole("Reportors");
ViewData["MenuMyLearning"] = User != null && User.Identity.IsAuthenticated;
However it turns out the User object is null at this point in the lifecycle. I've also tried creating a custom global filter but the ViewData is then not accessable.
What is the recommended way of doing something like this? Should I just leave it how it was at first with all the HttpContext code in the view?
General advice about roles
The way I have done this is to create a custom principal and to store the extra required information in there. In your example, this would at least include the roles for the user. That way you avoid making lots of extra trips to the user store (which is likely a SQL database).
Have a look a this question of mine in which I give the code which I am using successfully: Is this Custom Principal in Base Controller ASP.NET MVC 3 terribly inefficient?
Note that I am storing the custom principal in the cache rather than in the session (just being paranoid about session hijacking).
I like this approach as it is very extensible. For example, I have since extended this to expose Facebook credentials for when the user logs in via Facebook.
Just remember that if you are caching data you need to remember to update it when it changes!
Answer to your question
Just to add, in your specific case, you should probably store this extra information in a ViewModel
and then your view would say things like:
@if(ShowReports) { <li id="ReportTab">@Html.ActionLink("Reports", "Index", "Reports")</li> }
@if(ShowDashboard) { <li id="DashboardTab">@Html.ActionLink("Dashboard", "Dashboard", "Admin")</li> }
@if(ShowAdmin { <li id="AdminTab">@Html.ActionLink("Admin", "Index", "Admin")</li> }
with the ViewModel Code saying something like:
public bool ShowReports {get;set;}
public bool ShowDashboard {get;set;}
public bool ShowAdmin {get;set;}
public void SetViewModel()
{
if (User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.IsInRole("Reporters"))
{
ShowReports = true;
}
if (HttpContext.Current.User.IsInRole("Administrators"))
{
ShowDashboard = true;
ShowAdmin = true;
}
}
}
I actually tend to take this one step further and create a ReportsLink
in my ViewModel
and set it to contain the link if the user is authorised or to be an empty string if they are not. Then the view just says:
@Model.ReportsLink
@Model.DashboardLink
@Model.AdminLink
In that case the pertinent part of the ViewModel might be like this:
ReportLink = new MvcHtmlString(HtmlHelper.GenerateLink(HttpContext.Current.Request.RequestContext, System.Web.Routing.RouteTable.Routes, "linktext", "routename", "actionname", "controllername", null, null));