If you are new to MVC web development it can initially be tricky to figure out how to handle UI features that will be used by multiple views. In WebForms you would simply create a control that encapsulated this element's look and function. In CodeSaga there are many views that share view elements, for example many views have tabs. In order to handle the common view elements I organized the hard typed view models into an inheritance hierarchy.

image

The above class diagram shows a subset of the view models in CodeSaga. The ViewModelBase exposes a list of MenuTabs and a method AddTabs that inheritors can use to add menu tabs. Here is the code for the RepositoryContextViewModel, the base class for all views that have the history, browse, search and authors tabs.

public class RepositoryContextViewModel : ViewModelBase
{
    public RepositoryUrlContext UrlContext { get; set; }
    
    public RepositoryContextViewModel()
    {
      AddTabs(
        MenuTab
          .WithName(MenuTabName.History)
          .ToAction<HistoryController>(x => x.ViewHistory("", null, null)),
        MenuTab
          .WithName(MenuTabName.Browse)
          .ToAction<HistoryController>(x => x.ViewBrowse("")),
        MenuTab
          .WithName(MenuTabName.Search)
          .ToAction<SearchController>(x => x.Search("")),
        MenuTab
          .WithName(MenuTabName.Charts)
          .ToAction<ChartsController>(x => x.ViewChart("")),
        MenuTab
          .WithName(MenuTabName.Authors)
          .ToAction<AuthorsController>(x => x.ViewStats("")));
    }    
}

The AdminViewBase class defines the admin tabs in a similar way. The view code that then renders the tabs is very simple:

<ul class="menu">
  <for each="var tab in tabs">        
    <li class="on?{tab.IsActive}">
      ${Html.ActionLink(tab.Text, tab.Action, tab.Controller)}
    </li>
  </for>        
</ul>

image

The only job left to do in the controller action is to set the currently active tab, like this:

public ActionResult ViewDiff(string urlPath, int? r1, int? r2)
{
  var urlContext = RepositoryUrlContext.FromString(urlPath);
      
  var diff = repository.GetFileDiff(urlContext.ReposName, urlContext.Path, r1.Value, r2.Value);

  var model = new DiffViewModel
  {
    UrlContext = urlContext,
    FileDiff = diff,
    ActiveTabName = MenuTabName.History
  };

  return View("Diff", model);
}

Setting the active tab like this could be refactored to be handled in a more declarative way, for example with an attribute on the controller class or action method. But I haven't found the need to do that yet as I think it's pretty declarative as it is.

One can question why I have used a MenuTab presentation model at all, why not define the tabs directly in the views? Since the tabs are only declared statically you could achieve a similar result using a hierarchy of partial views. The reason I did not choose that solution is because I actually needed (or wanted) the solution to support creating tabs dynamically in an easy way. This is used in CodeSaga when you click the edit link in the repository list (in the admin), this will open the edit view in a new tab.

image

The controller action for this:

private ActionResult Edit(string reposName)
{
    var model= new RepositoryEditViewModel();
    model.Repository = reposRepository.GetByName(name);
    
    model.AddTabs(
      MenuTab
        .WithName("Edit " + reposName)
        .ToAction<RepositoryAdminController>(x => x.Edit(reposName))
        .SetActive());
        
    return View(model);
}

It is worth to point out that the term View Model in this post should not be confused with Presentation Model. Instead I see it as a sort of container data structure for all the domain and presentation model data a specific view need.

5 comments:

Daniel Fernandes said...

Great article.
And thanks to make me double check again the definitions of View Model and Presentation Model. I have done a bit of WPF in the past and the View Model is used there quite a bit. And as you said, the View Model is to present "a" Model to a View. That Model could well be the Domain Model assuming the system is simple enough, Data Transfer Objects to facilitate serialization for instance, or Presentation Model which has the only reason of existence to provide GUI architecture independent data to GUI controls and no business semantic embedded.Well, hopefully I am right, I have to say I have a hard time finding good resources about Presentation architectures.

Rasmus said...

Looks great,

But where do you get the tabs from you are rendering in the view?

Torkel Ödegaard said...

They are part of the view model that is passed to the view.

Prakash said...

Doesn't this make your ViewModel totally dependent on your View, by the way of tabs?

Ramon said...

This is fantastic!