skip to main | skip to sidebar

Friday, December 07, 2007

Exposing generic methods to frameworks that don't understand generics.

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:

LocationSelect

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();
 }
 }
 }
}
Posted by Mike Hadlow at 11:13 am

No comments:

Post a Comment

Subscribe to: Post Comments (Atom)
 

AltStyle によって変換されたページ (->オリジナル) /