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.
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>
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.
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.