Monday, January 11, 2010

Auto-Discoverable Views using Fluent PRISM in Silverlight

One reason a developer would use a technology like MEF is to, as the name implies, make an application extensible through a process called discovery. Discovery is simply a method for locating classes, types, or other resources in an assembly. MEF uses the Export tag to flag items for discovery, and the composition process then aggregates those items and provides them to the entities requesting them via the Import tag.

Download the source code for this example

It occurred to me when working with PRISM and MEF (see the recap of my short series here) that some of this can be done through traditional means and I might be abusing the overhead of a framework if all I'm doing is something simple like marrying a view to a region.

Challenges with PRISM include both determining how views make it into regions and how to avoid magic strings and have strict type compliance when dealing with regions. This post will address one possible solution using custom attributes and some fluent interfaces.

Fluent Interfaces

Fluent interfaces simply refers to the practice of using the built-in support for an object-oriented language to create more "human-readable" code. It's really a topic in and of itself, but I felt it made sense to simplify some of the steps in this post and introduce some higher level concepts and examples along the way.

There are several ways to provide fluent interfaces. One that is built-in to the C# language is simply using the type initializer feature. Instead of this:


public class MyClass 
{
   public string Foo { get; set; }
   public string Bar { get; set; }

   public MyClass() 
   {
   }

   public MyClass(string foo, string bar) 
   {
       Foo = foo; 
       Bar = bar;
   } 
}

Which results in code like this:


...
MyClass myClass = new MyClass(string1, string2); 
...

Question: which string is foo, and which string is bar, based on the above snippet? Would you consider this to be more readable and "self-documenting"?


...
MyClass myClass = new MyClass { Foo = string1, Bar = string2 };
...

It works for me! So let's do something simple in our PRISM project. If you've worked with PRISM, then you'll know the pattern of creating a Shell and then assinging it to the root visual in a Bootstrapper. The typical code looks like this in your Bootstrapper class:


protected override DependencyObject CreateShell()
{
   Shell shell = new Shell(); 
   Application.Current.RootVisual = shell;
   return shell; 
}

That's nice, but wouldn't it also be nice if you could do something simple and readable, like this? Keep in mind we're not cutting down on generated code (and in fact, sometimes fluent interfaces may increase the amount of generated code, which is a consideration to keep in mind), but we're focused on the maintainability and readability of the source code.


protected override DependencyObject CreateShell()
{
   return Container.Resolve<Shell>().AsRootVisual();             
}

In one line of code I'm asking the container to provide me with the shell (I do this as a common practice as opposed to creating a new instance so that any dependencies I may have in the shell will be resolved), then return it "as root visual." I think that is pretty readable, but how do we get there?

The answer in this case is using extension methods. In my "common" project I created a static class called Fluent which contains my fluent interfaces (this is for the example only and would not scale in production ... you will want to segregate your interfaces into separate classes related to the modules they act upon). In this static class, I create the following extension method:


public static UserControl AsRootVisual(this UserControl control)
{
    Application.Current.RootVisual = control;
    return control;
}

An extension method does a few things. By using the keyword this on the parameter, it tells the compiler this method will extend the type. The semantics in the code look you are calling something on the UserControl, but the compiler is really taking the user control, then calling the method on the static class and passing the instance in. It is common for the extension methods to return the same instance so they can be chained. In this case, we simply assign the control to the root visual, then return it so it can be used elsewhere. We're really doing the same thing we did before, but adding a second method call, in order to make the code that much more readable.

One important concern to have and address with fluent interfaces is the potential for "hidden magic." What I mean by this is the extension methods aren't available on the base class and only appear when you include a reference to the class with the extensions. This may make them less discoverable based on how you manage your code. It also means you will look at methods that aren't part of the known interface. It's not difficult to determine where the method comes from. Intellisense will flag extension methods as extensions, and you can always right click and "go to definition" to see where the method was declared.

Auto-discoverable Views

I have two main goals with this project: the first is to be able to tag views so they are automatically discovered and placed into a region, and the second is to type the region so I'm not using magic strings all over the place. My ideal solution would allow me to add a view to a project, tag it with a region, and run it, and have it magically appear in that region. Possible? Of course!

