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





5.00/5 (3投票s)
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 标记和您自己的设计时视图模型。
以下是我的设计时视图模型在设计时的结果(从左到右,RobotView
、SurfaceView
和 MainWindow
)
以下是我的运行时视图模型在运行时的结果
如您所见,结果有所不同。我在运行时有不同的尺寸、图像和命令。
现在您已经有了这些工具,在使用 WPF 的 MEF 时,您没有借口。;)
您可以从 这里 下载代码。