Introduction
Region in Prism is a named area of the screen where content may be rendered. (More information about regions and UI composition is available here).
Regions designed to contain any number of views of any type. If region contains just a single view everything is simple. Complications start when multiple views are associated with the same region. When views are of the same type and contain nested regions conflict occur. RegionManager
requires all regions to have unique name but when more then one nested region is created the name is no longer unique.
To work around this issue RegionManager
may create a scope and put view in that scope. Essentially a new instance
of RegionManager
is created and assigned to the view. This isolates the view from other views within parent region. More information about Scoped RegionManager
available
here.
Prism programmer’s guide states that "Scoped regions are available only with view injection". In other words, the only time when you could create a view with scoped region manager is when you call Add(Object view, string viewName, bool createRegionManagerScope )
on an instance of a region or manually create scoped region manager and attach it to the view
with a call to SetRegionManager( DependencyObject target, IRegionManager value)
.
This article demonstrates a way to enable creation of scoped regions during view discovery as well as during region navigation.
Background
Regions are created by decorating elements with RegionManager.RegionName
attached property. This associates this element and an instance
of RegionManager
which handles this region. When RegionManager.RegionName
property is attached
to the element RegionManager
also attaches two more properties RegionManager.RegionManager
and RegionManager.RegionContext
to the same element. This is very important as you will see later.
For in depth description of RegionManager.RegionContext
and
RegionManager.RegionManager
properties please follow this link.
The key feature of RegionManager.RegionManager
attribute is that it is initialized with the reference to the instance
of RegionManager
that holds the view. So if the view was created with scope the attribute will hold reference to that scoped
RegionManager
.
Problem
Implementation of Scoped Regions support could be divided into two separate aspects:
- At the time of creating the View PRISM library needs to be told (perhaps dynamically, on case by case bases) that Scoped Region is required for this particular View.
- Once View is created it should have direct access to the
RegionManager
associated with the view and its scope.
Solution
Require scoped region
During View Discovery or Region Navigation Prism implementation simply resolves View instance from DI container and adds it to the region. To enable support for scoped regions we need to inject code in between these two steps to determine if Scope is required and if yes add view to the region with the scope. We must have some kind of flag indicating if Scoped Region is required. This information could be delivered in few different ways:
-
A View class may implement certain Interface to indicate that scope is required. There could be a member on it returning Boolean indicating if scope is required at runtime.
-
Attribute could be used to mark the type as required scoped region.
There are other ways but for simplicity we will use these two.
Access correct region manager
Solution to second problem is not so straight forward. There is no direct access to an instance of
RegionManager
created for View’s scope. No reference is returned and no list of nested scope region managers is available anywhere in the region tree. To solve this obstacle we could use
RegionManager.RegionManager
attached property. When the View is being added to a Region it initializes this attached property with reference to an instance of
RegionManager
that handles this particular region. If Scoped region has been requested it would hold a reference to an instance of scoped
RegionManager
instead of parent/root
RegionManager
. To gain access to the correct instance of
RegionManager
all we have to do is to bind this attached property to a member parameter of the view. After all attached properties were designed just for that.
Implementation
Current implementation
PRISM 4 uses
IRegionNavigationContentLoader
to create new Views and add them to regions.
IRegionNavigationContentLoader
is implemented in RegionNavigationScopedContentLoader
class. The key method where all the magic happens is called LoadContent
. Examining source code reviles that after certain manipulations required for region navigation the loader executes these three lines:
...
view = this.CreateNewRegionItem(candidateTargetContract);
region.Add(view);
return view;
It creates new view, adds it to the region and returns new view to caller. So to extend this behavior and add support for scoped regions all we have to do is to determine if scoped region is required and add view with the scope. The most logical way would be to pass a flag as a parameter to LoadContent
function. Unfortunately during view discovery or region navigation content loader is called internally by the PRISM library and there is no easy way to change it.
Instead of redesigning PRISM we could make View class to carry this information. To do so we could mark type with custom attribute or make class to implement certain interface. There are pros and cons for each of these methods so for demonstration purposes I’ll implement both.
Interface
In order to add a view to a region with a scope IRegion.Add(view, viewName, createRegionManagerScope)
method should be called. In this function the view parameter is the new view we just created and the Name and Scope flag are missing info we need to provide. We will implement interface that deliver missing pieces of information:
public interface IProvideRegionScopeInfo
{
string ViewName { get; }
bool CreateRegionManagerScope { get; }
}
By implementing this interface a view class could at run time provide info if scope is required and could provide unique name for the view's instance.
Attribute
Sometimes it is enough to just mark a class with an attribute indicating that each instance of this view requires scoped region. For that we could use following attribute:
public class ScopedRegionManagerAttribute : Attribute, IProvideRegionScopeInfo
{
public ScopedRegionManagerAttribute()
{
CreateRegionManagerScope = true;
}
public string ViewName { get; set; }
public bool CreateRegionManagerScope { get; set; }
}
To unify access to both the interface and the attribute I’ve made the attribute to implement IProvideRegionScopeInfo
interface as well. So after adding all the necessary checks to the code we should have something like this:
...
view = this.CreateNewRegionItem(candidateTargetContract);
IProvideRegionScopeInfo info = (view as IProvideRegionScopeInfo)
?? (ScopedRegionManagerAttribute)view.GetType()
.GetCustomAttributes(typeof(ScopedRegionManagerAttribute), false)
.FirstOrDefault();
if (null == info)
region.Add(view);
else
region.Add(view, info.ViewName, info.CreateRegionManagerScope);
return view;
Accessing RegionManager
Gaining access to an appropriate region manager requires few extra lines of code. Usually we would use dependency injection container to resolve
IRegionManager
interface. We can still do that to access
root RegionManager
but in order to access
RegionManager
responsible for this instance of the View we have to use RegionManager.RegionManager
attached property. Doing so is very simple and is done in View’s constructor with following code:
Binding binding = new Binding("LocalRegionManager")
{ Mode= BindingMode.OneWayToSource, Source = viewModel };
this.SetBinding(Microsoft.Practices.Prism.Regions.RegionManager.RegionManagerProperty, binding);
Mode of the binding is BindingMode.OneWayToSource
because we do not want to allow this property to be changeable from within the source. Source is either a View
or ViewModel
or both if separate Binding object is used.
So, combination of these two enhancements would allow using Scoped Regions with View discovery as well as with PRISM region navigation.
Using the code
I’ve included sample application demonstrating this solution. In order to use this code in your projects just add RegionNavigationScopedContentLoader.cs to your solution and register it with DI container instead of default implementation. For sample please reference
Bootstrapper.cs file.
Credits
Sample for this article is based on the project posted on Prism Work Item 8927. It was modified to implement this fix but preserved rest of the project to enable comparison.
History
- 02/01/2012 – Initial release.
- 04/01/2012 - Added credits.
- 04/10/2012 - Changed language on few paragraphs.
- 03/04/2013 - Changed title to include work item reference.