FubuMVC: Partial Views
Today I was working on getting a partial view working properly in FubuMVC and started trying stumbling through it, very unsuccessfully. I ended up reaching out to Joshua Arnold for help with it. I'd like to share it with everyone that might be having trouble with them.
The way we're going to be doing our partials is by having the concept of an action extension which can be called by our partial action. It's a little difficult to explain, so let's get right into the code. I'll be emphasizing all of the gotchas that I ran into, to hopefully help explain things a little better.
With all of the generics, things can get a little confusing. The generic parameter in all of these classes is going to be the model that you're working with. What we're really building here is a very maintainable "model modifier".
To start off, let's create our partial action. We'll be using a generic action that we're calling PartialAction
. It is important to note that this is the action that's going to get called when you call your partials. Since it's generic, we're going be calling it for all of our partial views.
public class PartialAction<T>
where T : class
{
private readonly IPartialActionExtensionGraph<T> _graph;
public PartialAction(IPartialActionExtensionGraph<T> graph)
{
_graph = graph;
}
public T Execute(T input)
{
return _graph.Modify(input);
}
}
As you can see, we have a dependency on IPartialActionExtensionGraph
. That's a simple interface that has a single method, Modify
. Since we're going to be calling Modify
on our graph
the same way as we call Modify
on a single action extension, they share a common interface.
public interface IPartialActionExtensionGraph<T> : IPartialActionExtension<T>
where T : class
{
}
public interface IPartialActionExtension<T>
where T : class
{
T Modify(T model);
}
Now we're going to create a generic implementation for the action extension graph. We'll want to call the Modify
method on all of the action extensions that close the interface with the same type. We're going to use our IoC container to keep track of all our implementations, in this case we're using StructureMap, but you could probably use this with any dependency injection tool.
public class PartialActionExtensionGraph<T> : IPartialActionExtensionGraph<T>
where T : class
{
private readonly IContainer _container;
public PartialActionExtensionGraph(IContainer container)
{
_container = container;
}
public T Modify(T model)
{
_container.GetAllInstances<IPartialActionExtension<T>>()
.Each(extension => model = extension.Modify(model));
return model;
}
}
To load up StructureMap with all our action extensions, we'll want to use this registry & registry convention while we bootstrap StructureMap. This awesome piece of code is from Josh, so please go thank him. It will search through our entire assembly and find any types that close the interface IPartialActionExtension<>
, that is not an interface, and is not abstract. Please note that the Closes
and FindInterfaceThatCloses
extension methods are part of FubuCore.
public class PartialActionExtensionsRegistry : Registry
{
public PartialActionExtensionsRegistry()
{
Scan(x =>
{
x.TheCallingAssembly();
x.Include(type => type.Closes(typeof(IPartialActionExtension<>)));
x.Exclude(type => type.IsInterface);
x.Exclude(type => type.IsAbstract);
x.With(new PartialActionExtensionsConvention());
});
}
}
public class PartialActionExtensionsConvention : IRegistrationConvention
{
public void Process(Type type, Registry registry)
{
var extensionType = type.FindInterfaceThatCloses(typeof(IPartialActionExtension<>));
if(extensionType == null)
{
return;
}
registry
.For(extensionType)
.Add(type);
}
}
Now that our action is setup, we'll build our model. Until this point, we've been dealing with the plumbing to get the partial views to work. So I'll explain what we're about to write. On our master page, we want to see either a "Login" link if the user has not logged in, yet or a "Logout" link if the user is already logged in. Pretty simple. As you can see from the PartialAction
class, our input and output model will be the same. So we just need to write the model that will be used by the view.
[public class MasterLoginLogoutModel
{
public bool IsAuthenticated { get; set; }
}
]
Note the PartialModel
attribute. This is very important, because this is what we'll use to tell FubuMVC to link it up to the PartialAction
class.
In order for FubuMVC to know what to do, we'll need to setup our behavior chains for the input model that we just built. This is where things get a little more complicated, but we'll go through it step-by-step. We need to create action calls for each of our models, so let's create an action source.
Our action source will be fairly simple. We'll find all our classes that are marked with the PartialModel
attribute, close the PartialAction<>
action with this type, and create an action call for it. Please note that the GetExecuteMethod
method is a Type
extension method which just returns a MethodInfo
instance of the method to call. In this case it will be the Execute
method.
public class PartialActionsSource: IActionSource
{
public IEnumerable<ActionCall> FindActions(TypePool types)
{
return types
.TypesMatching(t => t.HasAttribute<PartialModelAttribute>())
.Select(m =>
{
var actionType = typeof(PartialAction<>).MakeGenericType(m);
return new ActionCall(actionType, actionType.GetExecuteMethod());
});
}
}
We'll create the view real quick, nothing too fancy about it.
<%@ Page Language="C#" CodeBehind="MasterLoginLogout.aspx.cs" Inherits="Project.Web.Shared.Partials.Views.MasterLoginLogout" %>
<%@ Import Namespace="Project.Web.Models.Users" %>
<% if (Model.IsAuthenticated) {%>
<a href="<%=Urls.UrlFor(new UserLogoutGetModel())%>">Logout</a>
<% } else { %>
<a href="<%= Urls.UrlFor(new UserLoginGetModel()) %>">Login</a>
<%} %>
public class MasterLoginLogout : FubuPage<MasterLoginLogoutModel>
{
}
Now, we can call the partial view from our master page very easily. It will look something like this.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="Project.Web.Shared.Master.Site" %>
<%@ Import Namespace="FubuCore" %>
<%@ Import Namespace="Project.Web.Shared.Partials.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Project Master Page</title>
</head>
<body>
<% this.Partial<MasterLoginLogoutModel>(); %>
</body>
</html>
public class Site : FubuMasterPage
{
}
When we call our page, we'll see that it almost works. It's just that it will always say "Login" because we never set the IsAuthenticated
property on our model. To do this, we just want to create an action extension (remember that base interface we made way up there?).
public class MasterLoginLogoutPartialActionExtension : IPartialActionExtension<MasterLoginLogoutModel>
{
private readonly IAuthenticationManager _authenticationManager;
public MasterLoginLogoutPartialActionExtension(IAuthenticationManager authenticationManager)
{
_authenticationManager = authenticationManager;
}
public MasterLoginLogoutModel Modify(MasterLoginLogoutModel model)
{
model.IsAuthenticated = _authenticationManager.LoggedIn;
return model;
}
}
Also, just for completeness, here are the parts of our FubuMVC Registry which pertains to these partials.
public class ProjectFubuRegistry : FubuRegistry
{
public ProjectFubuRegistry()
{
Applies.ToThisAssembly();
Actions.FindWith<PartialActionsSource>();
Views.TryToAttach(findViews =>
{
findViews.by_ViewModel_and_Namespace_and_MethodName();
findViews.by_ViewModel_and_Namespace();
findViews.by_ViewModel();
});
}
}
Step-by-step, what happens when we call the partial from the master page?
- FubuMVC looks for the action call setup to accept that input type.
- FubuMVC then calls the action call and passes a blank model to the
PartialAction
'sExecute
method that was created while bootstrapping. In this case, it's an instance ofPartialAction
, closed withMasterLoginLogoutModel
. - When this instance of
PartialAction
is instantiated, it depends on anIPartialActionExtensionGraph<MasterLoginLogoutModel>
. When we callModify
on this, StructureMap will have already found ourMasterLoginLogoutPartialActionExtension
class (since it implementsIPartialActionExtension
and closes it withMasterLoginLogoutModel
), and we will have a graph with this single action extension. If you had two classes that close this interface withMasterLoginLogoutModel
, then they would both be in the graph. - The
PartialAction
, then callsModify
on the graph, which loops through all of the action extensions and calls theModify
method, resetting the model to the return value for each call. - The model is then returned from the
PartialAction
, and is passed to the view.
Now, whenever we want to create a new partial view, we can just follow these steps:
- Create the model for the view, being sure to mark it with the
PartialModel
attribute. - Create as many implementations of
IPartialActionExtension
as we need (normally only one). Be sure it closes the interface with the model we've just setup. - Create a view that uses the model. FubuMVC will be smart enough to figure out that's the view we want to use, as long as we tell FubuMVC to match views by view models.
It's my understanding that most of this plumbing will be in a future release of FubuMVC. Until then, you can do this, instead. I'll write another post when these features are native to Fubu.