Monday, March 1, 2010

MEF instead of PRISM for Silverlight 3 Part 2 of 2: Region Management

In my last post, I showed you how to dynamically load modules on demand using the latest MEF release in Silverlight 3. This post, I will take you through managing regions with MEF. This will enable us to have a 100% MEF-based solution in Silverlight 3 if the only pieces of PRISM we were using were the dynamic module loading and region management.

Download the Source Code for this Post

Quick note: technically, the Composite Application Guidance (PRISM) is more of a guidance than an actual library. The library that is often used is the reference library for the guidance. I only mention this because in using the concept of the region manager and views, technically we are implementing the PRISM concept here and using CAG, we're just using MEF as the engine instead of the included reference library.

OK, let's get going ...

The first thing I did this time around was to rip out all of the IModuleInitialization pieces. They aren't needed because MEF will initialize what we need, as it's loaded. I destroyed the classes in the modules (it was good to see them load, anyway) and updated the navigation class to look like this:

[Export(typeof(INavigation))]
public class ViewNavigator : INavigation 
{        
    [Import]
    public ICatalogService CatalogService { get; set; }
    
    private readonly Dictionary<ViewType, string> _viewMap =
        new Dictionary<ViewType, string>
            {
                { ViewType.MainView, "DynamicModule.xap" },
                { ViewType.SecondView, "SecondDynamicModule.cs.xap" }
            };   

    private readonly List<string> _downloadedModules = new List<string>();
                                                                   
    public void NavigateToView(ViewType view)
    {
        if (!_downloadedModules.Contains(_viewMap[view]))
        {
            var catalog = new DeploymentCatalog(_viewMap[view]);
            CatalogService.Add(catalog);
            catalog.DownloadAsync();
            _downloadedModules.Add(_viewMap[view]);
        }
    }           
}

Thanks to Glenn Block for pointing out the excellent sample that comes included with the MEF bits (the deployment sample) ... this shows a different way of abstracting even the XAP file from the action of loading the catalog. In this case, I'm assuming I have prior knowledge of the controls and want to make my application more official by loading them dynamically as the user selects them.

So, now we've re-factored. That's a lot cleaner! Next, I want strongly-typed regions. I created a new project just for the regions, and added this class to define the enumeration:

public enum ViewRegion
{
    MainRegion,
    SubRegion 
}

public static class ViewRegionExtensions
{
    private static readonly List<ViewRegion> _regions = new List<ViewRegion> { ViewRegion.MainRegion, ViewRegion.SubRegion };       

    public static ViewRegion AsViewRegion(this string regionName)
    {
        foreach(var region in _regions)
        {
            if (regionName.Equals(region.ToString(), StringComparison.InvariantCultureIgnoreCase))
            {
                return region;
            }
        }
        return ViewRegion.MainRegion; 
    }
}

Unfortunately, I don't believe the Silverlight version of the Enum class allows you to enumerate the values of an enumeration. Therefore, I'm simply providing a static class to hold those and putting it next to the enum so it's easy to remember for maintenance. I also added an extension method that allows me to take a string and do AsViewRegion() to convert it to the enum.

In order to make the enum appear in XAML, we need to give Silverlight a hint that tells it how to convert from the string we'll key in the XAML to an actual enum we use in code. Here is the type converter, it basically says it only knows how to convert from strings, and uses the extension method to cast the string when called:

public class ViewRegionConverter : TypeConverter 
{        
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType.Equals(typeof (string));
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        return ((string) value).AsViewRegion();
    }
}

Now we can create our behavior. The behavior is an attached property we'll use to tag a container as a region. Eventually this can become complex as we support lists and content controls, but for the sake of this blog series I'm keeping it simple and limiting our containers to panels. Panels all have children, so it is easy to add an item to the children. The behavior will simply keep a static dictionary that maps the view region to the panel it was tagged to, and exposes a method that lets us request the panel for a view region. Here it is:

public static class RegionBehavior
{
    private static readonly Dictionary<ViewRegion, Panel> _regions = new Dictionary<ViewRegion, Panel>();
    
    public static readonly DependencyProperty RegionNameProperty = DependencyProperty.RegisterAttached(
        "RegionName",
        typeof (ViewRegion),
        typeof (RegionBehavior),
        new PropertyMetadata(ViewRegion.MainRegion, null));

    public static ViewRegion GetRegionName(DependencyObject obj)
    {
        return (ViewRegion) obj.GetValue(RegionNameProperty);
    }