Typing the Regions

I am going to type the regions to avoid magic strings. Because the main shell defines the regions, I'm fine with the strings there ... that is sort of the "overall definition", but then I want to make sure elsewhere in the code I can't accidentally refer to a region that doesn't exist. My first step is to create a common project that all other modules can reference, and then add an enumeration for the regions. The enumeration for this examle is simple:


namespace ViewDiscovery.Common
{
   public enum Regions
    {
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight
    }
}

Enumerations are nice because I can call ToString() and turn it into the string value of the enumeration itself. I decided to adopt the convention "Region." when tagging it in the shell, so my shell looks like this:


<UserControl x:Class="ViewDiscovery.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:region="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;assembly=Microsoft.Practices.Composite.Presentation"
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" Text="View Discovery"/>       
        <ItemsControl Grid.Row="1" Grid.Column="0" region:RegionManager.RegionName="Region.TopLeft"/>
        <ItemsControl Grid.Row="1" Grid.Column="1" region:RegionManager.RegionName="Region.TopRight"/>
        <ItemsControl Grid.Row="2" Grid.Column="0" region:RegionManager.RegionName="Region.BottomLeft"/>
        <ItemsControl Grid.Row="2" Grid.Column="1" region:RegionManager.RegionName="Region.BottomRight"/>
    </Grid>
</UserControl>

This is just a simple 2x2 grid with a region per cell. I used the ItemsControl so each cell can host multiple views. We're off to a good start! Now let's figure out how to tag our views.

The Custom Attribute

Custom attributes are powerful and easy to implement. I want to be able to tag a view as a region using my enumeration, so I define this custom attribute:


namespace ViewDiscovery.Common
{
    [AttributeUsage(AttributeTargets.Class,AllowMultiple=false)]
    public class RegionAttribute : System.Attribute
    {
        const string REGIONTEMPLATE = "Region.{0}";

        public readonly string Region;

        public RegionAttribute(Regions region)
        {
            Region = region.ToString().FormattedWith(REGIONTEMPLATE);              
        }
    }
}

Notice that I don't allow multiple attributes and that this attribute is only valid when placed on a class. Attributes can take both positional parameters (defined in the constructor) and named parameters (defined as properties). In this case, I only have one value so I chose to make it positional. When the region enumeration is passed in, I cast it to a string and then format it with the prefix, so that Regions.TopLeft becomes the string Region.TopLeft. Notice I snuck in another fluent interface, the FormattedWith. To me, that's a sight prettier than "string.Format" if I'm only dealing with a single parameter. The extension to make this happen looks like this:


public static string FormattedWith(this string src, string template)
{
    return string.Format(template, src);
}

