65.9K
CodeProject 正在变化。 阅读更多。
Home

Blendability 第四部分 – MEF 的设计时支持

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2011年2月10日

CPOL

3分钟阅读

viewsIcon

11431

MEF 的设计时支持

在我的上一篇文章中,我讨论了 MEF 与著名的 MVVM 模式的结合使用,并演示了我的 Import 标记扩展的使用,以及它如何用优雅的语法替换 View Model Locator。

在本文中,我想揭示和讨论 Import 标记扩展的实现。

让我们从一个小故事开始。假设您正在构建一个用于控制机器人的应用程序。机器人快乐地生活在一个二维表面上,并且可以在表面的墙壁之间自由移动。为了可视化机器人和表面部分,您创建了两个部分:一个 Robot 部分,包含一个 RobotView RobotViewModel,以及一个 Surface 部分,包含一个 SurfaceView SurfaceViewModel。视图模型与应用程序交互,调用服务并将必要的属性暴露给视图。机器人和表面视图都基于视图优先概念从 XAML 创建。为了控制机器人,您还创建了一个 CommandBarView CommandBarViewModel

受到我上一篇文章的启发,您可能希望使用 MEF 来组合这些部分

代码片段

[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; } 
}

代码片段

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

代码片段

[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) 
            }; 
        } 
    } 
}

代码片段

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

在代码片段中,SurfaceView RobotView 都通过使用 ImportExtension 标记扩展导入相关的视图模型来设置 DataContext

Import 标记扩展接收两个参数:Contract IsDesigntimeSupported

Contract 参数是视图模型契约类型。而 IsDesigntimeSupported 指示是否应在设计时导入视图模型。

现在的问题是:Import 标记扩展如何为运行时和设计时检索视图模型?

答案是

    在运行时,它通过使用附加到应用程序的 MEF 容器,通过契约导入视图模型。在设计时,它通过使用从 XAML 附加到应用程序的特殊设计时 MEF 容器,通过契约导入视图模型。

这是 Import 标记代码

代码片段

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; 
    } 
}

运行时容器是一个常规的 MEF 容器,在 C# 的 App.cs 中创建

代码片段

public partial class App : Application 
{         
    protected override void OnStartup(StartupEventArgs e) 
    { 
        var bootstrapper = new Bootstrapper(this);             
        bootstrapper.Run();             

        base.OnStartup(e); 
    } 
}

如您所见,我正在使用一种 Bootstrapper 类。这个类派生自我的 RuntimeBootstrapper ,它提供了简单的 MEF 容器设置逻辑,如下所示

代码片段

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)); 
    } 
}

代码片段

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

代码片段

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(); 
    } 
}

查看 RuntimeBootstrapper ctor,它使用 CompositionProperties.SetBootstrapper XAML 附加属性将自己附加到应用程序的实例。

这个特殊的附加属性提供了一个选项,可以从代码和 XAML 附加任何实例到应用程序。我正在使用这种技术从应用程序的 XAML 附加 DesigntimeBootstrapper

现在您可能猜到我也有一个 DesigntimeBootstrapper,这是我从 App.xaml 中使用它的方法

代码片段

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

DesigntimeBootstrapper 定义了它所使用的设计时 MEF 目录。在此目录中,您可以仅为设计时注册类型。

由于 MEF 目录的设计初衷并非是从 XAML 创建,因此我围绕 MEF 的某些目录创建了包装器。在这种情况下:DesigntimeAggregateCatalog DesigntimeAssemblyCatalog

这是 DesigntimeBootstrapper 的代码

代码片段

[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(); 
        } 
    } 
}

如您所见,DesigntimeBootstrapper 将自己附加到应用程序,并且仅在设计时激活自身。

回顾 Import 标记扩展的代码片段,您可能会发现它使用附加到应用程序实例的引导程序,并导入相关的契约。在设计时,它还会检查 IsDesigntimeSupported 标志是否为 true,如果不是,则返回 DependencyProperty.Unset

在设计时使用 Visual Studio 和 Blend 打开每个视图时,Import 标记扩展会导入设计时视图模型。

请注意,您始终可以将 IsDesigntimeSupported 设置为 false (这是默认设置),并继续使用可爱的 Blend 示例数据。在视图模型很复杂或您可能想要生成自己的数据的情况下,您可以使用 Import 标记和您自己的设计时视图模型。

以下是我的设计时视图模型在设计时的结果(从左到右,RobotViewSurfaceView MainWindow

imageimageimage

以下是我的运行时视图模型在运行时的结果

image

如您所见,结果有所不同。我在运行时有不同的尺寸、图像和命令。

现在您已经有了这些工具,在使用 WPF 的 MEF 时,您没有借口。;)

您可以从 这里 下载代码。

© . All rights reserved.