Click here to Skip to main content
15,884,177 members
Articles / Programming Languages / XML

Blendability Part IV – Design Time Support for MEF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
9 Feb 2011CPOL3 min read 11K   2  
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 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

C#
[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

XML
<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

C#
[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

XML
<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

C#
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) 
        { 
            // TODO : Handle DataTemplate/ControlTemplate case... 
            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

C#
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

C#
public sealed class Bootstrapper : RuntimeBootstrapper 
{ 
    public Bootstrapper(Application application) : base(application) 
    { 
    } 

    protected override void ConfigureAggregateCatalog() 
    { 
        base.ConfigureAggregateCatalog(); 

        // Add this assembly to export ModuleTracker. 
        AggregateCatalog.Catalogs.Add
	(new AssemblyCatalog(typeof(Bootstrapper).Assembly)); 
    } 
}

Code Snippet

C#
public abstract class RuntimeBootstrapper : CompositionBootstrapper 
{ 
    protected RuntimeBootstrapper(Application application) 
    { 
        CompositionProperties.SetBootstrapper(application, this); 
    } 
}

Code Snippet

C#
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

C#
<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

C#
[ContentProperty("Catalog")] 
public class DesigntimeBootstrapper : CompositionBootstrapper, ISupportInitialize 
{ 
    private readonly bool _inDesignMode; 

    /// <summary> 
    /// Gets or sets the design-time catalog. 
    /// </summary> 
    public DesigntimeCatalog Catalog 
    { 
        get; 
        set; 
    } 

    public DesigntimeBootstrapper() 
    { 
        _inDesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject()); 

        if (_inDesignMode) 
        { 
            CompositionProperties.SetBootstrapper(Application.Current, this); 
        } 
    } 

    /// <summary> 
    /// Use the Catalog added at design time. 
    /// </summary> 
    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):

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.

This article was originally posted at http://feeds.feedburner.com/EssentialWPF

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect CodeValue
Israel Israel
Tomer Shamam is a Software Architect and a UI Expert at CodeValue, the home of software experts, based in Israel (http://codevalue.net). Tomer is a speaker in Microsoft conferences and user groups, and in-house courses. Tomer has years of experience in software development, he holds a B.A degree in computer science, and his writings appear regularly in the Israeli MSDN Pulse, and other popular developer web sites such as CodePlex and The Code Project. About his thoughts and ideas you can read in his blog (http://blogs.microsoft.co.il/blogs/tomershamam).

Comments and Discussions

 
-- There are no messages in this forum --