DCSIMG
Blendability Part IV – Design Time Support for MEF - Essential WPF

Blendability Part IV – Design Time Support for MEF

In my previous post I've discussed the usage of MEF with the famous MVVM pattern, and demonstrated the usage of my Import markup-extension, and how it can replace the View Model Locator with an elegant syntax.

In this post I would like to reveal and discuss the implementation of the Import markup-extension.

Let's begin with a short story. Say that you're building an application for controlling a robot. The robot lives happily in a 2D surface, and can be move freely in between the surface's walls. To visualize both the robot and the surface parts you've created two parts: A Robot part, comprises a RobotView and RobotViewModel, and a Surface part, comprises a SurfaceView and SurfaceViewModel. The view-models interoperate with the application, call services and expose necessary properties to the view. Both the robot and the surface views created from XAML, based on the view first concept. To control the robot, you've also created a CommandBarView and CommandBarViewModel.

Inspired by my previous post, you may want to compose these parts using MEF:

Code Snippet
  1. [Export(typeof(ISurfaceViewModel)), PartCreationPolicy(CreationPolicy.NonShared)]
  2. public class SurfaceViewModel : NotificationObject, ISurfaceViewModel
  3. {
  4.     public int SurfaceWidth
  5.     {
  6.         get
  7.         {
  8.             return Configuration.ReadValue<int>("SurfaceWidth");
  9.         }
  10.     }
  11.     public int SurfaceHeight
  12.     {
  13.         get
  14.         {
  15.             return Configuration.ReadValue<int>("SurfaceHeight");
  16.         }
  17.     }
  18.     [Import]
  19.     private IConfigurationService Configuration { get; set; }
  20. }
Code Snippet
  1. <UserControl x:Class="Blendability.Solution.Parts.SurfaceView"
  2.              DataContext="{ts:Import ts:ISurfaceViewModel, True}"             
  3.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.              xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
  6.              xmlns:parts="clr-namespace:Blendability.Solution.Parts"
  7.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  9.              mc:Ignorable="d">
  10.     
  11.     <Border BorderThickness="10" BorderBrush="Brown">
  12.         <Canvas Width="{Binding SurfaceWidth}"
  13.                 Height="{Binding SurfaceHeight}">
  14.             <parts:RobotView d:DataContext="{ts:Import ts:IRobotViewModel, True}" />
  15.         </Canvas>        
  16.     </Border>
  17.     
  18. </UserControl>