Now that we have a tag, we can create a new module and get it wired in. I created a new project as a Silverlight Class Library (sorry, this example doesn't do any fancy dynamic module loading), built a folder for views, and tossed in a view. The view simply contains a grid with some text:


<Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock Text="I am in ModuleOne." Grid.Row="0"/>
        <TextBlock Text="I want to be at the top left." Grid.Row="1"/>
    </Grid>

Tagging the view was simple. I went into the code-behind, added a using statement to reference the common project where my custom attribute is defined and then tagged the view with the attribute. Here's the code-behind with the tag:


namespace ViewDiscovery.ModuleOne.Views
{
    [Region(Regions.TopLeft)] 
    public partial class View : UserControl
    {
        public View()
        {
            InitializeComponent();
        }       
    }
}

So now it's clear where we want the view to go. Now how do we get it there?

Discovering the Views

The pattern in PRISM for injecting a module is for the module to have an initialization class that implements IModule and then adds the views in the module to the region. We want to do this through discovery. To facilitate this, I created a base abstract class for any module that wants auto-discovered views. The class looks like this:


public abstract class ViewModuleBase : IModule
{
    protected IRegionManager _regionManager;

    public ViewModuleBase(IRegionManager regionManager)
    {
        _regionManager = regionManager;
    }

    #region IModule Members

    public virtual void Initialize()
    {
        IEnumerable<Type> views = GetType().Assembly.GetTypes().Where(t => t.HasRegionAttribute());

        foreach (Type view in views)
        {
            RegionAttribute regionAttr = view.GetRegionAttribute(); 
            _regionManager.RegisterViewWithRegion(regionAttr.Region, view); 
        }
    }

    #endregion
}

The code should be very readable. We enforce that the region manager must be passed in by creating a constructor that takes it and stores it. We implement Initialize as virtual so it can be overridden when needed. First, we get the assembly the module lives in, then grab a collection of types that have our custom attribute. Yes, our fluent interface makes this obvious because we can do type.HasRegionAttribute(). The extension method looks like this:


public static bool HasRegionAttribute(this Type t)
{
    return t.GetCustomAttributes(true).Where(a => a is RegionAttribute).Count() > 0; 
}

This takes the type, grabs the collection of custom attributes (using inheritance in case we're dealing with a derived type) and returns true if the count of our attribute, the RegionAttribute, is greater than zero.

Next, we iterate those types and get the region attribute, again with a nice, friendly interface (GetRegionAttribute) that looks like this:


public static RegionAttribute GetRegionAttribute(this Type t)
{
    return (RegionAttribute)t.GetCustomAttributes(true).Where(a => a is RegionAttribute).SingleOrDefault();
}

Now we have exactly what we need to place the view into the region: the region it belongs to, and the type. So, we register the view with the region and we're good to go!

In my module, I add a class for the module initializer called ModuleInit. I'm only using the auto-discovery so there is nothing more than an implementation of the base class that passes the region manager down:


namespace ViewDiscovery.ModuleOne
{
    public class ModuleInit : ViewModuleBase
    {
        public ModuleInit(IRegionManager regionManager)
            : base(regionManager)
        {
        }
    }
}

Now we go back to the main project and wire in the module catalog. I'm not using dynamic modules so I just reference my modules from the main project and register them by type:


protected override IModuleCatalog GetModuleCatalog()
{
    return new ModuleCatalog()
        .WithModule(typeof(ModuleOne.ModuleInit).AssemblyQualifiedName.AsModuleWithName("Module One"));            
}      

OK, so I had some fun here as well. I wanted to extend the module catalog to allow chaining WithModule for adding multiple modules, and be able to take a type name as a string, then make it a module with a name. Working backwards, we turn a string into a named ModuleInfo class like this:


public static ModuleInfo AsModuleWithName(this string strType, string moduleName)
{
    return new ModuleInfo(moduleName, strType);
}

Next, we extend the catalog to allow chaining on new modules like this:


public static ModuleCatalog WithModule(this ModuleCatalog catalog, ModuleInfo module)
{
    catalog.AddModule(module);
    return catalog;
}

Notice this simply adds the module then returns the original catalog.

At this point, we can run the project and see that the view appears in the upper left.

View Discovery with One View

I then added a second view with a rectangle:


<UserControl x:Class="ViewDiscovery.ModuleOne.Views.Rectangle"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <Rectangle Width="100" Height="100" Fill="Red" Stroke="Black"/>
    </Grid>
</UserControl>

... and tagged it:


namespace ViewDiscovery.ModuleOne.Views
{
    [Region(Regions.TopRight)] 
    public partial class Rectangle : UserControl
    {
        public Rectangle()
        {
            InitializeComponent();
        }
    }
}

An finally compiled and re-ran it. The rectangle shows up in the upper right, as expected:

View Discovery with Two Views

Next, I added a second module with several views ... including a few registered to the same cell. Adding the new module to the catalog was easy with the extension for chaining modules:


protected override IModuleCatalog GetModuleCatalog()
{
    return new ModuleCatalog()
        .WithModule(typeof(ModuleOne.ModuleInit).AssemblyQualifiedName.AsModuleWithName("Module One"))
        .WithModule(typeof(ModuleTwo.ModuleInit).AssemblyQualifiedName.AsModuleWithName("Module Two"));            
}   

Compiling and running this gives me the final result:

View Discovery with Multiple Views

And now we've successfully created auto-discoverable views that we can strongly type to a region and feel confident will end up being rendered where they belong.

Download the source code for this example

Jeremy Likness

No comments:

Post a Comment