This document discusses best practices for using the Model-View-Controller (MVC) pattern with Umbraco, an open source content management system (CMS). It covers topics like:
1. Using MVC patterns like separating logic from views and unit testing components with dependency injection.
2. Techniques for implementing MVC in Umbraco like using surface controllers, view models, and the Umbraco mapper package.
3. Tips for unit testing components that interface with Umbraco like mocking dependencies and using the Umbraco base test classes.
2. Just to introduce myself…
• I’m Andy Butland
• I work for Zone, a digital agency where I head up the
.NET development team in London
• We develop web sites and applications primarily using
ASP.Net MVC and Umbraco
• Blog (sporadically) at http://web-matters.blogspot.it/
• Find here a copy of slides and links to various resources
• Contact: abutland73@gmail.com / @andybutland
3. And what are we talking about… “MVC
Purée”?
• We’ll be discussing best practices with using MVC in
Umbraco:
• “For the session title, how about ‘MVC purist’?”
• “Hmm, don’t like the sound of that… makes me sound too
much of a pedant, and we all have to be pragmatic too.”
• “OK… we’ll go with ‘MVC purée’”
4. Contents
1
2
3
6
4
MVC best practices 5
7
Using MVC with Umbraco
Strongly typed views and
mapping from Umbraco
data
Cleaning up views
Dependency injection
Unit testing
Wrap up and Q & A
6. Journey to Umbraco MVC
“Classic”
ASP
ASP.Net
Webforms
ASP.Net
MVC
Umbraco
(XSLT)
Umbraco
(Razor)
Umbraco
(MVC)
Learnings and best practices…
7. MVC patterns and practices
MODEL
CONTROLLER
VIEW
VIEW
MODELS
Simple, strongly typed views,
with no domain logic Custom view models
for each view
Mapping from domain
models to view models
Application components
supported with unit tests
and composed via
dependency injection
COMPONEN
T
Separation of concerns
9. MVC techniques with Umbraco
• Since version 4.10 we’ve been able to use MVC as well
as traditional Web Forms for rendering Umbraco
templates.
• MVC rendering has been implemented in a flexible way,
giving a lot of scope to the developer in building their
application.
• Logic, querying and data access in the views
• Using surface controllers actions with partial views
• Hijacking routes
10. Logic and querying in the views
• This technique is most similar to that used in traditional
Umbraco templating and can be used be all developers,
not just those using Visual Studio.
• It’s not ideal though from a more purist MVC
perspective.
UMBRACO
DEFAULT
CONTROLLERRequest
TEMPLATE/
VIEWPopulates and
passes a standard
RenderModel
Within the view we can use
the Umbraco helper to:
• Access page properties
• Query for other nodes
• Run Examine searches
11. Example: using the Umbraco helper in our
view@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
<h1>@Model.Content.GetPropertyValue("heading")</h1>
@{
var rootNode = Umbraco.TypedContentAtRoot().First();
var categoryNodes = rootNode
.Descendants("CategoryFolder")
.First()
.Children;
foreach (var item in categoryNodes)
{
<div>@item.GetPropertyValue("title")</div>
}
}
Property access
not obvious for
front-end
developers less
familiar with
Umbraco APIs.
The Umbraco
helper provides
access to
functions that
arguably should
not be the
concern of the
view.
12. Surface controller actions and partials
• We can prepare and pass our own view model by
making an @Html.Action call from within our template,
to a surface controller that returns a partial.
• It’s downside is a more complex request flow, and the
necessity of creating two templates.
UMBRACO
DEFAULT
CONTROLLERRequest
TEMPLATE/
VIEW
SURFACE
CONTROLLER
ACTION
PARTIAL
VIEW
13. Example: using a surface controller and
partial@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@{
Layout = "_Layout.cshtml";
}
@Html.Action("Register", "AccountSurface")
[ChildActionOnly]
public PartialViewResult Register()
{
var vm = new RegisterViewModel();
PopulateRegisterViewModel(vm);
return PartialView("Register", vm);
}
@model RegisterViewModel
<h1>@Model.Heading</h1>
Our Umbraco template
view calls out to a
surface controller action
method…
… which populates a
custom view model and
passes this to a partial
view…
… that can benefit from
strong typing.
14. Route hijacking
• We can intercept the request with our own controller,
populate a custom view model and pass it to the
strongly typed view.
• We now have a very clean view with little logic, and
proper separation of concerns from an MVC
perspective.
• There is a little more work to do from a developer
perspective, but we can mitigate that…
CUSTOM
CONTROLLER
Request
TEMPLATE/
VIEWPopulates and
passes a custom
view model
15. Example: route hijacking
using System.Web.Mvc;
using Umbraco.Web.Mvc;
public class EventPageController : RenderMvcController
{
public ActionResult EventPage()
{
// Populate view model and
// render view
}
}
The controller name
must match the
document type alias,
e.g. EventPage
The action method
name must match the
template name
BETTER STILL… see Hybrid Framework method of
inheriting from SurfaceController and
implementing IRenderMvcController
17. Custom view model
• We can create a simple POCO class to represent our
view
• In most cases will represent a single document type
• However often we’ll want to pull in data from other nodes,
or even other data sources
• We’ll map the Umbraco content to our view model
• Our view can reference that as it’s @model
• It’s now simply displaying properties and collections from
the view model, with no business logic or data access
• Actually need have no reference to Umbraco at all…
• …unless you want it of course. You can still inherit from
the Umbraco base page if you need to.
18. Example: custom view model (definition)
public class NewsLandingPageViewModel
{
public string Heading { get; set; }
public IHtmlString BodyText { get; set; }
public IList<NewsItem> LatestNewsItems { get; set; }
}
Directly mapped
from fields on the
document type
Generated from a
node query
19. Example: custom view model (mapping)
public ActionResult NewsLandingPage()
{
var vm = new NewsLandingPageViewModel
{
Heading = CurrentPage.GetPropertyValue<string>("heading"),
BodyText = CurrentPage.GetPropertyValue<IHtmlString>("bodyText"),
LatestNewsItems = CurrentPage.Descendants("NewsItem")
.OrderByDescending(x => DateTime.Parse(
x.GetPropertyValue<string>(“publicationDate")))
.Take(10)
.Select(x => new NewsItem
{
Heading = CurrentPage.GetPropertyValue<string>("heading"),
PublicationDate = DateTime.Parse(
x.GetPropertyValue<string>("publicationDate"))),
})
.ToList(),
};
return View("NewsLandingPage", vm);
}
20. Using the “Umbraco Mapper” package
• The package has been created to streamline the
mapping of Umbraco content to view models
• Convention based mapping from single instances and
collections of IPublishedContent
• Ability to override those conventions for particular
properties as required
• Mapping from other sources such as XML and JSON
• Supply of custom mapping methods for handling custom
types
21. Example: mapping using conventions
public ActionResult NewsLandingPage()
{
var vm = new NewsLandingPageViewModel();
var latestNewsNodes = CurrentPage.Descendants("NewsItem")
.OrderByDescending(x => DateTime.Parse(
x.GetPropertyValue<string>(“publicationDate")))
.Take(10);
var mapper = new UmbracoMapper();
mapper.Map(CurrentPage, vm)
.MapCollection(latestNewsNodes, vm.LatestNewsItems);
return View("NewsLandingPage", vm);
}
22. Example: overriding conventions
mapper.Map(CurrentPage, vm, new Dictionary<string,PropertyMapping>
{
{
"Copy", new PropertyMapping
{
SourceProperty = "bodyText",
}
}
})
.MapCollection(latestNewsNodes, vm.LatestNewsItems,
new Dictionary<string,PropertyMapping>
{
{
"Category", new PropertyMapping
{
SourceProperty = "Name",
LevelsAbove = 1,
}
}
});
Maps from property
with different name
Maps from node at
a higher level in the
tree.
COMING SOON… attribute based property mappings
23. Example: a custom mapping (1)
public class GeoCoordinate
{
public decimal Longitude { get; set; }
public decimal Latitude { get; set; }
public int Zoom { get; set; }
}
...
var mapper = new UmbracoMapper();
mapper.AddCustomMapping(typeof(GeoCoordinate).FullName,
CustomMappings.MapGeoCoordinate);
24. Example: a custom mapping (2)
public static object MapGeoCoordinate(IUmbracoMapper mapper,
IPublishedContent contentToMapFrom, string propName, bool isRecursive)
{
var propertyValueAsCsv = contentToMapFrom
.GetPropertyValue<string>(propName, isRecursive, null);
if (!string.IsNullOrEmpty(propertyValueAsCsv))
{
var parts = propertyValueAsCsv.Split(',');
if (parts != null && parts.Length == 3)
{
return new GeoCoordinate
{
Latitude = decimal.Parse(parts[0]),
Longitude = decimal.Parse(parts[1]),
Zoom = int.Parse(parts[2]),
};
}
}
return null;
}
26. Dependency injection: what and why?
• By injecting our dependencies to a class, rather than
“newing” them up within one, we:
• Program to interfaces – improving the testability of our
code
• Reduce coupling
• Develop components with single responsibilities
• An IoC container can then help us with the instantiation
of the concrete classes at runtime
27. Example: injecting services to a controller
public class HomePageController : BaseController
{
private readonly IDataService _dataService;
public HomePageController(IDataService dataService)
{
_dataService = dataService;
}
public ActionResult HomePage()
{
// Do something with the data service
var data = _dataService.GetData();
...
}
}
An instance of
IDataService - as well as
any dependencies it may
have - is injected into the
controller’s constructor at
run-time.
28. Example: integrating Ninject with Umbraco
PM> Install-Package Ninject.MVC3 Install Ninject from NuGet
Creates a file NinjectWebCommon.cs in App_Start
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IDataService>().To<MyDataService>();
}
Any time a component “requests”
an IDataService, they’ll get a
concrete DataService
30. Isolating our unit under test from
dependencies• When unit testing, we aim to confirm the function of a
particular method or class, by replacing any
dependencies it has with versions that are under the
control of our tests
• We avoid brittle data or slow running processes
• We isolate our tests to just the small piece being
examined
UNIT UNDER
TEST
DEPENDENT
CLASS
DEPENDENT
CLASS
… which allows
us to we replace
them with mocks
or stubs we
control
Dependencies
are referenced
through
interfaces…
31. Example: using mocks (with Moq)
[TestMethod]
public void WebServiceTests_SuccessResponse_ReturnsStatusAndMessage()
{
// Arrange
var service = new WebServiceWrapper(MockHttpClient(),
"http://www.example.com/");
// Act
var result = service.Request("exchange-rates",
ResponseContentType.JSON).Result;
// Assert
Assert.IsTrue(result.Success);
Assert.IsNotNull(result.Response);
Assert.IsNull(result.ErrorMessage);
}
Within the test,
we are avoiding
calling the
external web
service directly
by mocking the
HTTP call
32. Example: using mocks (with Moq) (2)
private static IHttpClient MockHttpClient()
{
var mock = new Mock<IHttpClient>();
mock.Setup(x => x.GetAsync(It.IsAny<string>()))
.Returns(Task<HttpResponseMessage>.Factory.StartNew(() =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(@"{ 'rates': [{
'currencyCode': 'EUR',
'rate': 1.24
}]}";);
return response;
}));
mock.Setup(x => x.GetAsync(It.Is<string>(y => y.Contains("invalid"))))
.Returns(Task<HttpResponseMessage>.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}));
return mock.Object;
}
At runtime we
create our own
“HttpClient” by
mocking the
interface and
return fixed
responses.
33. Unit testing with Umbraco
• We can use various techniques to control our
dependencies, e.g. mocks and stubs
• Umbraco doesn’t make it particularly easy…
• Problems with unit testing surface controllers
• Issues with extension and static methods
• … but be no means impossible
• Avoid the problem - move logic into a separate class,
leaving a simple controller of little value to test
• Utilise the base test classes
• Look into MS Fakes
34. Example: unit testing a surface controller
[HttpPost]
public ActionResult CreateComment(CommentViewModel model)
{
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
TempData.Add("CustomMessage", "Thanks for your comment.");
return RedirectToCurrentUmbracoPage();
}
If validation fails,
should return to
view.
If successful,
should have
value in
TempData and
redirect
35. Example: unit testing a surface controller (2)
[TestMethod]
public void CreateComment_WithValidComment_RedirectsWithMessage()
{
// Arrange
var controller = new BlogPostSurfaceController();
var model = new CommentViewModel
{
Name = "Fred",
Email = "fred@freddie.com",
Comment = "Can I test this?",
};
// Act
var result = controller.CreateComment(model);
// Assert
Assert.IsNotNull(result);
}
Will fail, with null
reference for
UmbracoContext
36. Example: testing a “command handler” class
public class BlogPostSurfaceControllerCommandHandler
{
public ModelStateDictionary ModelState { get; set; }
public TempDataDictionary TempData { get; set; }
public bool HandleCreateComment(CommentViewModel model)
{
if (!ModelState.IsValid) {
return false;
}
TempData.Add("CustomMessage", "Thanks for your comment.");
return true;
}
}
Logic moved to class
with no Umbraco
dependency…
37. Example: testing a “command handler” class
(2)public class BlogPostSurfaceController : SurfaceController
{
BlogPostSurfaceControllerCommandHandler _commandHandler;
public BlogPostSurfaceController()
{
_commandHandler = new BlogPostSurfaceControllerCommandHandler();
_commandHandler.ModelState = ModelState;
_commandHandler.TempData = TempData;
}
[HttpPost]
public ActionResult CreateCommentWithHandler(CommentViewModel model)
{
if (!_commandHandler.HandleCreateComment(model)) {
return CurrentUmbracoPage();
}
return RedirectToCurrentUmbracoPage();
}
}
… which is referenced
in the surface
controller…
… leaving a thin
controller method, with
little value for testing.
38. Example: testing a “command handler” class
(3)[TestMethod]
public void CreateComment_WithValidComment_ReturnsTrueWithMessage()
{
// Arrange
var handler = new BlogPostSurfaceControllerCommandHandler();
handler.ModelState = new ModelStateDictionary();
handler.TempData = new TempDataDictionary();
var model = new CommentViewModel
{
Name = "Fred",
Email = "fred@freddie.com",
Comment = "Can I test this?",
};
// Act
var result = handler.HandleCreateComment(model);
// Assert
Assert.IsTrue(result);
Assert.IsNotNull(handler.TempData["CustomMessage"]);
}
The logic in the
handler though, can
now be tested.
39. Using the Umbraco core base test classes
• Allow us to test our surface controller without
modification:
• We need to clone the source code, build and reference
the Umbraco.Tests.dll in our project
• We have to use NUnit
• We then have access to some base classes we can
inherit from, to run tests with the appropriate contexts set
up
• But even then, there are some reflection hoops to jump
through
• Once done though, we can successfully run tests on
Umbraco surface controllers
40. Example: base test classes
[TestFixture]
[DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)]
public class BlogPostSurfaceControllerTests : BaseRoutingTest
{
[Test]
public void ExampleTest()
{
var controller = GetController();
var model = new CommentViewModel {
Email = "fred@freddie.com",
Comment = "Can I test this?",
};
var result = controller.CreateComment(model);
var redirectResult = result as RedirectToUmbracoPageResult;
Assert.IsNotNull(redirectResult);
Assert.AreEqual(1000, redirectResult.PublishedContent.Id);
Assert.IsNotNull(controller.TempData["CustomMessage"]);
}
}
We can test for
specific Umbraco
results and other
controller actions.
Using base class
methods and reflection,
the controller can be
instantiated with
necessary contexts.
The test class inherits
from the Umbraco base
test class.
41. Working around issues with extension
methods
• Some Umbraco methods – particularly on
IPublishedContent – are implemented as extension
methods
• These can’t be mocked or stubbed
• Using MS Fakes
• We can add a fakes
assembly for any dll
• And at runtime
replace a method’s
implementation with one of our own
• VS.Net Premium or Ultimate only though
42. Example: can’t mock IPublishedContent
methodprivate static IPublishedContent MockIPublishedContent()
{
var mock = new Mock<IPublishedContent>();
mock.Setup(x => x.Id).Returns(1000);
mock.Setup(x => x.Name).Returns("Test content");
mock.Setup(x => x.CreatorName).Returns("A.N. Editor");
// This won’t work...
mock.Setup(x => x.GetPropertyValue(It.IsAny<string>()))
.Returns((string alias) => MockIPublishedContentProperty(alias));
return mock.Object;
}
GetPropertyValue() is
an extension method,
which can’t be mocked.
43. Example: using Microsoft Fakes
[TestMethod]
public void MapFromIPublishedContent_MapsCustomProperties()
{
using (ShimsContext.Create())
{
var model = new SimpleViewModel();
var mapper = GetMapper();
var content = new StubPublishedContent();
Umbraco.Web.Fakes.ShimPublishedContentExtensions
.GetPropertyValueIPublishedContentStringBoolean =
(doc, alias, recursive) => {
switch (alias) {
case "bodyText": return "This is the body text";
default: return string.Empty;
}
};
mapper.Map(content, model);
Assert.AreEqual("This is the body text", model.BodyText);
}
}
We replace the
method
implementation with
our own at runtime.
45. Strongly typed partials
• Often our HTML will contain blocks of similar layout that
are repeated on multiple pages
• We can use @Html.Partial to “DRY” up our views
• By having our view models implement multiple small
interfaces, or by using inheritance, we can strongly
type our partial but avoid instantiating objects for each
one
46. Example: custom view model for partial
public class HeroSectionViewModel
{
public string Heading { get; set; }
public string Standfirst { get; set; }
}
@model CodegardenSamples.Models.HeroSectionViewModel
<h1>@Model.Heading</h1>
<p>@Model.Standfirst</p>
...
@model CodegardenSamples.Models.HomePageViewModel
@Html.Partial("_HeroSection“, new HeroSectionViewModel
{
Heading = Model.Heading,
Standfirst = Model.Heading
})
We can strongly type
our partials just as we
can full page views
BUT… we have to handle instantiating this
partial's model in our view
47. Example: view models with inheritance
public abstract class BaseViewModel
{
public string Heading { get; set; }
public string Standfirst { get; set; }
}
public class HomePageViewModel : BaseViewModel { }
public class ContentPageViewModel : BaseViewModel { }
...
@model CodegardenSamples.Models.HomePageViewModel
@Html.Partial("_HeroSection")
...
@model CodegardenSamples.Models.BaseViewModel
<h1>@Model.Heading</h1>
<p>@Model.Standfirst</p>
As our view models
inherit from a base
class…
… a strongly typed
partial can be created
that can reference the
model of the parent
page.
BUT… using inheritance in this way can be
restrictive – we can only inherit from one base.
48. Example: view models with interfaces (1)
public interface IHeroSection
{
string Heading { get; }
string Standfirst { get; }
}
public interface ISideBar
{
string SideBarTitle { get; }
string SideBarIntro { get; }
}
public class HomePageViewModel : IHeroSection, ISideBar
{
public string Heading { get; set; }
public string Standfirst { get; set; }
public string SideBarTitle { get; set; }
public string SideBarIntro { get; set; }
}
Having our view model implement
multiple small interfaces…
49. Example: view models with interfaces (2)
@model CodegardenSamples.Models.HomePageViewModel
@Html.Partial("_HeroSection")
@Html.Partial("_SideBar")
...
@model CodegardenSamples.Models.IHeroSection
<h1>@Model.Heading</h1>
<p>@Model.Standfirst</p>
...
@model CodegardenSamples.Models.ISideBar
<h2>@Model.SideBarTitle.</h2>
<p>@Model.SideBarIntro</p>
… again means we can reference
the model in a strongly typed
manner in the partials.
51. In summary
• No one true way to build an Umbraco site… pick
what works for you and your team
• If you like the “purist” MVC approach, you can apply
these best practices without too much additional
effort, and still be “pragmatic” about delivery to your
clients
52. Lastly, some thanks…
• Neil, Ali, Rob, Raffaele, Nnamdi, Ollie and the rest of my
colleagues at Zone
• Numerous discussions, questions and advice as we’ve
evolved techniques and technologies over the years
• Anthony, Darren, Ismail, Jeavon, Jeroen, Shannon,
Warren and many others
• Blogs, forum threads and other community contributions
that have influenced the thinking behind our work and this
presentation