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

This was very usefull. Thanks!


Post a Note

(required)

(required never shown)

On Twitter Follow MrAntix on Twitter

18 hours ago
scottgu
I'm excited to announce the GA Release of AMQP support with the Windows Azure Service Bus. I blogged details: http://t.co/L6SzbFbkfh

17 hours ago
clemensv
Announcing the General Availability of AMQP 1.0 in Windows Azure Service Bus! http://t.co/2ZRFUoa5rO #Subscribe #ServiceBus #AMQP #Azure

13 hours ago
WindowsAzure
RT to share the news: Announcing the release of AMQP support with #WindowsAzure Service Bus. http://t.co/2QQhagZiSW

just now
mohsin_husen
RT @andyravensable: Our baby on show! David Johnson shows Medilib to Springer developers whilst talking about PhoneGap #SpringerIT http://t…

just now
HTML5aldente
RT @aniskadri: PhoneGap MapKit plugin now supports iOS _AND_ Android with latest version of Cordova and plugman http://t.co/bIijsiKCzf