In my previous post, I’ve presented the Blendability concept and explained how to leverage Blend’s sample-data generation in order to support view-model at design time.
In this post, I would like to continue with this concept and reveal a tiny research I’ve done related to design time support of Prism modules.
If you’ve ever developed a WPF Composite Application using Prism, you may be aware of a frustrating problem while trying to work with the Shell at design time. It always ends with something like this:
As you can see, the Shell is always left blank, and there is no way to design it properly since no module was loaded at design time and you don’t have the perspective of a whole picture.
To fix this problem, all you have to do is force Blend or Visual Studio designers to load the missing modules.
A good start point would be the RegionManager
class. As you may notice, we are using the RegionManager
to “mark” a WPF control as region by setting the RegionManager.RegionName
attached property.
Digging inside RegionManager
, you can see the following lines of code:
public static readonly DependencyProperty RegionNameProperty =
DependencyProperty.RegisterAttached(
"RegionName",
typeof(string),
typeof(RegionManager),
new PropertyMetadata(OnSetRegionNameCallback));
private static void OnSetRegionNameCallback(
DependencyObject element,
DependencyPropertyChangedEventArgs args)
{
if (!IsInDesignMode(element))
{
CreateRegion(element);
}
}
Looking at the definition of the RegionNameProperty
attached property, there is a OnSetRegionNameCallback
property changed callback. This method actually does the job. But as you can see, in case you’re at design-time, it simply does nothing. Removing this condition yields an exception at design-time, caused by a missing service locator.
So now we are dealing with how to force the creation of a service locator, and so many other services such as the IEventAggregator
.
Herein, let me introduce the Designtime Bootstrapper. This guy is a special Prism bootstrapper we will use for design time-only modules, easily created from XAML.
Now before I shall begin, let’s have few words about the differences between module’s run-time and design-time activities.
Working at design-time, it’s not reasonable to load a module as it was at run-time. There may be special module configuration and run-time activities such as server calls, which are not appropriate at design-time. Here, you should be considering developing modules for design-time only. But this one is just my friendly suggestion, not a mandatory solution.
Now let’s continue with the Designtime Bootstrapper. Starting with Prism 4, there are two options to load modules: Unity and MEF. In this post, I’ll concentrate in one of these two options, which is MEF of course.
Loading modules with MEF, you should create a MefBootstrapper
.
Here is my design-time MEF bootstrapper:
[ContentProperty("Catalog")]
public class DesigntimeMefBootstrapper :
MefBootstrapper, ISupportInitialize
{
public DesigntimeCatalog Catalog
{
get;
set;
}
public static void SetBootstrapper(
Application application,
DesigntimeMefBootstrapper bootstrapper)
{
}
protected override DependencyObject CreateShell()
{
return null;
}
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
if (Catalog != null)
{
AggregateCatalog.Catalogs.Add(Catalog);
}
}
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
protected override void ConfigureContainer()
{
base.ConfigureContainer();
Container.ComposeExportedValue<CompositionContainer>(Container);
}
void ISupportInitialize.BeginInit()
{
}
void ISupportInitialize.EndInit()
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
Run();
}
}
}
This design-time bootstrapper provides the following:
- Special
Catalog
property of type DesigntimeCatalog
. This is an adapter for the MEF catalog so we can create design-time catalogs directly from XAML. - Implementation of the
ISupportInitialize
interface to ensure that all properties are initialized from XAML before acting. - Stub method for creating a design-time bootstrapper directly from
Application
’s XAML as an attached property.
And here is one of my MEF catalog adapters for design-time:
public class DesigntimeAssemblyCatalog :
DesigntimeCatalogAdapter<AssemblyCatalog>
{
protected override AssemblyCatalog CreateInnerCatalog()
{
var assembly = Assembly.Load(AssemblyName);
return new AssemblyCatalog(assembly);
}
public string AssemblyName { get; set; }
}
Having these, let's jump into the Application
’s XAML file to see how we can use them.
<Application x:Class="Blendability.Prism.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">
<ts:DesigntimeMefBootstrapper.Bootstrapper>
<ts:DesigntimeMefBootstrapper>
<ts:DesigntimeAggregateCatalog>
<ts:DesigntimeAssemblyCatalog AssemblyName=
"Blendability.Modules.Camera2dT1" />
<ts:DesigntimeAssemblyCatalog AssemblyName=
"Blendability.Modules.DesigntimeModule" />
</ts:DesigntimeAggregateCatalog>
</ts:DesigntimeMefBootstrapper>
</ts:DesigntimeMefBootstrapper.Bootstrapper>
<Application.Resources>
</Application.Resources>
</Application>
As you can see, I’ve created an instance of type DesigntimeMefBootstrapper
directly from the Application
’s XAML file, configured it to load two modules: a regular module, and another one created for design-time only.
Now that everything is ready, let’s go back to where we started. We had to remove the design-time condition from the RegionManager
, OnSetRegionNameCallback
method.
Since Prism 4 is an open source, we can open the RegionManager
source code and change it, but if you ask me, I’d rather have an adapter than change the original code since who knows what will be brought with the next version of Prism.
So here is my adapter:
public class RegionManager
{
public static readonly DependencyProperty RegionNameProperty =
DependencyProperty.RegisterAttached(
"RegionName",
typeof(string),
typeof(RegionManager),
new PropertyMetadata(OnSetRegionNameCallback));
public static void SetRegionName(
DependencyObject regionTarget,
string regionName)
{
if (regionTarget == null)
throw new ArgumentNullException("regionTarget");
regionTarget.SetValue(RegionNameProperty, regionName);
}
public static string GetRegionName(DependencyObject regionTarget)
{
if (regionTarget == null)
throw new ArgumentNullException("regionTarget");
return regionTarget.GetValue(RegionNameProperty) as string;
}
private static void OnSetRegionNameCallback(
DependencyObject element,
DependencyPropertyChangedEventArgs args)
{
Microsoft.Practices.Prism.Regions.RegionManager.SetRegionName(
element, (string)args.NewValue);
if (DesignerProperties.GetIsInDesignMode(element))
{
CreateRegion(element);
}
}
private static void CreateRegion(DependencyObject element)
{
IServiceLocator locator = ServiceLocator.Current;
DelayedRegionCreationBehavior regionCreationBehavior =
locator.GetInstance<DelayedRegionCreationBehavior>();
regionCreationBehavior.TargetElement = element;
regionCreationBehavior.Attach();
}
}
Ok, now let’s change the shell to work with the region-adapter and see what happens at design-time.
This is it guys, we are done! Now we are able to watch the whole story at design-time.
Update #1: Changed RegionManager
to RegionAdapter
to prevent confusion.
Here is the code.
In the next post, I’ll talk about a replacement for the view-model locator using MEF, providing a nice and clean solution for design-time.
Happy new year to you all.
CodeProject