Introduction
When you're developing a XAML based composite application using Prism and regions, you may have encountered a rather frustrating problem when dealing with the shell, or regions who act as shells/dashboards at design time.
Namely, this:
As you can see, we've defined our regions, but we don't really have good way to tell what the application is going to look like at run time once Prism loads content into those regions.
In this article, I will show you how to load a view and its associated design time view model into those regions so that we end up with this:
Only interested in how to use this and not how to put it together? Jump on down
Background
The idea for the implementation was originally spurned from this article, written in 2011 for Prism 4 and MEF. Since then, there's not been a whole lot of documentation on the subject. Prism has moved on to version 6, and while MEF is still great for doing DI things, there are other DI containers that work just as well, or better. I also had a couple of points on my mental checklist I wanted to make sure were met:
- I didn't want to have to bootstrap the whole application (again) just to get design time support- The object graph of views, view models, and their associated dependencies for actual, intended runtime objects might already be long BEFORE adding design time support - my solution shouldn't force a developer to add a lot of code that's percieved to be unnecessary or complicated.
- This is behavior I want developers to explicitly and knowingly subscribe to - turning it on or off shouldn't require going to an unintuitive place to toggle, configure, or opt-in. Also, I don't want this solution to feel significantly tangental to the existing direction of Prism. We should always be able to fall back to Prism's default functionality without a half-hour of ctrl-z'ing.
- Additionally, I might not yet have design time support fully built out at the module level, and therefore I want to be able to cherry-pick which views get loaded into their respective regions at design time. And if I'm explicitly subscribed, but a view isn't loaded into a region at design time, it should be pretty clear as to why.
- MEF is great, but it's 2015 and there are other DI containers that are just as good, if not better. This solution should be easily translatable to other DI containers.
- Individual views already have design time view models - when seeing the view loaded into the region, I want to leverage that so that the desgin time data I see at the individual view level is the same design time data I see when loading the view into the region.
- This one kind of goes without saying, but this should play well with Prism 6
Why doesn't Prism offer this functionality?
That's great question! From a philosophical stance, I don't really have an answer. The team managing the Prism library can probably answer that better than I can anyway. From a technical standpoint though, there's actually quite a few reasons:
First and foremost, if you delve into the source code of Prism's region manager, you'll find the following code for the RegionName attached property:
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);
}
}
So right here, off the bat, nothing happens if we're operating at design time
Additionally, if we look at the implementation of CreateRegion:
private static void CreateRegion(DependencyObject element)
{
IServiceLocator locator = ServiceLocator.Current;
DelayedRegionCreationBehavior regionCreationBehavior = locator.GetInstance<DelayedRegionCreationBehavior>();
regionCreationBehavior.TargetElement = element;
regionCreationBehavior.Attach();
}
We see that we're getting a DelayedRegionCreationBehavior
which waits for the target element to raise the Loaded
event to actually inject the view. At runtime, this isn't a probelm; however, at design time, the loaded event isn't actually called on the user control until it's explicitly placed on a window- and in my experience, even then it's a little unreliable that it'll get raised. There's a whole other list of considerations that have to be made, but the loaded event is the hump we need to get past.
Let's assume that we were able to get past the restriction above - AND we could get the event to raise during design time - we still need a view (and its associated view model) to be registered with the region - after all, the region manager has to have something to inject... right?
Let's write some code!
Ok, so let's start by addressing that last item - the region manager has to have something to inject.
Let's start by building out a class that will be a provider of views ONLY at design time. We'll call it DesignTimeViewProviderBase
.
public abstract class DesignTimeViewProviderBase
{
private readonly IDictionary<string, Type> ViewRegistrations = new Dictionary<string, Type>();
private bool IsInitialized = false;
public void Initialize()
{
if(IsInitialized)
{ return; }
RegisterViewsWithContainer();
RegisterViewsWithRegions();
IsInitialized = true;
}
protected abstract void RegisterViewsWithContainer();
protected abstract void RegisterViewsWithRegions();
protected abstract object ResolveView(Type viewType);
protected void RegisterViewWithRegion<T>(string regionName)
{
if(ViewRegistrations.ContainsKey(regionName) == false)
{
ViewRegistrations.Add(regionName, typeof(T));
}
}
public object GetViewForRegion(string regionName)
{
object view = null;
Type viewType;
if(ViewRegistrations.TryGetValue(regionName, out viewType))
{
try
{
view = ResolveView(viewType);
}catch(Exception ex)
{
view = new TextBlock() { Text = ex.Message };
}
}
else
{
view = new TextBlock() { Text = "No view registration for region " + regionName };
}
return view;
}
}
Ok, so right off the bat, you can see that we're not going for complex here - this base class contains a one-to-one mapping of view type to region name (as we're only able to see one view per region name at design time), and provides abstract methods where any container-specific code should go. In the event we fail to get a view, we'll just show the developer a block of text detailing what the problem is.
However, DesignTimeViewProviderBase is an abstract class, so we're not quite done. Because I'm a Unity fan, let's add a class on top of this to work with Unity.
public abstract class UnityDesignTimeViewProvider : DesignTimeViewProviderBase
{
protected readonly IUnityContainer container = new UnityContainer();
protected override object ResolveView(Type viewType)
{
return container.Resolve(viewType);
}
}
As you can see, this now provides a container and a Unity-specific method of resolving views while still leaving the RegisterViewsWithContainer
and RegisterViewsWithRegions
to be implemented by the actual concrete implementations - Additonally, this provides a nice point where other DI containers could step in, if desired.
Now that we've got a class that can serve as a base for providing views, let's work on being able to consume it.
What we're going to do is exploit a little quirk in how dependency properties work. If you the reader have a link which better explains why this works the way it does, I'd be grateful.
The naming of your attached dependency properties is important - if you have multiple attached dependency properties that depend on one another for providing values (as we do), and you don't set up some sort of value coercion (as I didn't), then the setters will execute in alphabetical order. This is important for us, because we need our setter to be execute after Prism's RegionName
is set. So in our case, an attached property named something like DesignTimeViewProvider
would not be ok, because it would execute too early; but an attached property named ViewProvider
would be ok as it executes after RegionName
.
Now, when the ViewProvider
attached property is set, we'll get the value for the RegionName
attached property, pull the design time view associated with that region, and then inject it.
Let's start by creating a class called RegionProvider which will allows us to set the design time view provider
public class RegionProvider
{
public static DesignTimeViewProviderBase GetViewProvider(DependencyObject obj)
{
return (DesignTimeViewProviderBase)obj.GetValue(ViewProviderProperty);
}
public static void SetViewProvider(DependencyObject obj, DesignTimeViewProviderBase value)
{
obj.SetValue(ViewProviderProperty, value);
}
public static readonly DependencyProperty ViewProviderProperty =
DependencyProperty.RegisterAttached("ViewProvider", typeof(DesignTimeViewProviderBase), typeof(RegionProvider), new PropertyMetadata(null, ViewProvider_Changed));
So far, pretty standard for an attached dependency property; let's look at that ViewProvider_Changed
callback:
private static void ViewProvider_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(sender) == false)
{ return; }
var viewProvider = e.NewValue as DesignTimeViewProviderBase;
var regionName = RegionManager.GetRegionName(sender);
object designTimeView = null;
try
{
viewProvider.Initialize();
designTimeView = viewProvider.GetViewForRegion(regionName);
}
catch (Exception ex)
{
designTimeView = new TextBlock() { Text = ex.Message };
}
if (sender is ContentControl)
{
(sender as ContentControl).Content = designTimeView;
}
else if (sender is Selector)
{
var itemsSource = new object[] { designTimeView };
(sender as Selector).ItemsSource = itemsSource;
(sender as Selector).SelectedItem = itemsSource.First();
}
else if (sender is ItemsControl)
{
(sender as ItemsControl).ItemsSource = new object[] { designTimeView };
}
}
So the first thing we'll do is bail out if we're not in design time mode - obviously, this is code we don't want to execute at run time.
Next, since the RegionName is an attached property on the same dependency object (and we're executing after it's been set), we can pull in the name of the region, and ask our newly set view provider to get us the design time view associated with that region. Once we have a view, the only thing left is to set it based upon the three types of containers that Prism supports.
Setting up design time at the module level
I want to take a step back for a second, and set up some design time support for an existing view. Let's look at "View A". It's really simple - it's just a grid with a bound text block:
<Grid>
<TextBlock Text="{Binding DisplayText}" />
</Grid>
We'll add the following attribute to the user control:
d:DataContext="{d:DesignInstance Type={x:Type designTime:ViewModel}, IsDesignTimeCreatable=True}"
and now when we add this class:
namespace ModuleA.Demonstration.DesignTime
{
public class ViewModel : IViewModel
{
public string DisplayText
{
get
{
return "Hello Module A from design time view model";
}
}
}
}
We get the following:
Ok, so this is great, we now have design time at the module level. Now, let's bring that to the shell.
Creating a design time view provider
Now we're going to use the UnityDesignTimeViewProvider we created earlier. In addition to the View A that we created earlier, my example also has View B and View C as well. We'll set those up too.
public class ViewProvider : UnityDesignTimeViewProvider
{
protected override void RegisterViewsWithContainer()
{
container.RegisterType<ModuleA.Demonstration.View>();
container.RegisterType<ModuleA.Demonstration.IViewModel, ModuleA.Demonstration.DesignTime.ViewModel>();
container.RegisterType<ModuleB.Demonstration.View>();
container.RegisterType<ModuleB.Demonstration.IViewModel, ModuleB.Demonstration.DesignTime.ViewModel>();
container.RegisterType<ModuleC.Demonstration.View>();
container.RegisterType<ModuleC.Demonstration.IViewModel, ModuleC.Demonstration.DesignTime.ViewModel>();
}
protected override void RegisterViewsWithRegions()
{
RegisterViewWithRegion<ModuleA.Demonstration.View>(NamedRegions.ModuleA);
RegisterViewWithRegion<ModuleB.Demonstration.View>(NamedRegions.ModuleB);
RegisterViewWithRegion<ModuleC.Demonstration.View>(NamedRegions.ModuleC);
}
}
If you dig through the code, you'll find that the DataContext
of each view is a constructor injected dependency; this allows us to do this registration at design time, and let our run-time implementation get registered in the associated IModule
when the bootstrapper runs at run-time.
Now that everything's been created, we can finally add design time support to the shell.
Let's start by adding our design time view provider that we just created as a resource of the shell window:
<Window.Resources>
<designTime:ViewProvider x:Key="DesignTimeViewProvider" />
</Window.Resources>
Now, anywhere I have a place where content is loaded via a region; I just neeed to add the ViewProvider attached property, and point it back to this resource to get the deisred design time view.
Essentially, this:
<GroupBox Grid.Column="0" Header="Module A" prism:RegionManager.RegionName="{x:Static infrastructure:NamedRegions.ModuleA}" />
becomes this:
<GroupBox Grid.Column="0" Header="Module A" prism:RegionManager.RegionName="{x:Static infrastructure:NamedRegions.ModuleA}" regions:RegionProvider.ViewProvider="{StaticResource DesignTimeViewProvider}" />
Build the solution, then close and re-open the xaml file, and the design time view should be there.
History
2015-11-30: Initial publish