Code Snippet
  1. [Export(typeof(IRobotViewModel)), PartCreationPolicy(CreationPolicy.NonShared)]
  2. public class RobotViewModel : NotificationObject, IRobotViewModel
  3. {        
  4.     private double _xPos;
  5.     private double _yPos;
  6.     private Uri _imagePath;
  7.     private DispatcherTimer _autoMovetimer;
  8.     private Random _rnd = new Random();              
  9.     [ImportingConstructor]
  10.     public RobotViewModel([Import] CompositionContainer container)
  11.     {
  12.         container.ComposeExportedValue(GoLeftCommand);
  13.         container.ComposeExportedValue(GoUpCommand);
  14.         container.ComposeExportedValue(GoRightCommand);
  15.         container.ComposeExportedValue(GoDownCommand);
  16.         container.ComposeExportedValue(AutoMoveCommand);
  17.         _autoMovetimer = new DispatcherTimer
  18.         {
  19.             Interval = TimeSpan.FromSeconds(3)
  20.         };
  21.         _autoMovetimer.Tick += timer_Tick;
  22.             
  23.     }
  24.     [Import]
  25.     private IConfigurationService Configuration { get; set; }
  26.     private void timer_Tick(object sender, EventArgs e)
  27.     {
  28.         XPos = (double)_rnd.Next(0, SurfaceWidth - RobotWidth);
  29.         YPos = (double)_rnd.Next(0, SurfaceHeight - RobotHeight);
  30.     }
  31.     public Uri ImagePath
  32.     {
  33.         get
  34.         {
  35.             if (_imagePath == null)
  36.             {
  37.                 var imagePath = Configuration.ReadValue<string>("RobotImagePath");
  38.                 _imagePath = new Uri(imagePath, UriKind.Relative);
  39.             }
  40.             return _imagePath;
  41.         }
  42.     }
  43.     public double XPos
  44.     {
  45.         get { return _xPos; }
  46.         set
  47.         {
  48.             if (_xPos != value)
  49.             {
  50.                 _xPos = Math.Max(0, Math.Min(SurfaceWidth - RobotWidth, value));
  51.                 RaisePropertyChanged(() => XPos);
  52.             }
  53.         }
  54.     }
  55.     public double YPos
  56.     {
  57.         get { return _yPos; }
  58.         set
  59.         {
  60.             if (_yPos != value)
  61.             {
  62.                 _yPos = Math.Max(0, Math.Min(SurfaceHeight - RobotHeight, value));
  63.                 RaisePropertyChanged(() => YPos);
  64.             }
  65.         }
  66.     }
  67.     public int SurfaceWidth
  68.     {
  69.         get
  70.         {
  71.             return Configuration.ReadValue<int>("SurfaceWidth");
  72.         }
  73.     }
  74.     public int SurfaceHeight
  75.     {
  76.         get
  77.         {
  78.             return Configuration.ReadValue<int>("SurfaceHeight");
  79.         }
  80.     }
  81.     public int RobotWidth
  82.     {
  83.         get
  84.         {
  85.             return Configuration.ReadValue<int>("RobotWidth");
  86.         }
  87.     }
  88.     public int RobotHeight
  89.     {
  90.         get
  91.         {
  92.             return Configuration.ReadValue<int>("RobotHeight");
  93.         }
  94.     }        
  95.     public ICommandBarAction GoLeftCommand
  96.     {
  97.         get
  98.         {
  99.             return new CommandBarActionCommand
  100.             {
  101.                 Content = "Left",
  102.                 Command = new DelegateCommand(() => XPos -= 10)
  103.             };
  104.         }
  105.     }
  106.     public ICommandBarAction GoUpCommand
  107.     {
  108.         get
  109.         {
  110.             return new CommandBarActionCommand
  111.             {
  112.                 Content = "Up",
  113.                 Command = new DelegateCommand(() => YPos -= 10)
  114.             };
  115.         }
  116.     }
  117.     public ICommandBarAction GoRightCommand
  118.     {
  119.         get
  120.         {
  121.             return new CommandBarActionCommand
  122.             {
  123.                 Content = "Right",
  124.                 Command = new DelegateCommand(() => XPos += 10)
  125.             };
  126.         }
  127.     }
  128.     public ICommandBarAction GoDownCommand
  129.     {
  130.         get
  131.         {
  132.             return new CommandBarActionCommand
  133.             {
  134.                 Content = "Down",
  135.                 Command = new DelegateCommand(() => YPos += 10)
  136.             };
  137.         }
  138.     }
  139.     public ICommandBarAction AutoMoveCommand
  140.     {
  141.         get
  142.         {
  143.             return new CommandBarActionCommand
  144.             {
  145.                 Content = "Auto",
  146.                 Command = new DelegateCommand(() => _autoMovetimer.IsEnabled = !_autoMovetimer.IsEnabled)
  147.             };
  148.         }
  149.     }
  150. }