    [TypeConverter(typeof(ViewRegionConverter))]
    public static void SetRegionName(DependencyObject obj, ViewRegion value)
    {
        obj.SetValue(RegionNameProperty, value);
        var panel = obj as Panel;
        if (panel != null)
        {
            _regions.Add(value, panel);
        }
    }
   
    public static Panel GetPanelForRegion(ViewRegion region)
    {
        return _regions[region];
    }

}

Notice the type converter hint I give the setter. That lets the XAML know how to convert. In fact, this will even give us intellisense in the XAML, so it knows what the valid enumerations are! Now I can go into my main shell and create my first region (the intellisense quickly gave me a list to choose from ... right now there are only two choices!)

<StackPanel Orientation="Vertical" Grid.Row="2" Regions:RegionBehavior.RegionName="MainRegion"/>

We can debug at this point and see that indeed the panel is registered with the dictionary with the behavior, so we're set on that front. Now we need to get our views into the region. We're still in the special "Regions" project and namespace. For now, I'm simply going to export type UserControl for my view, and specify the region with an attribute. In MEF, we can create a strongly typed attribute for metadata:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class TargetRegionAttribute : ExportAttribute 
{        
    public TargetRegionAttribute() : base(typeof(UserControl))
    {
        
    }

    public ViewRegion Region { get; set; }
}

So the type is UserControl but we can specify a region in the meta data. Now I can add a view to my dynamic module. I'm also specifying a region here for a nested view.

<UserControl x:Class="DynamicModule.View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:Regions="clr-namespace:RegionsWithMEF.Regions;assembly=RegionsWithMEF.Regions" 
    >
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" Text="Left Column of Outer View"/>
        <StackPanel Grid.Column="1" Orientation="Vertical" Regions:RegionBehavior.RegionName="SubRegion">
            <TextBlock Text="Right Column of Outer View"/>
        </StackPanel>
    </Grid>
</UserControl>

We then tag the view for export with our custom attribute, like this:

[TargetRegion(Region=ViewRegion.MainRegion)]
public partial class View
{
    public View()
    {
        InitializeComponent();
    }
}

Very easy and readable. I went ahead and added another control for the second view, and tagged it to the "sub region" view.

Now we've got our custom attributes and exports, but we still need to glue it together. When the catalog is loaded, the views will be tagged as exports. We just need something to import them and we can then inspect the meta data and put them in their proper places. For this, I created the RegionManager.

First, let me back up. We want to read our meta data, so I need an interface with read only properties to pull it in:

public interface ITargetRegionCapabilities
{
    ViewRegion Region { get;  }
}

Great! Now I can define region manager. We'll use the Lazy object so we can define the capabilities with the import and inspect the meta data. I put it into an observable collection and listen to the collection. When it fires, I inspect the data for new controls, and then route them to their region based on the meta data:

[Export]
public class RegionManager
{
    private readonly List<UserControl> _controls = new List<UserControl>();

    [ImportMany(AllowRecomposition = true)]
    public ObservableCollection<Lazy<UserControl, ITargetRegionCapabilities>> Controls { get; set; }

    public RegionManager()
    {
        Controls = new ObservableCollection<Lazy<UserControl, ITargetRegionCapabilities>>();
        Controls.CollectionChanged += Controls_CollectionChanged;
    }

    void Controls_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach(var item in e.NewItems)
            {
                var controlInfo = item as Lazy<UserControl, ITargetRegionCapabilities>;

                if (controlInfo != null)
                {
                    if (!_controls.Contains(controlInfo.Value))
                    {
                        ViewRegion region = controlInfo.Metadata.Region;
                        Panel panel = RegionBehavior.GetPanelForRegion(region);
                        panel.Children.Add(controlInfo.Value);
                        _controls.Add(controlInfo.Value);
                    }
                }
            }
        }
    }
}

Note: if you are wondering why I use a list and an observable collection, it's because with recomposition, MEF is free to rebuild the entire list. I need to know specifically what changed and the "new items" might include older items that were recomposed. So the internal list allows me to keep track of what I've processed while I can parse out the newer items. With custom items you'll want to make sure you have a good implementation of Equals and GetHashCode, and if you have items that will go out of focus and disappear, you'll want to switch the internal list to a list of WeakReference instead.

That's it! The last step is to make sure that the region manager participates in the entire chain of imports and exports. I could wire it up in the container, but I know the shell view model is the top of the hierarchy, so to speak, so I'll just import the region manager directly. I haven't interfaced it because it doesn't expose any methods or properties: all of the work is internal and interfaces with the static dictionary we put into our behavior.

