• Regions inside DataTemplates in Prism v4 using a region behavior

    Published by on November 10th, 2011 5:56 pm under Prism

    17 Comments

    Hi everybody,

    A couple of months ago I published a blog post (Regions inside DataTemplates in Prism v4) about a possible work around for placing a Prism Region inside a DataTemplate in WPF.

    However, a few days ago I was talking with Guido Maliandi (after he published his blog post about Binding to commands from inside DataTemplates) and we found another (and possibly better!) approach to be able to have a Region inside a DataTemplate (which also works in Silverlight too!).

    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,

    • Pingback: Regions inside DataTemplates in Prism v4 | Damian Cherubini()

    • Timo Kosig

      Hi there,

      we really appreciate the work you’ve been doing on Prism. We’ve been bumping into these limitations before and it’s great to see that someone has the time and perseverance to find a nice workaround.

      I’ve got a question: Will this workaround work if you’ve defined the DataTemplate in a file of its own? We can’t compile the DataTemplate (since it is not able to find the the StaticResource).

      Our scenario is actually slightly more complex: we have defined a default layout template which is a ContentTemplate for the UserControl type. The ContentTemplate defines 4 content controls which in turn each have a region attached. We tried adding a section to the ContentTemplate which defines the RegionManagerObservableObject in the ControlTemplate.Resources section.

      We went down this route to have a solution which makes sure that we can reuse the same layout throughout different parts of our application. If you think that the ControlTemplate approach sounds odd, I’d love to hear about your take on that.

      Best regards,
      Timo

    • Andrew Angell

      Does this work with WPF or only Silverlight? The project won’t load for me because it says I need the latest Silverlight Developers runtime. I download and install it but sitll nothing. I’ve got the SL 5.0 on 64-bit.

    • dcherubini

      Hi Timo,

      Sorry for the wait. As far as I know, if you are defining the DataTemplate in its own file, this workaround will not work as out of the box. As you mentioned, the StaticResource is being defined in each view and the DataTemplate will not be able to access it.

      As a possible approach for your scenario, you might benefit for using a ControlTemplate and its TemplateBinding binding. You could try setting the RegionManager property of the UserControl you want to apply the template to and then, in the DataTemplate, you could try to bind to the RegionManager property of the control through a TemplateBinding.

      For example, you can define the ContentControl like this:
      <ContentControl BorderThickness=”2″ BorderBrush=”Gray” Template=”{StaticResource Template2}” prism:RegionManager.RegionManager=”{Binding Value, Source={StaticResource ObservableRegionManager}}”>

      And then, inside the template, you can obtain the RegionManager like this:
      <ItemsControl MinWidth=”400″ MinHeight=”100″ prism:RegionManager.RegionName=”MainRegion” prism:RegionManager.RegionManager=”{TemplateBinding prism:RegionManager.RegionManager}” />

      I hope this help,
      Damian Cherubini

    • dcherubini

      Hi Andrew,

      This workaround should work in both WPF and Silverlight.
      The sample application provided here was developed using Silverlight 4 and Prism 4; however, as far as I know this should not have any compatibility issues with Silverlight 5.

      The problem you are describing seems to be related to your Silverlight installation; therefore, I believe you will able to find support for this in the Silverlight forums: http://forums.silverlight.net/.

      Regards,
      Damian Cherubini

    • Pingback: WPF Prism Region inside a tab control isn't in the Region List | PHP Developer Resource()

    • v

      hi, the sample code you have isn’t available anymore.

      thanks.

    • v

      found it. however, i’m wondering how i can implement this in MEF.

    • dcherubini

      Hi v,
      As far as I know, the only difference when using MEF is that you would need to export the behavior to MEF. As this behavior is based on the RegionActiveAwareBehavior, I believe it could be exported using a similar approach:

      [Export(typeof(RegionManagerAwareBehavior))]
      [PartCreationPolicy(CreationPolicy.NonShared)]
      public class MefRegionManagerAwareBehavior : RegionManagerAwareBehavior
      {
      }

      Also, remember to register the behavior in the RegisterDefaultRegionBehaviors method of the MEF bootrapper.

      Regards,
      Damian Cherubini

    • v

      Hi Damian,

      Thanks, i found out that using this was an important part

      protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
      {
      var regionAdapterMappings = base.ConfigureRegionAdapterMappings();
      IRegionBehaviorFactory factory = ServiceLocator.Current.GetInstance();
      regionAdapterMappings.RegisterMapping(typeof(ContentPresenter), new RegionWrapperAdapter(factory));
      return regionAdapterMappings;
      }

      it worked but having another nested region inside that injected view in the datatemplate is causing me complications :p

    • Anand

      Hi,
      I used your solution to navigate to scoped regions and i was able get different instances of region manager to those regions. Everything is great till now. Now, i inject different views within this region using RequestNavigate method. Whenever the view i inject here has inner regions and switch views, then i get an exception saying \Microsoft.Practices.Prism.Regions.Behaviors.RegionCreationException\. This is really troubling me to get forward. Please help me on this.

      To give you a clear picture. i have region Named TabRegion (scoped region) and i have HomeView there. Within HomeView i have another region ‘MainModuleRegion’ and 3 buttons to navigate 3 different views namely (ContactView, MailView and ProfileView) into this region. All these works fine. But, my ‘MailView’ has a region (MailViewRegion) and i inject MailViewDetatailsView and MailViewSummaryView based on toggle button. Now, if i navigate to MailView and then ProfileView and then back to MailView i get the aforementioned exception.

      But if i navigate from ContactView to ProfileView and then back to ContactView i dont get any exceptions. I think it has to do with clearing the inner regions in the MailViewRegion.

      Please help me on this, i have struggling for a few days now.

      Thanks
      Anand

    • dcherubini

      Hi Anand,
      .
      I believe your problem is related to the inner MailViewRegion being registered in the same RegionManager each time you navigate to a MailView. In this case, this is the parent RegionManager that contains the TabRegion.
      .
      A possible approach to solve this could be to inject the MailView with its own scoped RegionManager, so that the MailViewRegion would be registered in it and not in the parent RegionManager. Then the exception wouldn’t be throw as each MailViewRegion would be registered in the RegionManager of the corresponding MailView.
      .
      However, as far as I know, currently Prism doesn’t support navigating to a view and attaching a new scoped RegionManager to it. So it’s not possible to create a scoped RegionManager for the MailView when using navigation.
      If you cannot change your implementation to use view injection instead of navigation (in order to able to attach new RegionManagers) then you could check the following blog post by Agustin Adami where he proposes an approach to use navigation and scoped regions:
      http://blogs.southworks.net/aadami/2011/11/30/prism-region-navigation-and-scoped-regions/
      .
      Regards,
      Damian Cherubini

    • Zoltan

      Hi,

      I rewrote this example (RegionManagerAwareBaheviorSample) in WPF, but unfortunately does not work for me either ViewWithTemplate nor ViewWithTemplateAndScopedRegion. The region is not created in the Region Manager’s region list. Everything is the same as the sample application, because it comes copied, except for the reference dlls, and the app.xaml.cs of WPF.

      What worse can it?

      Thanks, Zoltan Kiss

    • dcherubini

      Hi Zoltan,
      .
      Sorry for the delay.
      Right now, the only thing that comes into my mind that could be causing the problems you are mentioning when migrating the sample from Silverlight to WPF, is that the App.xaml file might not be modified to run and initialize a Prism application.
      .
      Remember that in order to initialize a Prism application in WPF you need to remove the StartUri XAML parameter of the App.xaml file and override the OnStartup method in the App.xaml.cs file to create and run your bootstrapper.
      .
      Please, let me know if this solves the problem.
      .
      Regards,
      Damian Cherubini

      • Dima Shapiro

        Hi Damian,
        I am doing the same as Zoltan (taking your comment into account) but no luck. Would it be possible that you’ll take a look into the code?
        Regards,
        Dima

    • http://cyounes.com/ cYounes

      Does it work with Prism5 ?

      • Gabriel Ostrowsky

        Based on my understanding, Silverlight would not be supported yet in Prism 5. However, I will get into it and migrate the sample to WPF in order to be able to updated it afterwards.