The problem
There is a known issue in Prism where, if a Region is defined inside a DataTemplate, the Region is never register in the corresponding RegionManager.
Based on our understanding, the region is created, but it does not get registered by the RegionManagerRegistrationBehavior behavior. This happens because the RegionManagerRegistrationBehavior tries to obtain the RegionManager from the visual parent of the element containing the region. As a DataTemplate doesn’t have a visual parent, the RegionManagerRegistrationBehavior cannot find the RegionManager and the region is never registered.
In my previous post, the approach used to workaround this problem was to attach the RegionManager to the Region inside the DataTemplate. This was possible because the RegionManager was exposed as a dynamic resource and it was found in the code-behind of a custom user control from which your view had to inherit.
The new approach
This time we are going to find the corresponding RegionManager through a region behavior.
First, we are going to create an interface named IRegionManagerAware. This interface will simply expose a RegionManager property of type IRegionManager. The idea is that this interface is going to be implemented by either the view that contains the DataTemplate where the Region is going to be, or its view model.
After that, we are going to define a simple region behavior named RegionManagerAwareBehavior (which is based on the RegionActiveAwareBehavior). When a view is added to a region this behavior will check if that view or its view model implements IRegionManagerAware. If that is the case, the behavior will set the RegionManager property to the corresponding RegionManager for that view.
Finally, in the Region inside the DataTemplate, we are going to bind the RegionManager.RegionManager attached property of the control defining the Region to the RegionManager of our IRegionManagerAware object, which is going to be either our view or our view model. This is where the ObservableObject<T> class comes in. As we usually cannot create a binding from a DataTemplate to the view model or view, we are going to create a resource that inherits from ObservableObject<T>, which will act as a kind of “bridge” between the DataTemplate and our IRegionManagerAware object.
The result is that, when the DataTemplate is applied, the RegionManager of its view is attached to the Region inside the DataTemplate and the Region is registered correctly.
Below you can find the code of the aforementioned interface and behavior plus a sample application containing them. Please note that you need to override the RegisterDefaultRegionBehaviors method in your bootstrapper to register the RegionActiveAwareBehavior behavior to apply this workaround.
IRegionManagerAware
public interface IRegionManagerAware { IRegionManager RegionManager { get; set; } }
RegionManagerAwareBehavior
/// <summary> /// Behavior that monitors a <see cref="IRegion"/> object and /// changes the value for the <see cref="IRegionManagerAware.RegionManager"/> property when /// an object that implements <see cref="IRegionManagerAware"/> gets added or removed /// from the collection. /// </summary> public class RegionManagerAwareBehavior : IRegionBehavior { /// <summary> /// Name that identifies the <see cref="RegionManagerAwareBehavior"/> behavior in a collection of <see cref="IRegionBehavior"/>. /// </summary> public const string BehaviorKey = "RegionManagerAware"; /// <summary> /// The region that this behavior is extending /// </summary> public IRegion Region { get; set; } /// <summary> /// Attaches the behavior to the specified region /// </summary> public void Attach() { INotifyCollectionChanged collection = this.GetCollection(); if (collection != null) { collection.CollectionChanged += OnCollectionChanged; } } /// <summary> /// Detaches the behavior from the <see cref="INotifyCollectionChanged"/>. /// </summary> public void Detach() { INotifyCollectionChanged collection = this.GetCollection(); if (collection != null) { collection.CollectionChanged -= OnCollectionChanged; } } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { foreach (object item in e.NewItems) { IRegionManager correspondingRegionManager = this.Region.RegionManager; // If the view was created with a scoped region manager, the behavior uses that region manager instead. FrameworkElement element = item as FrameworkElement; if (element != null) { IRegionManager attachedRegionManager = element.GetValue(RegionManager.RegionManagerProperty) as IRegionManager; if (attachedRegionManager != null) { correspondingRegionManager = attachedRegionManager; } } InvokeInRegionManagerAwareElement(item, regionManagerAware => regionManagerAware.RegionManager = correspondingRegionManager); } } else if (e.Action == NotifyCollectionChangedAction.Remove) { foreach (object item in e.OldItems) { InvokeInRegionManagerAwareElement(item, regionManagerAware => regionManagerAware.RegionManager = null); } } // May need to handle other action values (reset, replace). Currently the ViewsCollection class does not raise CollectionChanged with these values. } private static void InvokeInRegionManagerAwareElement(object item, Action<IRegionManagerAware> invocation) { var regionManagerAware = item as IRegionManagerAware; if (regionManagerAware != null) { invocation(regionManagerAware); } var frameworkElement = item as FrameworkElement; if (frameworkElement != null) { var regionManagerAwareDataContext = frameworkElement.DataContext as IRegionManagerAware; if (regionManagerAwareDataContext != null) { // If a view doesn't have a data context (view model) it will inherit the data context from the parent view. // The following check is done to avoid setting the RegionManager property in the view model of the parent view by mistake. var frameworkElementParent = frameworkElement.Parent as FrameworkElement; if (frameworkElementParent != null) { var regionManagerAwareDataContextParent = frameworkElementParent.DataContext as IRegionManagerAware; if (regionManagerAwareDataContextParent != null) { if (regionManagerAwareDataContext == regionManagerAwareDataContextParent) { // If all of the previous conditions are true, it means that this view doesn't have a view model // and is using the view model of its visual parent. return; } } } // If any of the previous conditions is false, the view has its own view model and it implements IRegionManagerAware invocation(regionManagerAwareDataContext); } } } private INotifyCollectionChanged GetCollection() { return this.Region.ActiveViews; } }
A sample DataTemplate with a Region
<UserControl.Resources> <inf:RegionManagerObservableObject x:Key="ObservableRegionManager" Value="{Binding RegionManager}"/> </UserControl.Resources> ... <DataTemplate> <StackPanel Background="Beige" Orientation="Vertical"> <TextBlock Text="Region inside data template:" /> <Border BorderThickness="2" BorderBrush="Black"> <ScrollViewer MaxHeight="110" VerticalScrollBarVisibility="Auto"> <ItemsControl MinWidth="400" MinHeight="100" prism:RegionManager.RegionName="RegionInTemplate" prism:RegionManager.RegionManager="{Binding Value, Source={StaticResource ObservableRegionManager}}"/> </ScrollViewer> </Border> </StackPanel> </DataTemplate>
Sample
You can find a sample with this workaround stored here with the name RegionManagerAwareBaheviorSample.
(This code is provided “AS IS” with no warranties and confers no rights.)
I hope that you find this useful,