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 moved 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
[Export(typeof(ISurfaceViewModel)), PartCreationPolicy(CreationPolicy.NonShared)]
public class SurfaceViewModel : NotificationObject, ISurfaceViewModel
{
public int SurfaceWidth
{
get
{
return Configuration.ReadValue<int>("SurfaceWidth");
}
}
public int SurfaceHeight
{
get
{
return Configuration.ReadValue<int>("SurfaceHeight");
}
}
[Import]
private IConfigurationService Configuration { get; set; }
}
Code Snippet
<UserControl x:Class="Blendability.Solution.Parts.SurfaceView"
DataContext="{ts:Import ts:ISurfaceViewModel, True}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
xmlns:parts="clr-namespace:Blendability.Solution.Parts"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<Border BorderThickness="10" BorderBrush="Brown">
<Canvas Width="{Binding SurfaceWidth}"
Height="{Binding SurfaceHeight}">
<parts:RobotView d:DataContext="{ts:Import ts:IRobotViewModel, True}" />
</Canvas>
</Border>
</UserControl>
Code Snippet
[Export(typeof(IRobotViewModel)), PartCreationPolicy(CreationPolicy.NonShared)]
public class RobotViewModel : NotificationObject, IRobotViewModel
{
private double _xPos;
private double _yPos;
private Uri _imagePath;
private DispatcherTimer _autoMovetimer;
private Random _rnd = new Random();
[ImportingConstructor]
public RobotViewModel([Import] CompositionContainer container)
{
container.ComposeExportedValue(GoLeftCommand);
container.ComposeExportedValue(GoUpCommand);
container.ComposeExportedValue(GoRightCommand);
container.ComposeExportedValue(GoDownCommand);
container.ComposeExportedValue(AutoMoveCommand);
_autoMovetimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(3)
};
_autoMovetimer.Tick += timer_Tick;
}
[Import]
private IConfigurationService Configuration { get; set; }
private void timer_Tick(object sender, EventArgs e)
{
XPos = (double)_rnd.Next(0, SurfaceWidth - RobotWidth);
YPos = (double)_rnd.Next(0, SurfaceHeight - RobotHeight);
}
public Uri ImagePath
{
get
{
if (_imagePath == null)
{
var imagePath = Configuration.ReadValue<string>("RobotImagePath");
_imagePath = new Uri(imagePath, UriKind.Relative);
}
return _imagePath;
}
}
public double XPos
{
get { return _xPos; }
set
{
if (_xPos != value)
{
_xPos = Math.Max(0, Math.Min(SurfaceWidth - RobotWidth, value));
RaisePropertyChanged(() => XPos);
}
}
}
public double YPos
{
get { return _yPos; }
set
{
if (_yPos != value)
{
_yPos = Math.Max(0, Math.Min(SurfaceHeight - RobotHeight, value));
RaisePropertyChanged(() => YPos);
}
}
}
public int SurfaceWidth
{
get
{
return Configuration.ReadValue<int>("SurfaceWidth");
}
}
public int SurfaceHeight
{
get
{
return Configuration.ReadValue<int>("SurfaceHeight");
}
}
public int RobotWidth
{
get
{
return Configuration.ReadValue<int>("RobotWidth");
}
}
public int RobotHeight
{
get
{
return Configuration.ReadValue<int>("RobotHeight");
}
}
public ICommandBarAction GoLeftCommand
{
get
{
return new CommandBarActionCommand
{
Content = "Left",
Command = new DelegateCommand(() => XPos -= 10)
};
}
}
public ICommandBarAction GoUpCommand
{
get
{
return new CommandBarActionCommand
{
Content = "Up",
Command = new DelegateCommand(() => YPos -= 10)
};
}
}
public ICommandBarAction GoRightCommand
{
get
{
return new CommandBarActionCommand
{
Content = "Right",
Command = new DelegateCommand(() => XPos += 10)
};
}
}
public ICommandBarAction GoDownCommand
{
get
{
return new CommandBarActionCommand
{
Content = "Down",
Command = new DelegateCommand(() => YPos += 10)
};
}
}
public ICommandBarAction AutoMoveCommand
{
get
{
return new CommandBarActionCommand
{
Content = "Auto",
Command = new DelegateCommand(() => _autoMovetimer.IsEnabled =
!_autoMovetimer.IsEnabled)
};
}
}
}
Code Snippet
<UserControl x:Name="View" x:Class="Blendability.Solution.Parts.RobotView"
DataContext="{ts:Import ts:IRobotViewModel, True}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
xmlns:parts="clr-namespace:Blendability.Solution.Parts"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
mc:Ignorable="d"
RenderTransformOrigin="0.5,0.5">
<UserControl.Resources>
<Storyboard x:Key="RobotStoryboard" Storyboard.TargetName="View">
<DoubleAnimation To="{Binding XPos}" Storyboard.TargetProperty=
"(UIElement.RenderTransorm).(TranslateTransform.X)">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation To="{Binding YPos}" Storyboard.TargetProperty=
"(UIElement.RenderTransform).(TranslateTransform.Y)">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</UserControl.Resources>
<i:Interaction.Triggers>
<ei:PropertyChangedTrigger Binding="{Binding XPos}">
<ei:ControlStoryboardAction Storyboard="{StaticResource RobotStoryboard}" />
</ei:PropertyChangedTrigger>
<ei:PropertyChangedTrigger Binding="{Binding YPos}">
<ei:ControlStoryboardAction Storyboard="{StaticResource RobotStoryboard}" />
</ei:PropertyChangedTrigger>
<ei:KeyTrigger Key="Left">
<i:InvokeCommandAction Command="{Binding GoLeftCommand, Mode=OneTime}" />
</ei:KeyTrigger>
<ei:KeyTrigger Key="Up">
<i:InvokeCommandAction Command="{Binding GoUpCommand, Mode=OneTime}" />
</ei:KeyTrigger>
<ei:KeyTrigger Key="Right">
<i:InvokeCommandAction Command="{Binding GoRightCommand, Mode=OneTime}" />
</ei:KeyTrigger>
<ei:KeyTrigger Key="Down">
<i:InvokeCommandAction Command="{Binding GoDownCommand, Mode=OneTime}" />
</ei:KeyTrigger>
</i:Interaction.Triggers>
<UserControl.RenderTransform>
<TranslateTransform />
</UserControl.RenderTransform>
<Image Width="{Binding RobotWidth}"
Height="{Binding RobotHeight}"
Source="{Binding ImagePath}" />
</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, 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
public class ImportExtension : MarkupExtension
{
public Type Contract { get; set; }
public bool IsDesigntimeSupported { get; set; }
public ImportExtension()
{
}
public ImportExtension(Type contract)
: this(contract, false)
{
}
public ImportExtension(Type contract, bool isDesigntimeSupported)
{
Contract = contract;
IsDesigntimeSupported = isDesigntimeSupported;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (Contract == null)
{
throw new ArgumentException("Contract must be set with the contract type");
}
var service = serviceProvider.GetService(typeof(IProvideValueTarget))
as IProvideValueTarget;
if (service == null)
{
throw new ArgumentException("IProvideValueTarget service is missing");
}
var target = service.TargetObject as DependencyObject;
if (target == null)
{
throw new ArgumentException("The target object of
ImportExtension markup extension must be a dependency object");
}
var property = service.TargetProperty as DependencyProperty;
if (property == null)
{
throw new ArgumentException("The target property of
ImportExtension markup extension must be a dependency property");
}
object value;
if (DesignerProperties.GetIsInDesignMode(target))
{
value = ImportDesigntimeContract(target, property);
}
else
{
value = ImportRuntimeContract(target, property);
}
return value;
}
private object ImportDesigntimeContract(DependencyObject target,
DependencyProperty property)
{
if (IsDesigntimeSupported)
{
return ImportRuntimeContract(target, property);
}
return DependencyProperty.UnsetValue;
}
private object ImportRuntimeContract(DependencyObject target,
DependencyProperty property)
{
var bootstrapper = CompositionProperties.GetBootstrapper(Application.Current);
if (bootstrapper == null)
{
throw new InvalidOperationException
("Composition bootstrapper was not found.
You should attach a CompositionBootstrapper
with the Application instance.");
}
return GetExportedValue(bootstrapper.Container);
}
private object GetExportedValue(CompositionContainer container)
{
var exports = container.GetExports(Contract, null, null).ToArray();
if (exports.Length == 0)
{
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));
}
var lazy = exports.First();
return lazy.Value;
}
}
The runtime container is a regular MEF container created from C# in the App.cs:
Code Snippet
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
var bootstrapper = new Bootstrapper(this);
bootstrapper.Run();
base.OnStartup(e);
}
}
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
public sealed class Bootstrapper : RuntimeBootstrapper
{
public Bootstrapper(Application application) : base(application)
{
}
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add
(new AssemblyCatalog(typeof(Bootstrapper).Assembly));
}
}
Code Snippet
public abstract class RuntimeBootstrapper : CompositionBootstrapper
{
protected RuntimeBootstrapper(Application application)
{
CompositionProperties.SetBootstrapper(application, this);
}
}
Code Snippet
public abstract class CompositionBootstrapper
{
protected AggregateCatalog AggregateCatalog
{
get;
private set;
}
public CompositionContainer Container
{
get;
private set;
}
protected CompositionBootstrapper()
{
AggregateCatalog = new AggregateCatalog();
}
protected virtual void ConfigureAggregateCatalog()
{
}
protected virtual void ConfigureContainer()
{
Container.ComposeExportedValue<CompositionContainer>(Container);
}
public void Run()
{
ConfigureAggregateCatalog();
Container = new CompositionContainer(AggregateCatalog);
ConfigureContainer();
Container.ComposeParts();
}
}
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
<Application x:Class="Blendability.Solution.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ts="http://blogs.microsoft.co.il/blogs/tomershamam"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
StartupUri="MainWindow.xaml">
<ts:CompositionProperties.Bootstrapper>
<ts:DesigntimeBootstrapper>
<ts:DesigntimeAggregateCatalog>
<ts:DesigntimeAssemblyCatalog AssemblyName="Blendability.Design" />
</ts:DesigntimeAggregateCatalog>
</ts:DesigntimeBootstrapper>
</ts:CompositionProperties.Bootstrapper>
<Application.Resources>
</Application.Resources>
</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
[ContentProperty("Catalog")]
public class DesigntimeBootstrapper : CompositionBootstrapper, ISupportInitialize
{
private readonly bool _inDesignMode;
public DesigntimeCatalog Catalog
{
get;
set;
}
public DesigntimeBootstrapper()
{
_inDesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject());
if (_inDesignMode)
{
CompositionProperties.SetBootstrapper(Application.Current, this);
}
}
protected override void ConfigureAggregateCatalog()
{
if (Catalog != null)
{
AggregateCatalog.Catalogs.Add(Catalog);
}
}
void ISupportInitialize.BeginInit()
{
}
void ISupportInitialize.EndInit()
{
if (_inDesignMode)
{
Run();
}
}
}
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 where 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
):
Here are the results of my runtime view-models at runtime:
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.