Editor/Display templates are great.. wouldn't it be nice to be able to supply additional view data with a strongly typed object or better still using named parameters?
This is helpful for other developers to know what is available in the template for them to use and help provide a consistent look and feel for a system built by many developers in a team.
for example, you have a person name model
class PersonNameModel {
string Title { get; set; }
string FirstName { get; set; }
string LastName { get; set; }
}you have a template who's Single Responsibility is to display the data in this model
@model Sandbox.MVCStrongTemplate.Models.PersonNameModel @Model.Title @Model.FirstName @Model.LastName
so where ever you have the need, you simply call the template helper
@Html.Display(m => m.Name)
This works well, and is simple to use, however, what if you want to make this template more capable? You add some html mark-up
@model Sandbox.MVCStrongTemplate.Models.PersonNameModel <div class="@ViewData["class"]"> @Model.Title @Model.FirstName @Model.LastName </div>
and allow the developer to pass some additional view data to the template
@Html.Display(m => m.Name, new{ @class = "specialPerson" })Suddenly, you are providing functionality which can only be seen from within the template. The developer must go into the template and see what they can do here, and what if the template is compiled into an assembly and cannot be seen easily... what then? (hi MVC team)
So now is the time to add an HtmlHelper extension to provide strong typing, compile time checking and a declaration of the templates capabilities.
public static class HtmlHelperExtensions
{
public static IHtmlString TemplateFor<TModel, TValue>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TValue>> expression,
string @class = null)
{
return helper.DisplayFor(
expression,
new {
@class
}
);
}
}Great, now you can see all the capabilities of the template in the extension signature, which also enforces compile-time* constraints on those capabilities.
IntelliSense, the optional params show up, yay (tho there is a bit of noise too)

Compile-time error, *you'll need your views set to compile or ReSharper for this to be obvious without having the view open

But.. we have lost something vital, because this function constructs an anonymous type which is passed to the standard function you have lost the ability to supply any old property through (additionalViewData).
public static IHtmlString TemplateFor<TModel, TValue>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TValue>> expression,
object additionalViewData = null,
string @class = null)
{
return helper.DisplayFor(
expression,
----> ??? <----
);
}Dynamic to the Rescue!
Well no, you'd think so, but no, because the TemplateHelpers class does not support dynamic types, nothing gets through, the properties on your dynamic type are not exposed by the reflection on the object passed in to the template, effectively it does this
typeof(ExpandoObject).GetProperties()
which yields nothing..
Object Extend/Merge/Combine (Whatever) to the Rescue!
What you need to do, is create a new type which has the combined properties of the two objects and pass that through to the TemplateHelpers and this can be done in two ways, Reflection.Emit and CodeDom
I have gone for CodeDom, its easier to understand and much less verbose, and although it may be a bit slower, caching means its done once per type so ... anyway
public interface IObjectExtender
{
object Extend(object obj1, object obj2);
}I've got into the habit of abstracting components like this, although here I am creating a static helper extension and this will be no use, it points to a future where this can be injected, perhaps into a HtmlHelperExt class ... mumble
Here's the implementation
public class ObjectExtender : IObjectExtender
{
private readonly IDictionary<Tuple<Type, Type>, Assembly>
_cache = new Dictionary<Tuple<Type, Type>, Assembly>();
public object Extend(object obj1, object obj2)
{
if (obj1 == null) return obj2;
if (obj2 == null) return obj1;
var obj1Type = obj1.GetType();
var obj2Type = obj2.GetType();
var values = obj1Type.GetProperties()
.ToDictionary(
property => property.Name,
property => property.GetValue(obj1, null));
foreach (var property in obj2Type.GetProperties()
.Where(pi => !values.ContainsKey(pi.Name)))
values.Add(property.Name, property.GetValue(obj2, null));
// check for cached
var key = Tuple.Create(obj1Type, obj2Type);
if (!_cache.ContainsKey(key))
{
// create assembly containing merged type
var codeProvider = new CSharpCodeProvider();
var code = new StringBuilder();
code.Append("public class mergedType{ \n");
foreach (var propertyName in values.Keys)
{
// use object for property type, avoids assembly references
code.Append(
string.Format(
"public object @{0}{{ get; set;}}\n",
propertyName));
}
code.Append("}");
var compilerResults = codeProvider.CompileAssemblyFromSource(
new CompilerParameters
{
CompilerOptions = "/optimize /t:library",
GenerateInMemory = true
},
code.ToString());
_cache.Add(key, compilerResults.CompiledAssembly);
}
var merged = _cache[key].CreateInstance("mergedType");
Debug.Assert(merged != null, "merged != null");
// copy data
foreach (var propertyInfo in merged.GetType().GetProperties())
{
propertyInfo.SetValue(
merged,
values[propertyInfo.Name],
null);
}
return merged;
}
}Some things to note:
1) the merged class properties are all typed to object, this is avoid having to add references to the generated assembly, there is no need to type in this example anyway
2) you may be able to get away with further reuse of the assembly by keying on the property names, as the resultant properties are not typed this should work fine.
Usage
Now you can create a "nice" extension method, with all the compile-time checking and full declaration of capabilities that you need and not loose any of the convenience of passing on the additionalViewData.
public static class HtmlHelperExtensions
{
private static readonly IObjectExtender Extender
= new ObjectExtender();
public static IHtmlString TemplateFor<TModel, TValue>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TValue>> expression,
object additionalViewData = null,
string @class = null)
{
return helper.DisplayFor(
expression,
Extender.Extend(
new
{
@class
},
additionalViewData)
);
}
}and call it from your views
@Html.TemplateFor(m => name, @class: "special")
marvellous

