53
MVC Purée Codegard en June 2014

MVC Puree - Approaches to MVC with Umbraco

Embed Size (px)

Citation preview

Page 1: MVC Puree - Approaches to MVC with Umbraco

MVCPurée

Codegarden

June 2014

Page 2: MVC Puree - Approaches to MVC with Umbraco

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: [email protected] / @andybutland

Page 3: MVC Puree - Approaches to MVC with Umbraco

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’”

Page 4: MVC Puree - Approaches to MVC with Umbraco

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

Page 5: MVC Puree - Approaches to MVC with Umbraco

1. MVC best practices

Page 6: MVC Puree - Approaches to MVC with Umbraco

Journey to Umbraco MVC

“Classic”ASP

ASP.NetWebforms

ASP.NetMVC

Umbraco(XSLT)

Umbraco(Razor)

Umbraco(MVC)

Learnings and best practices…

Page 7: MVC Puree - Approaches to MVC with Umbraco

MVC patterns and practices

MODEL

CONTROLLER

VIEW

VIEWMODELS

Simple, strongly typed views, with no domain logic Custom view models

for each view

Mapping from domainmodels to view models

Application componentssupported with unit tests

and composed viadependency injection

COMPONENT

Separation of concerns

Page 8: MVC Puree - Approaches to MVC with Umbraco

2. Using MVC with Umbraco

Page 9: MVC Puree - Approaches to MVC with Umbraco

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

Page 10: MVC Puree - Approaches to MVC with Umbraco

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.

UMBRACODEFAULT

CONTROLLERRequestTEMPLATE/

VIEWPopulates andpasses a standardRenderModel

Within the view we can use the Umbraco helper to:• Access page properties• Query for other nodes• Run Examine searches

Page 11: MVC Puree - Approaches to MVC with Umbraco

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

Page 12: MVC Puree - Approaches to MVC with Umbraco

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.

UMBRACODEFAULT

CONTROLLERRequestTEMPLATE/

VIEWSURFACE

CONTROLLERACTION

PARTIALVIEW

Page 13: MVC Puree - Approaches to MVC with Umbraco

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.

Page 14: MVC Puree - Approaches to MVC with Umbraco

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…

CUSTOMCONTROLLERRequest

TEMPLATE/VIEWPopulates and

passes a customview model

Page 15: MVC Puree - Approaches to MVC with Umbraco

Example: route hijackingusing 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

Page 16: MVC Puree - Approaches to MVC with Umbraco

3. Strongly typed views and mapping from

Umbraco data

Page 17: MVC Puree - Approaches to MVC with Umbraco

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.

Page 18: MVC Puree - Approaches to MVC with Umbraco

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

Page 19: MVC Puree - Approaches to MVC with Umbraco

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);}

Page 20: MVC Puree - Approaches to MVC with Umbraco

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

Page 21: MVC Puree - Approaches to MVC with Umbraco

Example: mapping using conventionspublic 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);}

Page 22: MVC Puree - Approaches to MVC with Umbraco

Example: overriding conventionsmapper.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

Page 23: MVC Puree - Approaches to MVC with Umbraco

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);

Page 24: MVC Puree - Approaches to MVC with Umbraco

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;}

Page 25: MVC Puree - Approaches to MVC with Umbraco

4. Dependency injection

Page 26: MVC Puree - Approaches to MVC with Umbraco

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

Page 27: MVC Puree - Approaches to MVC with Umbraco

Example: injecting services to a controllerpublic 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.

Page 28: MVC Puree - Approaches to MVC with Umbraco

Example: integrating Ninject with UmbracoPM> 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

Page 29: MVC Puree - Approaches to MVC with Umbraco

5. Unit testing

Page 30: MVC Puree - Approaches to MVC with Umbraco

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 UNDERTEST

DEPENDENTCLASS

DEPENDENTCLASS

… which allows us to we replace them with mocks or stubs we control

Dependencies are referenced through interfaces…

Page 31: MVC Puree - Approaches to MVC with Umbraco

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

Page 32: MVC Puree - Approaches to MVC with Umbraco

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.

Page 33: MVC Puree - Approaches to MVC with Umbraco

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

Page 34: MVC Puree - Approaches to MVC with Umbraco

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

Page 35: MVC Puree - Approaches to MVC with Umbraco

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 = "[email protected]", Comment = "Can I test this?", };

// Act var result = controller.CreateComment(model);

// Assert Assert.IsNotNull(result);}

Will fail, with null reference for UmbracoContext

Page 36: MVC Puree - Approaches to MVC with Umbraco

Example: testing a “command handler” classpublic 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…

Page 37: MVC Puree - Approaches to MVC with Umbraco

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.

Page 38: MVC Puree - Approaches to MVC with Umbraco

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 = "[email protected]", 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.

Page 39: MVC Puree - Approaches to MVC with Umbraco

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

Page 40: MVC Puree - Approaches to MVC with Umbraco

Example: base test classes[TestFixture][DatabaseTestBehavior(DatabaseBehavior.NoDatabasePerFixture)]public class BlogPostSurfaceControllerTests : BaseRoutingTest{ [Test] public void ExampleTest() { var controller = GetController(); var model = new CommentViewModel { Email = "[email protected]", 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.

Page 41: MVC Puree - Approaches to MVC with Umbraco

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 fakesassembly for any dll

• And at runtimereplace a method’simplementation with one of our own

• VS.Net Premium or Ultimate only though

Page 42: MVC Puree - Approaches to MVC with Umbraco

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.

Page 43: MVC Puree - Approaches to MVC with Umbraco

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.

Page 44: MVC Puree - Approaches to MVC with Umbraco

6. Cleaning up views

Page 45: MVC Puree - Approaches to MVC with Umbraco

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

Page 46: MVC Puree - Approaches to MVC with Umbraco

Example: custom view model for partialpublic 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 [email protected]("_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

Page 47: MVC Puree - Approaches to MVC with Umbraco

Example: view models with inheritancepublic abstract class BaseViewModel{ public string Heading { get; set; } public string Standfirst { get; set; }}public class HomePageViewModel : BaseViewModel { }public class ContentPageViewModel : BaseViewModel { }

...

@model [email protected]("_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.

Page 48: MVC Puree - Approaches to MVC with Umbraco

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…

Page 49: MVC Puree - Approaches to MVC with Umbraco

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.

Page 50: MVC Puree - Approaches to MVC with Umbraco

7. Wrap up and Q &A

Page 51: MVC Puree - Approaches to MVC with Umbraco

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

Page 52: MVC Puree - Approaches to MVC with Umbraco

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

Page 53: MVC Puree - Approaches to MVC with Umbraco