Thursday, 13 October 2011

Binding Views to Navigation Elements

Last night I did a bit of work on how I bind views to navigation items. I have tended to include information about 'active tabs' as part of the view model (which fits well with the idea of having one model per view) - but I didn't like the hierarchy of view models that emerged from it.

UPDATE: After a an anonymous comment on this post I have updated the implementation to use ViewData rather than TempData as the commenter rightly pointed out that, while the TempData implementation will work, TempData is for redirect.

What I ended up doing was sticking a piece of data in the ViewData dictionary and pulling it out in the view to determine which tab should be rendered as 'active'. I created some extension methods for ViewData to do this:

 
public static class ViewDataExtensions
{
public static void SetNavigation(this ViewDataDictionary viewData, T navElement)
{
var key = GetNavKey(navElement);
viewData.Add(key, navElement);
}

public static T GetNavElement(this ViewDataDictionary viewData)
{
var key = GetNavKey(typeof(T));
T t;
try
{
t = (T)viewData[key];
}
catch
{
t = default(T);
}

return t;
}

private static string GetNavKey(T navElement)
{
return GetNavKey(navElement.GetType());
}

private static string GetNavKey(Type t)
{
return "sitenav:" + t.Name;
}
}

Then I created an action filter which I can stick on a controller and/or an action. Notice the AttributeUsage which specifies the allowable targets and that the attribute can be applied more than once (this could be important if you've got more than one menu):

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple = true)]
public class BindNavigation : ActionFilterAttribute
{
private readonly object _navElement;

public BindNavigation(object navElement)
{
_navElement = navElement;
}

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewData.SetNavigation(_navElement);
base.OnActionExecuting(filterContext);
}
}

I apply this to a controller like this:
   
[BindNavigation(MainNavigation.Members)]
public class MembersController : FlyingFieldsBaseController
{
...
}

And an action like this:
 
[BindNavigation(ProfileNavigation.Wall)]
public ActionResult Index(string id)
{
var model = _getMemberProfileViewQuery.Invoke(Guid.Parse(id));
return View(model);
}

As you can tell I am using enumerations for my different "types" of navigation (main header nav, left
hand nav for profile pages, left hand nav for club management pages, etc). The thing is, though, you
can use anything you like - because TempData stores things as objects (you'll notice that my TempData extension methods use generics so you get type safety as well). You could, for example, store an object that holds the state for several layers of navigation if that's what you need.

In the views you just do this:
@{
var currentSection = ViewData.GetNavElement();
}

and use 'currentSection' however you please. I just use it to determine if I should set a CSS "selected" class on my navigation items.

Thoughts are welcome!

3 comments:

Anonymous said...

Hi Oyvind,

TempData is for holding things between web requests. For just stashing some info somewhere to be used in the View you have ViewData.

Øyvind Valland said...

Hello,

Yes - that's a fair point, and the implementation can easily change.

Øyvind Valland said...

I have updated the blog post to reflect the suggested change. Thanks!