Code Snippet
  1. <UserControl x:Name="View" x:Class="Blendability.Solution.Parts.RobotView"
  2.              DataContext="{ts:Import ts:IRobotViewModel, True}"             
  3.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.              xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
  6.              xmlns:parts="clr-namespace:Blendability.Solution.Parts"
  7.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  9.              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
  10.              xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
  11.              mc:Ignorable="d"             
  12.              RenderTransformOrigin="0.5,0.5">
  13.     
  14.     <UserControl.Resources>
  15.         <Storyboard x:Key="RobotStoryboard" Storyboard.TargetName="View">
  16.             <DoubleAnimation To="{Binding XPos}" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
  17.                 <DoubleAnimation.EasingFunction>
  18.                     <CircleEase EasingMode="EaseOut"/>
  19.                 </DoubleAnimation.EasingFunction>
  20.             </DoubleAnimation>
  21.             <DoubleAnimation To="{Binding YPos}" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
  22.                 <DoubleAnimation.EasingFunction>
  23.                     <CircleEase EasingMode="EaseOut"/>
  24.                 </DoubleAnimation.EasingFunction>
  25.             </DoubleAnimation>
  26.         </Storyboard>        
  27.     </UserControl.Resources>
  28.     
  29.     <i:Interaction.Triggers>
  30.         <ei:PropertyChangedTrigger Binding="{Binding XPos}">
  31.             <ei:ControlStoryboardAction Storyboard="{StaticResource RobotStoryboard}" />
  32.         </ei:PropertyChangedTrigger>
  33.         <ei:PropertyChangedTrigger Binding="{Binding YPos}">
  34.             <ei:ControlStoryboardAction Storyboard="{StaticResource RobotStoryboard}" />
  35.         </ei:PropertyChangedTrigger>
  36.         
  37.         <ei:KeyTrigger Key="Left">
  38.             <i:InvokeCommandAction Command="{Binding GoLeftCommand, Mode=OneTime}" />
  39.         </ei:KeyTrigger>
  40.         <ei:KeyTrigger Key="Up">
  41.             <i:InvokeCommandAction Command="{Binding GoUpCommand, Mode=OneTime}" />
  42.         </ei:KeyTrigger>
  43.         <ei:KeyTrigger Key="Right">
  44.             <i:InvokeCommandAction Command="{Binding GoRightCommand, Mode=OneTime}" />
  45.         </ei:KeyTrigger>
  46.         <ei:KeyTrigger Key="Down">
  47.             <i:InvokeCommandAction Command="{Binding GoDownCommand, Mode=OneTime}" />
  48.         </ei:KeyTrigger>
  49.         
  50.     </i:Interaction.Triggers>
  51.     
  52.     <UserControl.RenderTransform>
  53.         <TranslateTransform />
  54.     </UserControl.RenderTransform>
  55.     <Image Width="{Binding RobotWidth}"
  56.            Height="{Binding RobotHeight}"
  57.            Source="{Binding ImagePath}" />
  58.     
  59. </UserControl>

In the code snippets, both the SurfaceView and RobotView set the DataContext by importing the relevant view-model using the ImportExtension markup extension.

The Import markup extension receives two parameters: Contract and IsDesigntimeSupported.

The Contract parameter is the view-model contract type. And the IsDesigntimeSupported indicates whether a view-model should be imported at design-time.

Now the question is: How the Import markup extension retrieves a view-model for both runtime and design-time?

And the answer is:

  • At runtime it imports the view-model by contract using the MEF container attached with the application.
  • At design-time time it imports the view-model by contract using a special design-time MEF container attached with the application from XAML.

Here is the Import markup code:

Code Snippet
  1. public class ImportExtension : MarkupExtension
  2. {
  3.     public Type Contract { get; set; }
  4.     public bool IsDesigntimeSupported { get; set; }
  5.     public ImportExtension()
  6.     {
  7.     }
  8.     public ImportExtension(Type contract)
  9.         : this(contract, false)
  10.     {
  11.     }
  12.     public ImportExtension(Type contract, bool isDesigntimeSupported)
  13.     {
  14.         Contract = contract;
  15.         IsDesigntimeSupported = isDesigntimeSupported;
  16.     }
  17.     public override object ProvideValue(IServiceProvider serviceProvider)
  18.     {
  19.         if (Contract == null)
  20.         {
  21.             throw new ArgumentException("Contract must be set with the contract type");
  22.         }
  23.         var service = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
  24.         if (service == null)
  25.         {
  26.             throw new ArgumentException("IProvideValueTarget service is missing");
  27.         }
  28.         var target = service.TargetObject as DependencyObject;
  29.         if (target == null)
  30.         {
  31.             // TODO : Handle DataTemplate/ControlTemplate case...
  32.             throw new ArgumentException("The target object of ImportExtension markup extension must be a dependency object");
  33.         }            
  34.         var property = service.TargetProperty as DependencyProperty;
  35.         if (property == null)
  36.         {
  37.             throw new ArgumentException("The target property of ImportExtension markup extension must be a dependency property");
  38.         }
  39.         object value;
  40.         if (DesignerProperties.GetIsInDesignMode(target))
  41.         {
  42.             value = ImportDesigntimeContract(target, property);
  43.         }
  44.         else
  45.         {
  46.             value = ImportRuntimeContract(target, property);
  47.         }
  48.             
  49.         return value;
  50.     }
  51.     private object ImportDesigntimeContract(DependencyObject target, DependencyProperty property)
  52.     {
  53.         if (IsDesigntimeSupported)
  54.         {
  55.             return ImportRuntimeContract(target, property);                
  56.         }
  57.         return DependencyProperty.UnsetValue;
  58.     }
  59.     private object ImportRuntimeContract(DependencyObject target, DependencyProperty property)
  60.     {
  61.         var bootstrapper = CompositionProperties.GetBootstrapper(Application.Current);
  62.         if (bootstrapper == null)
  63.         {
  64.             throw new InvalidOperationException("Composition bootstrapper was not found. You should attach a CompositionBootstrapper with the Application instance.");
  65.         }
  66.         return GetExportedValue(bootstrapper.Container);
  67.     }
  68.     private object GetExportedValue(CompositionContainer container)
  69.     {
  70.         var exports = container.GetExports(Contract, null, null).ToArray();
  71.         if (exports.Length == 0)
  72.         {
  73.             throw new InvalidOperationException(string.Format("Couldn't resolve export with contract of type {0}. Please make sure that the assembly contains this type is loaded to composition.", Contract));
  74.         }
  75.         var lazy = exports.First();
  76.         return lazy.Value;
  77.     }
  78. }

