Getting real business value from strongly typed models in Umbraco
Unmasking "Code First"
There was a thread on twitter the other day where someone had asked someone about "Code First" in Umbraco. "Code First" has been something like a holy grail for many developers, including me. The thread ended with a conclusion that nobody wants it. But that discussion reminded me of something. how my own attempts at "round-trip engineering" document types in Umbraco just faded. I even made a tool that does it, but neither did it get many users, nor did I actually use it for "Code First" myself. What I used my tool for was generating strongly typed models for Umbraco, and all the benefits it brings.
With Umbraco 7.4, Umbraco ModelsBuilder is now built-in to Umbraco.
It does what my tool does, and is now available to all Umbraco developers with the flip of an appSetting.
If you'd like to tag along with this article, you should make sure it's set to PureLive
mode.
So what does generated strongly typed models enable us developers (and designers) to do? Ignoring the elevator answer "IntelliSense and compiler errors when we make a typo". Why would you use this, unless you're one of those guys who come back from conventions full of "good ideas"? Turns out it opens up a plethora of useful techniques to use and squeeze for value. Business value!
Not only that! By leveraging compositional document types, you actually may do a kind of "Code First".
There's only thing associated with "Code First" that you won't get. It is something that updates the document models in Umbraco, based on your code.
But why would you want that? There's an excellent UI that lets you annotate and describe all the fields for the editors. You also get to preview the editor experience. And you get to configure stuff using well crafted tools.
Coding your document types would mean messing around with loads of ugly metadata attributes. In the end your business artifacts would drown in irrelevant data. Not to mention you'd have to remember all the bloody editor aliases and types and everything.
Real value, with a sprinkle of code first on top
So how do we use these techniques, and how do we get value from them?
Some of you are probably familiar with the SOLID programming principles. Leveraging the I and the D is what gets things going with typed models. The I stands for "Interface Segregation" and the D stands for "Dependency Inversion".
The Dependency Inversion principle states that you should depend on abstractions, not on concretions. In the "typed model world" it means that you don't use instances of the generated type Article. You code against an interface IArticle said class implements. You can also one of the compositions the document type is built from.
The Interface Segregation principle states that you should make fine grained interfaces that are client specific. Again, in a "typed model world" this can be applied as "compose your document types of several types, each relevant for a specific use in the site".
Let's have a look at how we can extract value from these patterns. We'll try to apply it to a simple feature most of us have in all our sites.
Prototyping
With all sites, I'm sure we promote some content in a more or less generic way. While prototyping and wireframing we sketch it. We use static HTML, PowerPoint or some other tool.
What if we could already prepare a Razor file that would still work when the site is live? Before we even set up the Umbraco site. With or without Umbraco, you can quickly create what you need to get started. Just to really prove the point that we're prototyping here, let's just start with a view:
// ~/Views/Partials/Promotion.cshtml
<div class="promotion">
<a href="/promoted-content">
<img src="/media/1001/fancy-picture.jpg" />
<h2>Lorem Ipsum</h2>
<p>More lorem ipsum with sugar on top.</p>
</a>
</div>
So that's more or less what we've imagined we're gonna use for promotions for now.
Maybe we'll add a col-md-x
later, but for now, the HTML is good.
Of course it can't stay like that. We need to show some Umbraco data.
So we need a model to bind to. It could be an IPublishedContent, or it could be a dynamic object like before.
However, the markup would prabably be ugly, and either way you'd need to remember all the aliases.
I'll just remind you before we continue:
<h2>@(Model.Content.GetPropertyValue<IHtmlString>("summray"))</h2>
We need something to help us out. So we create an interface:
// ~/Models/IPromotable.cs
namespace YourWeb.Models
{
public interface IPromotable
{
string Title { get; }
object Image { get; }
IHtmlString Summary { get; }
string Url { get; }
}
}
Now we can revisit the view and use some @ markup without getting complaints from our IDE. If you type this, you'll even get help along the way:
// ~/Views/Partials/Promotion.cshtml
@model YourWeb.Models.IPromotable
<div class="promotion">
<a href="@Model.Url">
<img src="@Model.Image" />
<h2>@Model.Title</h2>
<p>@Model.Summary</p>
</a>
</div>
Now it's actually in a form where it doesn't have to change at all until the requirements actually change.
But we haven't viewed it yet. Let's do that too without bringing Umbraco into the picture.
We'll create a simple controller:
// ~/Controllers/PrototypingController.cs
public class PrototypeController : Controller
{
public ActionResult Promotion()
{
return View(
"~/Views/Partials/Promotion.cshtml",
new Promotable
{
Title = "Here's a fake promotable",
Image = "/media/1001/fancy.jpg",
Summary = new HtmlString("<p>Fancy markup</p>"),
Url = "/fancy-url"
}
);
}
class Promotable : IPromotable
{
public string Title { get; set; }
public object Image { get; set; }
public IHtmlString Summary { get; set; }
public string Url { get; set; }
}
}
I'll let the simplicity of the code speak for itself.
To actually get to run and view this, we just need one more step.
Depending on whether you've installed Umbraco, used an MVC template or have a blank project, we need to get the route configured.
I say we bring Umbraco into the picture now. We can still prototype peacefully alongside it.
Above or below the PrototypeController
, just add a simple ApplicationEventHandler
:
public class PrototypeRouting : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
RouteTable.Routes.MapRoute("prototyping", "prototype/{action}", new { controller = "Prototype" });
}
}
Now build, and navigate to:
http://localhost:nnnn/prototype/promotion
You should see something like:
So we've got a live Razor view with a server-side model. This is a simple sample, but imagine modifying or adding to the model. Imagine using logic in your view (or controller) to test different behavior.
Now let's boot up Umbraco and add some real content. We'll create a re-usable composition type called Promotion to match our partial.
Name | Property Editor |
---|---|
Title | Textstring |
Image | Image picker |
Summary | Rich text editor |
Then we'll create a promotable Article type to use as our first real content type. Make it usable on root for our current purposes. Add Promotable as a composition.
Name | Property Editor |
---|---|
Body | Rich text editor |
Go ahead and create one article while you're at it. It should get the URL "/".
Now if you go back to your project and have a look in the ~/App_Data/Models folder,
you'll see that there's a few generated files there.
You've got all.generated.cs
, models.generated.cs
and models.hash
.
Go ahead and open the models.generated.cs
file.
This code is actually being compiled at runtime and loaded into your application.
Now look closer at the (hopefully) top type. It's a partial interface called IPromotable
.
Does it look familiar?
Add a file to the ~/App_Data/Models folder:
namespace Umbraco.Web.PublishedContentModels
{
public partial interface IPromotable : YourWeb.Models.IPromotable
{
}
}
Now go over to the Article.cshtml
template Umbraco generated for you in the ~/Views
folder.
Add a line to it:
@inherits UmbracoTemplatePage<ContentModels.Article>
@using ContentModels = Umbraco.Web.PublishedContentModels;
@{
Layout = null;
}
@Html.Partial("Promotion", Model.Content) // < this one
Don't even care to build. Navigate to http://localhost:nnnn.
You should see something similar to this:
Seem familiar? It's our prototype view! Unchanged! And it's working with real Umbraco content!
( If you've gotten this far you're probably swearing at me for putting an integer in the image source attribute. To the best of my knowledge, there's a lack of good built-in property value converters for images. I'll leave it up to you as an excercise to figure out how to expose a URL. )
We showed the promotion on the content itself now, but in the real world, we'd probably be using it from the front-page or some area node in our web. Well, there's not much to that. If you'd like to show anything that has the composition Promotable now, you'll just query for it:
@Html.Partial("promotion", Umbraco.TypedContent(Model.Content.PromotedId))
You might have to check for the type, though, but let's not think about that yet.
Re-using our logic
So how about keeping this HTML snippet and promotional logic around for the next web that needs promoted content?
We'll need to move the code out of the site itself. (I'll assume you're using Visual Studio from now)
For now, create a new Class Library project in your solution. Name it something nice, personal and re-usable.
You'll also have to add a reference to System.Web.Mvc.
Move the IPromotable interface over to that project and adjust its namespace.
namespace YourReusableLibrary
{
public interface IPromotable
{
string Title { get; }
object Image { get; }
IHtmlString Summary { get; }
string Url { get; }
}
}
Let's also be so naive to believe we'll never change the markup of a promotion.
Create another class in the library called PromotableExtensions
:
public static class PromotableExtensions
{
public static IHtmlString Promotion(this IPromotable promotable)
{
return String.Format(@"
<div class=""promotion"">
<a href=""{0}"">
<img src=""{4}"" />
<h2>{2}</h2>
<p>{3}</p>
</a>
</div>
",
promotable.Url,
promotable.Image,
promotable.Title,
promotable.Summary
);
}
}
Let's go back to our partial again. It can now be modified as follows:
@model YourWeb.Models.IPromotable
@using YourReusableLibrary
@this.Promotion()
That's it! I'd say we'll just scrap it and revisit the article view:
@inherits UmbracoTemplatePage<ContentModels.Article>
@using ContentModels = Umbraco.Web.PublishedContentModels;
@using YourReusableLibrary
@{
Layout = null;
}
@this.Promotion()
What? Right! We need to do it from our grid-editor view, property editor or whatever:
@using YourReusableLibrary
@(((IPromotable)Umbraco.TypedContent(Model.Content.PromotedItemId)).Promotion())
Bah, it's to verbose. Back to PromotableExtensions
and add:
public static IHtmlString Pomotion(IPublishedContent candidate)
{
var promotable = candidate as IPromotable;
if (promotable != null)
return promotable.Promotion();
return new HtmlString("");
}
And our view:
@using YourReusableLibrary
@Umbraco.TypedContent(Model.Content.ProbablyAPromotedItemId).Promotion()
Now we have an extension that assumes we have a composition called Promotable.
It assumes said promotable has a title, an image and a summary.
If we call .Promotion()
on any content, it'll return a nice promotion snippet if it's promotable, or blank if not.
All we have to do is add the tiny partial in ~/App_Data/Models
.
We can re-use our logic in any site.
We made the whole thing in a "code-first" sense, and we never have to create a promotional HTML snippet again.
Now that's what I call value for coding!
Applying to "real business logic"
The example in this article is dead simple. You won't save hours with it.
However, the principles it demonstrates can be applied to almost any use-case.
Coupled with property value converters and well thought through compositional models,
you create rich code that clearly states it's purpose. As well as code that is re-usable and simple.
A final point that we haven't touched upon, but is at least as valuable, is unit testing.
All the code we've written for promotional content is completely testable. It has no dependencies,
except MVC for the IHtmlString, but that's also a solvable issue using the exact same principles.
With that, happy modeling, and happy "code-firsting". :)