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