The ViewModels in my current project had got quite complex; as well as properties copied from model
objects, they increasingly had flags used by Views to know whether to render links or sub-sections.
The logic which set these properties was bloating Controllers, so I factored it out into objects
which populate all non-editable properties of a ViewModel; ViewModelBuilders.
The system has the following components:
- ViewModels - objects which provide a View with the information it needs to be rendered. These all
implement an empty
IViewModelmarker interface. ViewModelBuilders - objects which populate the non-editable properties of a particular type ofIViewModel.ViewModelBuilders implement anIViewModelBuilderinterface with aBuild(IViewModel)method.- A
ViewModelBuilderLibrary- a self-populating library of all the availableViewModelBuilders. The Library takes requests to populate anIViewModel, and passes it to the appropriateViewModelBuilders to be built. - A base Controller which uses the
ViewModelBuilderLibraryto populateIViewModels after an Action executes for a GET request, and before an action executes for a POST request. This ensures ViewModels are always populated when they need to be.
An Example
Here’s a User Details action method which populates an IsEditable property on a UserViewModel -
obviously this is a very simple example to illustrate the principal. The Controller has a
_userRepository it can use to find Users, and an _activeUserFinder it can use to access
the currently-active User.
public ActionResult Details(int userId)
{
User userToEdit = _userRepository.FindUserById(userId);
UserViewModel viewModel = new UserViewModel(userToEdit)
{
IsEditable = IsUserEditable(userToEdit)
};
return View(viewModel);
}
private bool IsUserEditable(User userToEdit)
{
User activeUser = _activeUserFinder.CurrentlyActiveUser;
return (activeUser == userToEdit) ||
activeUser.IsInRole("Administrator");
}
The ViewModelBuilder
Now let’s move the IsEditable logic from the Controller to a UserViewModelBuilder; Builder classes
are named <ViewModelType>Builder by convention. The IUserRepository and IActiveUserFinder
also come across from the Controller.
public class UserViewModelBuilder : IViewModelBuilder
{
private readonly IActiveUserFinder _activeUserFinder;
private readonly IUserRepository _userRepository;
public UserViewModelBuilder(
IActiveUserFinder activeUserFinder,
IUserRepository userRepository)
{
_activeUserFinder = activeUserFinder;
_userRepository = userRepository;
}
public void Build(IViewModel viewModelToBuild)
{
UserViewModel viewModel = (UserViewModel)viewModelToBuild;
User userToEdit =
_userRepository.FindUserById(viewModel.UserId);
viewModel.IsEditable = IsUserEditable(userToEdit);
}
private bool IsUserEditable(User userToEdit)
{
User activeUser = _activeUserFinder.CurrentlyActiveUser;
return (activeUser == userToEdit) ||
activeUser.IsInRole("Administrator");
}
}
The ViewModelBuilderLibrary
The ViewModelBuilderLibrary class pairs ViewModelBuilders with the IViewModel type they build
and caches them in static scope. It also caches IViewModel properties of IViewModels in order
to populate those. The ProcessViewModelProperties method uses some extension methods for IEnumerable
which match the core library ones for IEnumerable<T>; Any(), First() and ForEach(). These
extension methods are freely available here. Population
of an IViewModel occurs recursively down though its properties and its properties’ properties.
public class ViewModelBuilderLibrary
{
// This is a cache of IViewModel types against the
// ViewModelBuilders which build them:
private static readonly Dictionary<Type, IViewModelBuilder>
_viewModelBuilderCache = CreateViewModelBuilderCache();
// This is a cache of IViewModel types against PropertyInfos
// describing their IViewModel properties:
private static readonly Dictionary<Type, PropertyInfo[]>
_viewModelModelPropertiesCache =
CreateViewModelModelPropertiesCache();
public void BuildViewModel(IViewModel viewModelToBuild)
{
BuildViewModel(viewModelToBuild.GetType(), viewModelToBuild);
}
private static void BuildViewModel(
Type viewModelType,
IViewModel viewModelToBuild)
{
// Keys contains the Types of the IViewModels which the keyed
// ViewModelBuilders build:
_viewModelBuilderCache.Keys
.Where(bvmt => bvmt.IsAssignableFrom(viewModelType))
.ForEach(bvmt =>
{
_viewModelBuilderCache[bvmt].Build(viewModelToBuild);
ProcessViewModelProperties(bvmt, viewModelToBuild);
});
}
private void ProcessViewModelProperties(
Type viewModelType,
IViewModel viewModelToBuild)
{
if (!_viewModelModelPropertiesCache.ContainsKey(viewModelType))
{
return;
}
_viewModelModelPropertiesCache[viewModelType].ForEach(pi =>
{
object viewModelPropertyValue =
pi.GetValue(viewModelToBuild, null);
IViewModel propertyViewModel =
viewModelPropertyValue as IViewModel;
if (propertyViewModel != null)
{
BuildViewModel(
propertyViewModel.GetType(),
propertyViewModel);
}
else
{
IEnumerable viewModelEnumerable =
(IEnumerable)viewModelPropertyValue;
if ((viewModelEnumerable != null) &&
viewModelEnumerable.Any())
{
viewModelType =
viewModelEnumerable.First().GetType();
viewModelEnumerable.ForEach(vm =>
BuildViewModel(viewModelType, (IViewModel)vm));
}
}
});
}
#region Setup Methods
private static Dictionary<Type, IViewModelBuilder>
CreateViewModelBuilderCache()
{
IEnumerable<Type> allAvailableClassTypes = Assembly
.GetExecutingAssembly()
.GetAvailableTypes(typeFilter: t => !t.IsInterface);
IEnumerable<Type> allViewModelTypes = allAvailableClassTypes
.Where(t => typeof(IViewModel).IsAssignableFrom(t))
.ToArray();
return allAvailableClassTypes
.Where(t => typeof(IViewModelBuilder).IsAssignableFrom(t))
.Select(t => (IViewModelBuilder)InjectionService.Resolve(t))
.ToDictionary(
vmb => GetViewModelBuilderBuiltType(
allViewModelTypes,
vmb),
vmb => vmb);
}
private static Type GetViewModelBuilderBuiltType(
IEnumerable<Type> allViewModelTypes,
IViewModelBuilder viewModelBuilder)
{
// Builder classes are named <ViewModelType>Builder
// by convention:
string viewModelTypeName = viewModelBuilder.GetType().Name
.Replace("Builder", null);
Type viewModelType = allViewModelTypes
.FirstOrDefault(vmt => vmt.Name == viewModelTypeName);
if (viewModelType == null)
{
throw new NotSupportedException(
"Unable to find a matching ViewModel Type " +
"for ViewModelBuilder " +
viewModelBuilder.GetType().FullName);
}
return viewModelType;
}
private static Dictionary<Type, PropertyInfo[]>
CreateViewModelModelPropertiesCache()
{
return _viewModelBuilderCache.Keys.ToDictionary(
vmt => vmt,
vmt => vmt
.GetProperties(
BindingFlags.Public | BindingFlags.Instance)
.Where(PropertyIsIViewModelType())
.ToArray());
}
private static Func<PropertyInfo, bool> PropertyIsIViewModelType()
{
// The PropertyInfo is one we want to cache if the property is
// an IViewModelBuilder type, or a generic IEnumerable with an
// IViewModelBuilder generic argument:
return pi =>
typeof(IViewModel).IsAssignableFrom(pi.PropertyType)
||
(typeof(IEnumerable).IsAssignableFrom(pi.PropertyType)
&&
pi.PropertyType.IsGenericType
&&
typeof(IViewModel).IsAssignableFrom(
pi.PropertyType.GetGenericArguments().First()));
}
#endregion
}
The Base Controller
Finally, these two methods in a base Controller class plug the ViewModelBuilderLibrary into the
ASP.NET MVC pipeline; the base Controller class has the ViewModelBuilderLibrary as one of its properties.
protected override void OnActionExecuting(
ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request
.RequestType.ToUpperInvariant() == "POST")
{
IViewModel viewModel;
if (IsIViewModelViewRequest(filterContext, out viewModel))
{
ViewModelBuilderLibrary.BuildViewModel(viewModel);
}
}
base.OnActionExecuting(filterContext);
}
protected override void OnActionExecuted(
ActionExecutedContext filterContext)
{
if (filterContext.HttpContext.Request
.RequestType.ToUpperInvariant() == "GET")
{
IViewModel viewModel;
if (IsIViewModelViewResult(filterContext, out viewModel))
{
ViewModelBuilderLibrary.BuildViewModel(viewModel);
}
}
base.OnActionExecuted(filterContext);
}
private static bool IsIViewModelViewRequest(
ActionExecutingContext context,
out IViewModel viewModel)
{
if (context.ActionParameters == null ||
!context.ActionParameters.Any())
{
viewModel = null;
return false;
}
viewModel = context.ActionParameters
.Select(kvp => kvp.Value)
.OfType<IViewModel>()
.FirstOrDefault();
return viewModel != null;
}
private static bool IsIViewModelViewResult(
ActionExecutedContext context,
out IViewModel viewModel)
{
ViewResult viewResult = context.Result as ViewResult;
if (viewResult == null)
{
viewModel = null;
return false;
}
viewModel = viewResult.ViewData.Model as IViewModel;
return viewModel != null;
}
Summing Up
I’ve found using ViewModelBuilders to have several advantages:
-
More isolated responsibilities; ViewModel population occurs in dedicated objects, better satisfying the Single Responsibility Principle.
-
Easier to test; I can test ViewModel population without creating or invoking any methods on a Controller.
-
Less data written into hidden fields; as ASP.NET MVC is stateless I’ve previously used hidden fields on the client to persist property values; now the only data required on the client is identifiers for the objects to which a View relates - everything else is reliably, consistently and transparently populated on the server.
Comments
One comment