The runtime container is a regular MEF container created from C# in the App.cs:

Code Snippet
  1. public partial class App : Application
  2. {        
  3.     protected override void OnStartup(StartupEventArgs e)
  4.     {
  5.         var bootstrapper = new Bootstrapper(this);            
  6.         bootstrapper.Run();            
  7.         base.OnStartup(e);
  8.     }
  9. }

As you can see, I'm using kind of Bootstrapper class. This class derives from my RuntimeBootstrapper which provides simple MEF container setup logic as follows:

Code Snippet
  1. public sealed class Bootstrapper : RuntimeBootstrapper
  2. {
  3.     public Bootstrapper(Application application) : base(application)
  4.     {
  5.     }
  6.     protected override void ConfigureAggregateCatalog()
  7.     {
  8.         base.ConfigureAggregateCatalog();
  9.         // Add this assembly to export ModuleTracker.
  10.         AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Bootstrapper).Assembly));
  11.     }
  12. }
Code Snippet
  1. public abstract class RuntimeBootstrapper : CompositionBootstrapper
  2. {
  3.     protected RuntimeBootstrapper(Application application)
  4.     {
  5.         CompositionProperties.SetBootstrapper(application, this);
  6.     }
  7. }
Code Snippet
  1. public abstract class CompositionBootstrapper
  2. {
  3.     protected AggregateCatalog AggregateCatalog
  4.     {
  5.         get;
  6.         private set;
  7.     }
  8.     public CompositionContainer Container
  9.     {
  10.         get;
  11.         private set;
  12.     }
  13.     protected CompositionBootstrapper()
  14.     {
  15.         AggregateCatalog = new AggregateCatalog();
  16.     }
  17.     protected virtual void ConfigureAggregateCatalog()
  18.     {
  19.     }
  20.     protected virtual void ConfigureContainer()
  21.     {
  22.         Container.ComposeExportedValue<CompositionContainer>(Container);
  23.     }
  24.     public void Run()
  25.     {
  26.         ConfigureAggregateCatalog();
  27.         Container = new CompositionContainer(AggregateCatalog);
  28.         ConfigureContainer();
  29.         Container.ComposeParts();
  30.     }
  31. }

Looking at the RuntimeBootstrapper ctor, it attaches itself with the Application's instance, using the CompositionProperties.SetBootstrapper XAML attached property.

This special attached property provides an option to attach any instance with the application from code and much important from XAML. I'm using this technique to attach the DesigntimeBootstrapper from the application's XAML.

Now you may guess that I also have a DesigntimeBootstrapper, and here is how I'm using it from App.xaml:

