While working on a current DD4T project, I encountered a requirement to implement content management of a small section of copy in an otherwise application-focused, "pure MVC" area of the site. While DD4T is an increasingly promising and highly flexible framework for delivering Tridion content via the medium of an ASP.NET MVC web application, there is not - as yet - a clearly-defined convention for approaching this kind of problem.
This blog post will seek to address this issue by proposing a convention for injecting Tridion content into pages in such a way as to ensure the integrity of your statically-routed content, while allowing content editors to customise pages, without disruption, in the familiar way. It's assumed that you have an existing DD4T implementation and an
IPageFactory registered with the MVC dependency resolver.
What's the point?
Suppose that your web application has a number of routes configured to divert requests away from your default page controller to an independent, application-driven (or legacy) part of the site; this may be, for example, a user profile screen with some introductory copy.
If a requirement to content manage part of this page emerges, one approach would be to implement customisable labels (as Rob discusses here), which can be published from Tridion, and render these out on the page. This approach is ideal for short snippets of plain text; but, what if your content editors want more power?
Another approach would be to refactor the various parts of each page into partial views and create Component Templates to allow this entire section of the site to be published from Tridion. This is an excellent solution in a situation where your site has many small, independent pieces of functionality (such as promotional forms), which will benefit from being easily placed on multiple content pages; however, in pages primarily focused around dynamic, application-driven content, this approach comes with a great deal of unwarranted design and development overhead and a considerable CMS footprint in terms of "single-use" Components, Templates and, potentially, Schemas.
In addition, exposing entire pages to a CMS comes with the risk that mission-critical content may be inadvertently removed or manipulated - it isn't difficult to envisage a scenario in which part of an important multi-page process is moved (or removed) from where it should be by an unwitting CMS user!
There must, surely, be a clean, simple middle-ground approach to allow flexible customisation of page content without adding a hard and fast dependency on the CMS?
So, what's the solution?
The essence of this approach is to retrieve Tridion pages with routes corresponding to those of your MVC controller actions - effectively manually doing what your
PageController would do for you if you hadn't diverted the request in the first place - and render this content in the appropriate place as if it were ordinary Tridion content. If our MVC action exists at
/users/profile, we want to retrieve the page in Tridion that corresponds to that route.
The first thing to do, then, is generate the appropriate URL. The important consideration here is that we need to exclude any dynamic route values - such as action method parameters - from the URL we are requesting. For example, if our user requests the URL
/users/profile/001, we'll need to retrieve the Tridion page corresponding to the route
One of the more helpful features of MVC's built-in URL helper is that it will automatically insert parameters from the current request's
RouteData collection when generating URLs. Not so helpful, for our situation, is the fact that there is no easy way to override this behaviour. One (sadly less-than-elegant) solution to this problem is to define a new URL helper that temporarily removes the route values from the current request context, resolves the URL and then recompiles the route values for use in the rest of the request pipeline.
Now that we have a way to build our URL, we can create an action filter that retrieves our page content. We'll first resolve an instance of
IPageFactory and then use it to fetch our page content and add it to the
ViewBag in the current request context:
Note: Unless your implementation of IPageFactory.FindPage inserts default Page filenames (e.g. 'index.html') into the routes it's passed, you may need to append these to the url string before calling helper.NonContextualAction. Any consistent naming convention will work here as long as your Tridion Page filenames reflect those appended in your application.
RenderComponentPresentations helper will - by default - examine the
ViewBag.Page property when trying to resolve its content so, now that we've populated it, the only thing left to do is to render it out. We'll define one last helper to ensure that we don't try and render out any content in the absence of a matching Tridion page (as, in the current version of DD4T, this will result in a NullReferenceException).
This gives us everything we need. We can now simply decorate our action method with
[InjectTridionPage] and call
@Html.RenderComponentPresentationsOrEmpty() in the appropriate place in our view.
That's it! Content editors can now seamlessly create, modify and publish pages for these actions in exactly the same way as they normally would, provided the pages are created in the right place. Meanwhile, your developers maintain control of how, when and where this content appears, free of any risk of interference with critical static page content.
This approach also maintains DD4T's convention of rendering views specified in Component Templates' metadata as well as giving you the usual flexibility to render Component Presentations by view in specific parts of the page.
There is, of course, enormous scope for extending and improving the implementation above; however, this will hopefully provide a useful foundation for a convention-based approach to adding customisation to primarily static areas of your DD4T sites.
Update: Since writing this, I've been made aware of DD4T's little-documented (and, evidently, little-known) built-in UsesTridionPage attribute, which does more or less exactly what the
InjectTridionPage attribute detailed above does, so this implementation should probably be your first port of call. Note that
UsesTridionPage will, by default, throw an exception if there is no matching page to retrieve. It also requires that your controller impelements
The built-in attribute allows you to specify a page URL if you don't want to resolve it implicitly from the current action (or if your action has additional URL parameter route values, as the default implementation provides no mechanism for stripping action method parameters).