Non Invasive API, take 2

time to read 2 min | 399 words

A few days ago I presented my typical way of handling container driven polymorphism.

// only use for selection
public interface IRegion<T> : IRegion {}

public void SetRegion(DependencyObject containerElement, string regionName)
{
	IRegion region = (IRegion) IoC.TryResolve(typeof(IRegion<>).MakeGenericType(containerElement.GetType()));

	if (region != null)
		_regions.Add(regionName, region);
}

I got an email with some good questions, and it is worth a second post to clarify. The questions:

The only thing I don’t like is that a lookup in the form of TryResolve(typeof(IRegion<>).MakeGenericType(containerElement.GetType())) would infer an explicit wrapper type for ever possible UI container type. On one hand it gives me the fine grain control I need to specify a different wrapper for an inherited UI container. But on the other hand, it means that ListBox, TreeView, ComboBox and TabControl would all need their own custom wrappers even though they all derive from ItemsControl and all handle adding and removing child entities in the same way.

But if containerElement == typeof(ListBox), won’t TryResolve(typeof(IRegion<>).MakeGenericType(containerElement.GetType())) fail to resolve? TryResolve expects an explicit match. It would need to find a registration for IRegion<ListBox>, not IRegion<ItemContainer> right? (Assuming Unity here.)

Even if that worked, what if I want all elements that inherit from ItemContainer to be handled by IRegion<ItemContainer> except TreeView? Say I need to handle TreeView differently for some reason. What’s a nice way to handle the 80% rule generically but still allow an override for special cases?

My response to that is very simple. Take the same approach, but bigger. Here is all we need to make this happen.

public void SetRegion(DependencyObject containerElement, string regionName)
{
	Type current = containerElement.GetType()
	while(current != typeof(DependencyObject))
	{
		IRegion region = (IRegion) IoC.TryResolve(typeof(IRegion<>).MakeGenericType(current));
	
		if (region != null)
		{
			_regions.Add(regionName, region);
			return;
		}
		
		current = current.BaseType;
	}
}

Now, we can register the following:

  • IRegion<TreeView>
  • IRegion<MyCustomRegion>
  • IRegion<ItemsControl>

And no matter what we will send to SetRegion, we can modify the behavior merely by changing the registration of IRegion<T> implementations. Note that this doesn't assumes any capacity on the container (except the basic resolve-generic-service one).

This is also a very natural model to follow, in nearly all cases (there is an issue here regarding whatever we should consider interfaces as well, but that is beside the point for this demo, and I would generally not do that anyway).