Code Snippet
  1. <Application x:Class="Blendability.Solution.App"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             
  7.              mc:Ignorable="d"
  8.              StartupUri="MainWindow.xaml">
  9.     <ts:CompositionProperties.Bootstrapper>
  10.         <ts:DesigntimeBootstrapper>
  11.             <ts:DesigntimeAggregateCatalog>
  12.                 <ts:DesigntimeAssemblyCatalog AssemblyName="Blendability.Design" />
  13.             </ts:DesigntimeAggregateCatalog>
  14.         </ts:DesigntimeBootstrapper>
  15.     </ts:CompositionProperties.Bootstrapper>
  16.     <Application.Resources>
  17.     </Application.Resources>
  18. </Application>

The DesigntimeBootstrapper defines the design-time MEF catalog it works with. In this catalog you can register types for design-time only.

Since MEF catalogs weren't designed to be created from XAML, I've created wrappers around some of the MEF's catalogs. In this case: DesigntimeAggregateCatalog and DesigntimeAssemblyCatalog.

Here is the code for the DesigntimeBootstrapper:

Code Snippet
  1. [ContentProperty("Catalog")]
  2. public class DesigntimeBootstrapper : CompositionBootstrapper, ISupportInitialize
  3. {
  4.     private readonly bool _inDesignMode;
  5.     /// <summary>
  6.     /// Gets or sets the design-time catalog.
  7.     /// </summary>
  8.     public DesigntimeCatalog Catalog
  9.     {
  10.         get;
  11.         set;
  12.     }
  13.     public DesigntimeBootstrapper()
  14.     {
  15.         _inDesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject());
  16.         if (_inDesignMode)
  17.         {
  18.             CompositionProperties.SetBootstrapper(Application.Current, this);
  19.         }
  20.     }
  21.     /// <summary>
  22.     /// Use the Catalog added at design time.
  23.     /// </summary>
  24.     protected override void ConfigureAggregateCatalog()
  25.     {
  26.         if (Catalog != null)
  27.         {
  28.             AggregateCatalog.Catalogs.Add(Catalog);
  29.         }
  30.     }
  31.     void ISupportInitialize.BeginInit()
  32.     {
  33.     }
  34.     void ISupportInitialize.EndInit()
  35.     {
  36.         if (_inDesignMode)
  37.         {
  38.             Run();
  39.         }
  40.     }
  41. }

As you can see, the DesigntimeBootstrapper attaches itself to the application and activates itself only at design-time.

Looking back at the code snippet of the Import markup extension you may find that it uses the bootstrapper attached with the application instance and imports the relevant contract. At design-time it also checks if the IsDesigntimeSupported flag is true, if not it returns  DependencyProperty.Unset.

Opening each view at design-time using both Visual Studio and Blend, the Import markup extension imports design-time view-models.

Note that you can always set IsDesigntimeSupported to false (this is the default) and keep using the lovely Blend Sample Data. In cases were view-model is complex, or you may want to generate your own data, you can user the Import markup with you own design-time view-model.

Here are the results of my design-time view-models at design time (left to right, RobotView, SurfaceView and MainWindow):

imageimageimage

Here are the results of my runtime view-models at runtime:

image

As you can see the results are different. I have different sizes, images and commands at runtime.

Now that you have the tools you've no excuses using MEF with WPF ;)

You can download the code from here.

Published Wednesday, February 09, 2011 11:53 PM by Tomer Shamam

Comments

# re: Blendability Part IV – Design Time Support for MEF

Sunday, June 19, 2011 9:43 PM by WayneB

Hello Tomer, I would like to integrate your techniques into my framework which is built upon Prism and MEF. I use Prism's MefBootstrapper ( compositewpf.codeplex.com/.../52595 ). Since my own bootsrapper inherits from MefBootstrapper and I'd like to make a new DesignTimeBootstrapper that also inherits from MefBootsrapper, my question is whether MefBootstrapper does anything that cannot be done during Design time?

# re: Blendability Part IV – Design Time Support for MEF

Monday, June 20, 2011 11:17 AM by Tomer Shamam

Hi Wayne,

Try this:

blogs.microsoft.co.il/.../blendability-part-ii-design-time-support-for-prism.aspx

It's exactly what I did. So you can!

# re: Blendability Part IV – Design Time Support for MEF

Monday, June 20, 2011 11:19 PM by WayneB

Ahh thank you. Please forgive me; I only looked at the code for this article because I assumed it contained everything from the previous posts :)

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above:
Powered by Community Server (Commercial Edition), by Telligent Systems