views:

89

answers:

4

I am in the process of creating tabbed navigation where the route location can vary. The parameter used to render the tabs should be based on the presently viewed data (which when it is a user, may not be the logged in user).

In the example image this is a user. Therefore, if I am looking at Andrew Steele then the links should be contextual to Andrew Steele (Andrew's summary, computers, accounts etc.). When I am looking at Bruce Hamilton the links should be contextual to Bruce Hamilton (Bruce's summary, computers, accounts etc.).

Example of Tabbed Navigation

I've solved this by sticking the necessary parameter value in each ViewModel and then passing the value onto a partial to render the links; this feels kludgey. I'd prefer to not shove the linking parameter data into each ViewModel. It would seem reasonable to use Html.Action or Html.RenderAction combined with a ViewData Dictionary value, but I tend to shy away from using "magic strings" where possible.

Is there a better way to get the parameter value to the view that I am missing?

A: 

one solution is to create base controller and have all ur controllers inherit from it. in base controller u can add necessary value into viewmodel

public applicationController:Controller
{
   ViewData["linkvals"] = someValues;
}

public HomeContorller:ApplicationController{}

second solution is to create a base view model and have all ur viewmodels inherit it. in constructor of base viewmodel u can create link values and assign them to ur properties

public baseViewModel
{
   public LinkVal{get;set;}
   public baseViewModel()
   {
      //calculate link vals
   }
}

public ViewModelA:baseViewModel{}
Muhammad Adeel Zahid
+1  A: 

You can create a viewmodel for this that exposes the tab preference. If each tab is totally different you could have a viewmodel base class that exposes the tab preference and each of the tabs could have their own view model.

Andrew Siemer
I see what you're saying. Thankfully, the tabs are all using the same key so one `ViewModel` would suffice. How do I get the data to the tabbed ViewModel though?
ahsteele
+1  A: 

I wrote a site a while back where I had different tabs that went to different places for different users. I'll walk you through my solution hopefully I understood the question correctly and some of this helps.

As far as getting data to and from the View, I do use the ViewDataDictionary. To the best of my knowledge, that's what it's for when your model doesn't consist of a single simple object. In order to get around the "magic strings" of view keys, I create a bunch of extension methods on the ViewDataDictionary. This has the drawback that you end up with a slew of extra methods, but at least all of your string keys are isolated in a single location. You could even go the extra step of create constants in the class, but it seems redundant when only this class uses them. Extension properties would be better but...

/// <summary>
/// Gets the list of tabs to show.
/// </summary>
/// <param name="dictionary"></param>
/// <returns></returns>
public static IList<TabItemDisplay> TabListGet(this ViewDataDictionary dictionary)
{
    IList<TabItemDisplay> result;
    if (dictionary.ContainsKey("TabList"))
        result = dictionary["TabList"] as IList<TabItemDisplay>;
    else
        result = null;
    return result;
}

/// <summary>
/// Sets the list of tabs to show.
/// </summary>
/// <param name="dictionary"></param>
/// <param name="tabList"></param>
/// <returns></returns>
public static IList<TabItemDisplay> TabListSet(this ViewDataDictionary dictionary, IList<TabItemDisplay> tabList)
{
    dictionary["TabList"] = tabList;
    return tabList;
}

You'll notice that I have an explicit view object, TabItemDisplay, that I pass into the dictionary. This contains all of the values necessary to pass to Html.ActionLink.

public class TabItemDisplay
{
    public string Name { get; set; }
    public string Action { get; set; }
    public string Controller { get; set; }
    public object RouteValues { get; set; }
}

Since this view is not the main content of the page, I prefer to put the logic of creating the tab items, including destination parameters, into an ActionFilter. This allows me to reuse the tab creation logic across different actions and controllers. Any View that contains the tab partial control gets the CreatTabAttribute slapped across the corresponding Action or Controller and it's good to go.

This may be more than you needed, but I hope some of it helps.

EDIT: Just realized I didn't include what this looks like in the partial view. I actually have an HtmlHelper extension that renders a more intricate tab, but you get the idea.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<div id="tabs">
<%  
    if (null != ViewData.TabListGet()) {
        foreach(var item in ViewData.TabListGet()) {
%>
    <%= Html.ActionLink(item.Name, item.Action, item.Controller, item.RouteValues, null)%>
<%      
        }
    }
%>
</div>

EDIT: Adding a short example of the ActionFilter I use.

public class CreateContentTabsAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var result = filterContext.Result as ViewResultBase;
        if (null == result) return;

        var routeValues = filterContext.RouteData.Values;
        var repository = ObjectFactory.GetInstance<ITabRepository>();
        var context = filterContext.HttpContext;
        var userName = context.User.Identity.Name; // Or get id from Membership.
        var tabs = repository.ReadByUserId(userName);  

        TabItemDisplay defaultTab = null;
        var tabItems = new List<TabItemDisplay>();
        foreach (var tab in tabs)
        {
            var tabItem = new TabItemDisplay 
            { 
                Name = tab.Name,
                Action = "View",
                Controller = "Tab",
                RouteValues = new { key = tab.Key }
            };
            tabItems.Add(tabItem);
        }

        if (context.Request.IsAuthenticated)
        {
            tabItems.Add(new TabItemDisplay
            {
                Name = "Account",
                Action = "ChangePassword",
                Controller = "Account",
                RouteValues = new { siteKey = site.Key }
            });
        }

        result.ViewData.TabListSet(tabItems);
    }
}

This is only a basic example of pulling tabs from a repository (instantiated using StructureMap), and a simple check to see if the user is authenticated. But you can do other things such as pull the requested user id for the user being displayed from routeValues.

Chuck
This looks pretty compelling, do you decorate each action that would render a view w/ the associated tabs with the `ActionFilter`?
ahsteele
That I do. If you don't limit your ActionFilter and all of your actions in a controller require the filter, you can just use it once at the controller level. Regardless, the upshot is keeping your tab creation logic separate and seeing at a glance which controllers/actions are using it.
Chuck
@Chuck, I'm in the process of implementing your suggested solution. Which of the `ActionFilterAttribute` do you override and what does that method look like?
ahsteele
@ahsteele, Added an example of the ActionFilter to my answer above. I took out some code that was there for testing purposes and such, but you get the idea.
Chuck
@Chuck, awesome! I to had overridden the `OnActionExecuted` as it seemed the most appropriate. Thanks for updating the answer!
ahsteele
+1  A: 

Personally, I'd use either RenderAction from the MVC futures or a render partial call here depending on performance requirements and taste.

Advantage to RenderAction is you won't need to pass all the required rendering data to the delegate controller action as it could do the lookup itself. Whereas a partial view would require your main page have enough view data to render the tab. Either way its just two for loops (one for the tab list, one for the tab rendering) and a bit of extra data in your Model.

Wyatt Barnett
It's the bit of extra data in the model that leaves a sour taste in my mouth. In going this route I'd agree that `RenderAction` was the better of the two as it requires less "extra" information but it still might require a few giant leaps in the controller to get the necessary identification data into the model.
ahsteele
Interesting. I wouldn't call it extra data -- you do need it to render the page. You could just as easily render these tabs as part of the page, its just a little more managable to have separate files . . .
Wyatt Barnett