How do you wrap a generic method like this:
public string WriteHtml<T>(string target, T source)
so that frameworks which don't understand generics can understand it, like this:
public string WriteHtml(string target, object source)
I've been writing a web application with Monorail. It's been loads of fun, and the speed at which I've been able to get a fully featured application up and running is awesome. I'll be blogging more about this soon, but today I just want to make a quick 'howto' about exposing generic APIs to frameworks that don't understand generics.
The problem is this: Monorail's default view engine is NVelocity, a port of the java Velocity template engine. I've found it pretty nice so far. To use it you just add stuff into a 'PropertyBag' in your controller and then reference in your view using the $parameterName syntax. You can invoke methods on your types and indeed Monorail comes with a nice collection of helpers to use within the view. I wanted to create a new helper to display a tree of objects (in this case a location hierarchy) in a combo box like this:
So in my favorite TDD style I quickly got a class (HierarchicalSelect) up and running that took any object of type t with a property of IList<t> and spat out the HTML for my desired combo box. I then referenced my new helper in my template like this:
$hierarchicalSelect.WriteHtml("location.id", $location)
But it didn't work. Unhappily, NVelocity doesn't throw, it simply outputs your placeholder when it can't bind. I eventually worked out that the reason it didn't like WriteHtml() was because it's an generic method:
public string WriteHtml<T>(string target, T source)
What I needed was a way of exposing WriteHtml with the source argument typed as object rather than T. It turned out to be quite an effort. This is the obvious way:
public string WriteHtml(string target, object source) { return WriteHtml(target, source); }
But of course it doesn't work because T becomes System.Object rather than the actual type of 'source'.
Instead you have to use reflection to create a MethodInfo for WriteHtml<T> and then create a typed version before invoking it.
public string WriteHtml(string target, object source) { Type sourceType = source.GetType(); // get our unambiguous generic method MethodInfo writeHtmlMethod = typeof(HierarchicalSelect).GetMethod("WriteHtmlUnambiguous", BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance); // create a typed version MethodInfo typedWriteHtmlMethod = writeHtmlMethod.MakeGenericMethod(sourceType); // invoke it. return (string)typedWriteHtmlMethod.Invoke(this, new object[] { target, source }); } // need to provide this so that GetMethod can unambiguously find it. private string WriteHtmlUnambiguous<T>(string target, T source) { return WriteHtml<T>(target, source, null); }
Just for your entertainment. Here's the full code for HierarchicalSelect:
using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using System.Reflection; using System.Web; using System.Web.UI; using System.Linq; namespace Hadlow.PropertyFinder.HtmlHelpers { public class HierarchicalSelect { // NVelocity doesn't support generic methods :( // we have to take an object argument and then construct one of the generic methods first // before calling it. public string WriteHtml(string target, object source) { Type sourceType = source.GetType(); // get our unambiguous generic method MethodInfo writeHtmlMethod = typeof(HierarchicalSelect).GetMethod("WriteHtmlUnambiguous", BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance); // create a typed version MethodInfo typedWriteHtmlMethod = writeHtmlMethod.MakeGenericMethod(sourceType); // invoke it. return (string)typedWriteHtmlMethod.Invoke(this, new object[] { target, source }); } // need to provide this so that GetMethod can unambiguously find it. private string WriteHtmlUnambiguous<T>(string target, T source) { return WriteHtml<T>(target, source, null); } public string WriteHtml<T>(string target, T source) { return WriteHtml<T>(target, source, null); } public string WriteHtml<T>(string target, T source, IDictionary attributes) { string id = CreateHtmlId(target); string name = target; StringBuilder sb = new StringBuilder(); StringWriter sbWriter = new StringWriter(sb); HtmlTextWriter writer = new HtmlTextWriter(sbWriter); writer.WriteBeginTag("select"); writer.WriteAttribute("id", id); writer.WriteAttribute("name", name); writer.Write(" "); writer.Write(GetAttributes(attributes)); writer.Write(HtmlTextWriter.TagRightChar); writer.WriteLine(); if (source != null) { HierarchicalThing<T> root = new HierarchicalThing<T>(source); WriteOptions(writer, root, 0); } writer.WriteEndTag("select"); return sbWriter.ToString(); } private void WriteOptions<T>( HtmlTextWriter writer, HierarchicalThing<T> item, int level) { writer.WriteBeginTag("option"); //if (item.IsSelected) //{ // writer.Write(" selected=\"selected\""); //} writer.WriteAttribute("value", HttpUtility.HtmlEncode(item.Id)); writer.Write(HtmlTextWriter.TagRightChar); writer.Write(GetLevelString(level) + HttpUtility.HtmlEncode(item.ToString())); writer.WriteEndTag("option"); writer.WriteLine(); level++; foreach (HierarchicalThing<T> child in item.Children) { WriteOptions(writer, child, level); } } private string GetLevelString(int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < level; i++) { sb.Append(" "); } return sb.ToString(); } private string CreateHtmlId(string name) { StringBuilder sb = new StringBuilder(name.Length); bool canUseUnderline = false; foreach (char c in name.ToCharArray()) { switch (c) { case '.': case '[': case ']': if (canUseUnderline) { sb.Append('_'); canUseUnderline = false; } break; default: canUseUnderline = true; sb.Append(c); break; } } return sb.ToString(); } private string GetAttributes(IDictionary attributes) { if (attributes == null || attributes.Count == 0) return string.Empty; StringBuilder contents = new StringBuilder(); foreach (DictionaryEntry entry in attributes) { if (entry.Value == null || entry.Value.ToString() == string.Empty) { contents.Append(entry.Key); } else { contents.AppendFormat("{0}=\"{1}\"", entry.Key, entry.Value); } contents.Append(' '); } return contents.ToString(); } /// <summary> /// Represents a hierarchial object with one property that has a type of /// IList<sametype> /// </summary> /// <typeparam name="T"></typeparam> private class HierarchicalThing<T> { readonly T source; readonly PropertyInfo childrenProperty; readonly string id; public string Id { get { return id; } } public HierarchicalThing(T source) { this.source = source; Type sourceType = source.GetType(); childrenProperty = FindChildProperty(sourceType); id = FindIdValue(source); if (childrenProperty == null) { throw new ApplicationException("The source object must have a property that " + "represents it's children. This property much have a type of IList<source type>. " + "No such property was found in type: " + sourceType.Name); } } private string FindIdValue(T source) { return source.GetType().GetProperties().First(p => p.GetCustomAttributes(true).Any(a => a is Castle.ActiveRecord.PrimaryKeyAttribute) ).GetValue(source, null).ToString(); } private PropertyInfo FindChildProperty(Type sourceType) { return sourceType.GetProperties().First(p => p.PropertyType == typeof(IList<T>)); } public HierarchicalThing<T>[] Children { get { List<HierarchicalThing<T>> children = new List<HierarchicalThing<T>>(); IList<T> childValues = (IList<T>)childrenProperty.GetValue(source, null); foreach (T childValue in childValues) { children.Add(new HierarchicalThing<T>(childValue)); } return children.ToArray(); } } public override string ToString() { return source.ToString(); } } } }
No comments:
Post a Comment