Date post: | 14-Apr-2017 |
Category: |
Technology |
Upload: | andy-butland |
View: | 1,335 times |
Download: | 0 times |
MVCPurée
Codegarden
June 2014
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
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’”
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
1. MVC best practices
Journey to Umbraco MVC
“Classic”ASP
ASP.NetWebforms
ASP.NetMVC
Umbraco(XSLT)
Umbraco(Razor)
Umbraco(MVC)
Learnings and best practices…
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
2. Using 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
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
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.
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
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.
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
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
3. Strongly typed views and mapping from
Umbraco data
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.
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
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);}
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
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);}
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
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);
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;}
4. Dependency injection
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
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.
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
5. Unit testing
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…
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
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.
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
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
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
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…
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.
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.
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
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.
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
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.
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.
6. Cleaning up views
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
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
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.
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…
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.
7. Wrap up and Q &A
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
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