[Import]
public RegionManager Manager { get; set; }

OK, let's fire it up. I made a short video to show you the results.

Download the Source Code for this Post

Watch a great video tutorial covering dynamic modules with Glenn Block on Channel 9: Dynamically Loading XAPs with MEF.

Jeremy Likness

12 comments:

  1. Nice post! This is actually something that was explored internally some time ago. The one caveat is it is coupling the part with the region it needs to go in. True it's not dependent on what that is, but it still gives the part the knowledge of "where" it should reside. It does make sense for certain scenarios though.

    An alternative approach is to have parts declare metadata which a layout manager uses to determine where it should go. For example instead of a news part having metadata that says it go in the News Region, it can simply export a piece of additional metadata like "ViewType" which indicates that it IS news. For example...

    [View(Type=ViewTypes.News)]
    public class NewsPart {
    }

    View is a custom export which exposes Type metadata.

    This way the part is completely decoupled from any connection to the UI, somethign which is important if you are reusing parts across applications.

    This is only a semantical difference, but it has great implications. Also you can extend the metadata to be even richer, thus giving more opportunities for how the UI is rendered based on the metadata.

    Both approaches have their place though.

    Keep up the great work!

    ReplyDelete
  2. Yes, thanks for the comment! I wanted to show mainly how the PRISM patterns could be extended into MEF. There is definitely debate over whether the region manager solution makes sense. I have a lot of people who are drawn to MEF but feel they need PRISM for certain aspects, so I wanted to show a clean break using the patterns they are familiar with.

    I've found most of my line of business applications are really looking for a routing mechanism, to make it easier to route the view to the correct location. Decoupling would suggest we tag the view, we tag the region, then we have something in between that routes views to regions.

    On the other hand, I think of a social networking application that can have columns for Twitter, Facebook, etc, and within those columns perhaps different areas for pictures, profile names, and actions ... that is the perfect example where there is truly no knowledge of views or regions before hand and will be a very interesting pattern to explore!

    I encourage those reading this to watch the video link I supplied as well, which shows a different way of tagging/routing the views.

    Thanks!

    ReplyDelete
  3. First of all, thanks for the effort. However, are there any real world samples out there that demonstrate MEF in a real world LOB type of scenario.

    I just got through going through Prism. It was a lot of frustration etc. You find "short" samples like these, you get excited and start trying to build a prototype and it doesn't take long to realize there is a lot missing.

    Looks like you just recreated the wheel and wrote code to for things such as the RegionManager, so if you know Prism, so to me the samples don't accomplish what they state.

    What if your view models need to require constructor params for instantiation? WHat if your in a situation where you need controllers etc.

    I'm trying to look into MEF but I just got through with learning Prism to a level where I'm comfortable that there isn't anything our team can't accomplish without hacking and ending up with a mess just as bad as using the code behind philosophy.

    If MEF is a lot easier and concise, I'd love to give it a try but like most other people don't have 2 months more to waste trying to figure it out.

    As I put in another post, I'm starting a community to help lower the learning curve for developers who want to start utilizing Design patterns to create layered code. I've taken the time to try and put together a real world sample covered over 9 Lessons.

    They can use some improving as well. Plus if anybody has something they'd like to see, if I have time, I'm open to adding it as a lesson.
    http://www.compositedevpatterns.com/list/category/48-Training-(Prism)

    Jeremy, do you know of any real world examples out there for MEF developers can go to and find samples covering and appropriately showing how to utilize them for real world scenarios?

    I'm sure if you worked with MS on the olympics you guys covered a lot of areas.

    To start, from the environments I work in, we want to build our code in modules. Our shell needs to be dynamic in that you need to be able to have multiple different region layouts. As the shell in some cases may need only 2 regions of a certain size. But then in others it may need 6 regions.

    Popups, like them or not are still common as editors in quite a few applications.

    Right click menu, context functionality etc.

    If there is not anything out there like it, if anybody would care to help I'd like to get a section for MEF up on the community. I can look at doing with MEF what I did with Prism and start a separate set of Lessons if anybody out there with MEF knowledge wouldn't mind sharing their contact to bounce questions off of. (Right now I don't have enough free time to go through the same trials and tribulations I did with Prism.)

    P.S. Again Jeremy, I posted this because of a comment you left on the MEF codeplex site indicating you were a part of the MEF contrib team. So I posted these comments as constructive criticism and hopes they get to the right persons at MS to help get us better samples and training materials.

    ReplyDelete
  4. There are lots of real world examples, the question is, can you have access to them? The reason you see short posts is because I'm not at liberty to post a customer's code and frankly don't have the time to build a huge reference project ... I'd love to and try to get bits and pieces, but it's just not practical.

    What if my objects require constructor params? First, that's a design detail, not a problem for MEF or PRISM. I decide if I want dependencies to be injected in the constructor or exposed as properties. MEF can handle either, via ImportingConstructor for example.

    I understand you don't have "time to waste" but it is a core part of the CLR now, so it makes sense to start to learn it. I would say if it takes 2 months to learn, programming probably isn't the right profession to be in. Seriously, it's not rocket science and most people I know can have code up and running in days.

    The examples you gave me don't sound like PRISM or MEF problems. They are design. So your shell needs different types of regions? Fine. I'll create a master shell with one region, then my modules will have a module-specific shell with module-specific regions, etc. This isn't Prism or MEF specific, it's how I design it.

    The frameworks are there for guidance and to help you build the application, not to build the whole thing for you! I've given examples of all of the things you speak of.

    For MEF, specifically, part of the MEFContrib is building documentation. We're working on it as we speak, including both a comprehensive programming guide as well as quickstarts that include text, sample videos, and reference projects. They will all be based on the real world scenarios the members of the contrib team encounter day to day.

    Hope that helps and thanks for sharing your links!

    ReplyDelete
  5. This is great information Jeremy. Thing is, I get the following error when I try to run:
    The base class or interface of 'System.Lazy' could not be resolved or is invalid

    c:\Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Client\System.ComponentModel.Composition.dll RegionsWithMEF.Regions

    ReplyDelete
  6. Could it work on silverlight 4 ? Cause it doesn't work for me :(

    The view are not loaded. But the button become enable true / false. I've try step by step first, fail.

    Then i try to start with your solution, add new project sl4 application, new website, then drag & drop file rename only the namespace. Still fail.

    Does anybody make it work on silverlight 4 ? I succeed to build the part 1 in sl4 but not this one :/
    I'll try another times after watching the video.

    by the way, Thanks a lot !

    ReplyDelete
  7. Downloaded, compiled, ran the app. Main shell shows up, regions don't! Did anybody manage to reproduce the app from the downloadables?

    ReplyDelete
  8. First of all: Thank you very much.
    I have started with MEF just through your post.

    I would like to use this, but here in Silverlight 4 it does not work, the views do not show up.

    Being a MEF-Newbie I can but guess:

    I would assume that after a module has
    been downloaded, the CollectionChanged
    event handler in RegionManager should
    fire. But it does not.

    Excellent post, Jeremy. Lots of brain-food.

    Andreas.

    ReplyDelete
  9. I take everything back, works like a breeze.

    I had slipped your tutorial into my code and
    there was a difference. It seems that
    DeploymentCatalog.DownloadAsync has to be
    called on the UI thread, it fails rather
    silently otherwise. After routing it via the
    Dispatcher, everything went OK.

    Sorry for the premature comment.

    Andreas.

    ReplyDelete
  10. Thank you, for the introduction!

    I've got a few newbie questions:
    In your initial post, you wrote that this example is good, but that if you are only using it "for dynamic module loading and view management".

    What exactly are you leaving out of Prism?
    And what Unity-features are missing?
    Under which scenarios *not* to take MEF?

    Also, I'd like to know: How to change/register a different (mock) implementation of an interface with a second one in MEF? (How does MEF know which one to take?)

    ReplyDelete
  11. Lots of questions!

    Prism has a LOT of functionality so it would be difficult to inventory all of the differences. Let's just say there is a lot there that if you are going to use it, I definitely would - and keep in mind the latest version does support MEF as the underlying way to wire things up.

    The Unity features are more a comparison between IoC containers than a direct PRISM to MEF or otherwise comparison.

    For scenarios not to take MEF - it's tough to note, depends on the application, IoC strategy, etc. I won't use MEF if I'm not as concerned about life time management but the second I find several classes that are shared across the application I prefer MEF to import/export those and get solid reuse - other consideration is size of the include in the XAP.

    For your last point, I typically wire mocks manually in test mode and use MEF in production.

    ReplyDelete
  12. Jeremy - nice post,

    Are there code changes required to this project for Silverlight 4?

    I've just compiled it and run it with VS2010 and the views do not appear?

    The same was true with Part 1, the break points in the initalisers never got hit.

    In your part 2 project, if I set break points in the View's constructor, they do not get hit, as if they are not getting loaded?

    Thank you
    Rob

    ReplyDelete