I am building a pretty big MVC project, and I'm having some trouble with my view data. I have a pretty complicated model structure. I didn't want to 'polute' my models with simple display data, but I wanted strongly typed data, so I wrote a couple of these simple classes:
public class GeneralViewData
{
private static string Id = "GeneralViewData";
public string PageTitle { get; set; }
public bool ShowGuestMenu { get; set; }
public string CurrentUserName { get; set; }
public bool ShowOrganisationAdminMenu { get; set; }
protected GeneralViewData() { }
public static GeneralViewData Retrieve(ViewDataDictionary viewDataDictionary)
{
object viewData = viewDataDictionary[Id];
if (viewData == null) return Initiate(viewDataDictionary);
return (GeneralViewData)viewData;
}
private static GeneralViewData Initiate(ViewDataDictionary viewDataDictionary)
{
GeneralViewData viewData = new GeneralViewData();
viewDataDictionary[Id] = viewData;
return viewData;
}
}
So anywhere you can access the ViewData, you can easily get the model and access it with:
var generalViewData = GeneralViewData.Retrieve(ViewData);
This works perfectly for most of the site, but I'm having trouble with the views of MVCSiteMapProvider. Aparently the sitemapprovider does not pass the main ViewData, but instead passes a new one to the partial views.
I need a way to either make MVCSiteMapProvider pass the full ViewData to its views, or a complete way around this by maybe storing the GeneralViewData instance somewhere else. I've been racking my brain for a way around this, so I'm open for anything.
Option #1
You could use the SiteMapTitleAttribute, but since your node is part of the menu, you would literally have to do that in every controller action method.
Option #2
To make it work globally without adding the SiteMapTitleAttribute to every controller action, you could also set the title explicitly in the Application_PostAuthorizeRequest event.
protected void Application_PostAuthorizeRequest(object sender, EventArgs eventArgs)
{
var user = HttpContext.Current.User;
if (user.Identity.IsAuthenticated)
{
var accountNode = MvcSiteMapProvider.SiteMaps.Current.FindSiteMapNodeFromKey("MyAccount");
if (accountNode != null)
{
accountNode.Title = "Hello, " + user.Identity.Name;
}
}
}
This assumes your account node is configured with an explicit key set with the value of "MyAccount".
Option #3
A cleaner way might be to use a custom attribute to add a class that can retrieve the current username and other properties.
If you set the object in the Application_PostAuthorizeRequest event above, you could potentially use your existing object with static properties.
However, a more integrated way would be to use [IDynamicNodeProvider] or ISiteMapNodeProvider to supply a custom object. Do note that if you go this route, you will need to use an object with properties/methods that retrieve the data from the current request (rather than storing them), because this object will be shared between all users of the SiteMap.
You can then customize the MvcSiteMapProvider display templates to show this data on the appropriate nodes when the custom object(s) exist.
Option #4
Integrate your model with ASP.NET better by implementing IPrincipal and/or IIdentity as described here, then you can add your profile data to your custom IPrincipal and/or IIdentity implementation so it is available globally through the HttpContext object.
Once the data is available globally, you can access it directly from your views. Then you could customize the SiteMapNodeModel display template so it displays the data when appropriate.
@model MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel
@using System.Web.Mvc.Html
@using MvcSiteMapProvider.Web.Html.Models
@* Fix the title for the MyAccount Node *@
var title = Model.Title;
if (Model.Key == "MyAccount")
{
title = "Hello, " + (User as CustomPrincipal).CurrentUserName;
}
@if (Model.IsCurrentNode && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>@title</text>
} else if (Model.IsClickable) {
if (string.IsNullOrEmpty(Model.Description))
{
<a href="@Model.Url">@title</a>
}
else
{
<a href="@Model.Url" title="@Model.Description">@title</a>
}
} else {
<text>@title</text>
}
This example assumes you have explicitly set a key attribute/property of your account node to "MyAccount".
I would also recommend that instead of having a boolean for "ShowGuestMenu" and "ShowOrganisationAdminMenu" that you instead make an ASP.NET roles for this and use AuthorizeAttribute in conjunction with Security Trimming to control the visibility of the sub menus. I would only recommend doing it your way if you want the user to have the ability to toggle the menu on and off, and in that case I would recommend using a custom visibility provider instead of controlling the visibility by changing the display templates.