EPiServer: strongly typed layout model without IPageViewModel
Problem
Lot of ASP.NET MVC examples show data sharing between controllers and layouts using dynamic ViewBag or ViewData dictionary. This approach works fine for small application where you do not have a lot of data in a layout, but in a more complex application you want strongly typed model for layout.
EPiServer provided sample site - Alloy which uses IPageViewModel<T> interface which has property Layout of type LayoutModel.
public interface IPageViewModel<out T> where T : SitePageData
{
T CurrentPage { get; }
LayoutModel Layout { get; set; }
IContent Section { get; set; }
}
Then there is a base class for view models - PageViewModel<T> which implements this interface. All your view models now should implement IPageViewModel<T> or inherit from PageViewModel<T>.
Developer usually modifies LayoutModel to add required data to site's layout and then in PageViewContextFactory loads all necessary data into this layout model.
Sometimes it is also required to update layout model from page's controller. In this case sample provides IModifyLayout interface which you use to decorate controller and implement ModifyLayout method which takes layout model as parameter. Then in this method it is possible to update the model. Layout model is injected into controller using PageContextActionFilter global filter which watches for controllers implementing ModifyLayout.
So there are two tasks IPageViewModel<T> does:
- provides stringly typed layout model,
- shares data between page controller and layout.
Then why bother and try something else if it solves these tasks? Because this approach has several important issues.
1. Issue: form posting
When you are creating form and post to controller's action, MVC automatically binds form data to your model. This is great. But it is more complicated when your view model inherits from PageViewModel<T>. Model binder can't bind your model because PageViewModel<T> requires currentPage injected into contructor. So you have to create separate model for posting with same fields and same validation annotations as in view model.
2. Issue: coupling
While project is small this might not be an issue - just inherit all views from PageViewModel<T> and it's fine.
When your project starts to grow and you split your project in separate libraries by features, then all should share some common library with layout model even if feature library does not use it.
Or you might start creating reusable UI libraries with own controllers and view models, then dependency on IPageViewModel<T> becomes important issue. Not all projects share same layout model so it can't be common for all your projects.
And if you want to use some 3rd party UI library, you are stuck. Because 3rd party library is not going to use your layout model.
Solution
ASP.NET MVC has a way to inject objects into your views. You just have to create base class for your views and this base class should inherit from WebViewPage<T>.
public class MyBaseWebViewPage : WebViewPage
{
public string MyProperty { get { return "Injected property"; } }
public override void Execute() { }
}
Then use @inherits keyword in your layouts and/or pages to use newly created base class. All properties and methods in this base class will be available in the view.
@inherits MyBaseWebViewPage
@MyProperty
You are not forced to use the base class in pages. You can use this base view only in layout. So your pages are not coupled to this view base implementation.
EPiServer example
Creating layout model
EPiServer is not much different from raw ASP.NET MVC. So first create layout model for your site and base class for your layout view. You can use EPiServer's Injected class to inject your objects into the view.
public class LayoutModel
{
public string Constant
{
get { return "Layout: constant value"; }
}
}
public class BaseViewPage : WebViewPage
{
public Injected<LayoutModel> LayoutModel { get; set; }
public override void Execute() { }
}
Then inherit your site's layout from BaseViewPage and use layout model's property.
@inherits CmsLayout.Models.Pages.BaseViewPage
<!DOCTYPE html>
<html>
<head>
<title>Cms Layout sample</title>
</head>
<body>
<div>
@LayoutModel.Service.Constant <br />
@RenderBody()
</div>
</body>
</html>
When you run the project, you should see "Layout: constant value" on the page.
Modifying layout from page's controller
First of all let's create mutable property in layout model which we want to modify in the page's controller.
public class LayoutModel
{
public string Constant
{
get { return "Layout: constant value"; }
}
public string Mutable { get; set; }
}
We can inject this model into controller now and change the value of Mutable property.
public class StartPageController : PageController<StartPage>
{
Injected<LayoutModel> LayoutModel { get; set; }
public ActionResult Index(StartPage currentPage)
{
LayoutModel.Service.Mutable = "Layout: mutated from controller";
return View(currentPage);
}
}
And render this property in the layout.
@inherits CmsLayout.Models.Pages.BaseViewPage
<!DOCTYPE html>
<html>
<head>
<title>Cms Layout sample</title>
</head>
<body>
<div>
@LayoutModel.Service.Constant <br />
@LayoutModel.Service.Mutable <br />
@RenderBody()
</div>
</body>
</html>
Also notice that StartPage's view does not depend on anything than StartPage's page type.
@model CmsLayout.Models.Pages.StartPage
Unfortunately after running the application, only constant value get's rendered. The issue is with LayoutModel lifetime in StructureMap container. By default it uses Transient lifetime that it creates new instance each time someone requests it. To fix this issue we have to add StructureMap configuration into project (which should be in each project anyway :) ) and configure LayoutModel's lifetime to HybridHttpOrThreadLocalScoped that it will live for whole request.
For<LayoutModel>()
.HybridHttpOrThreadLocalScoped()
.Use<LayoutModel>();
NOTE This example uses StructureMap 2, but StructureMap 3 requires different configuration.
Now after running application, you should see two messages - "Layout: constant value" and "Layout: mutated from controller".
Full source code for sample is here.
Summary
Creating strongly typed layout model is simple task. You do not need to have lot of infrastructure code to make it work and you can make it decoupled from your pages.