Search code examples
c#wpfxamlprismregion-management

Adding Views to the ItemsSource of an ItemsControl via a Region of a Scoped RegionManager


I am trying to populate the ItemsSource of a ComboBox (a derivative of ItemsControl) via a region.

View

The scoped RegionManager (found on the View Model) is assigned to the view via prism:RegionManager.RegionManager="{Binding RegionManager}".

MainWindow.xaml

<Window x:Class="Applications.Testing.Wpf.RegionCreationTester.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Applications.Testing.Wpf.RegionCreationTester"
    xmlns:prism="http://www.codeplex.com/prism"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525"
    prism:RegionManager.RegionManager="{Binding RegionManager}"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <ComboBox prism:RegionManager.RegionName="{x:Static local:RegionNames.itemsControlRegion}"/>
    </Grid>
</Window>

MainWindow.xaml.cs

namespace Applications.Testing.Wpf.RegionCreationTester
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IModelled<MainWindowViewModel>
    {
        public MainWindowViewModel ViewModel
        {
            get
            {
                return (MainWindowViewModel)DataContext;
            }
            set
            {
                DataContext = value;
            }
        }

        public MainWindow()
        {
            InitializeComponent();

            ViewModel.PopulateItemsControl();
        }
    }
}

ViewModel

The view model is assigned to the view's DataContext via prism:ViewModelLocator.AutoWireViewModel="True".

MainWindowViewModel.cs

namespace Applications.Testing.Wpf.RegionCreationTester
{
    public class MainWindowViewModel
    {
        /// <summary>
        /// Gets the <see cref="RegionManager"/> scoped to this control.
        /// </summary>
        /// <remarks>Exists so that child controls can register regions for their own child controls which are also child controls in this control.</remarks>
        public RegionManager RegionManager { get; } = new RegionManager();

        /// <summary>
        /// Adds some child views to the <see cref="RegionNames.itemsControlRegion"/>.
        /// </summary>
        /// <remarks>Normally these views would be resolved using an IoC container but this have been omitted for brevity.</remarks>
        public void PopulateItemsControl()
        {
            var region = RegionManager.Regions[RegionNames.itemsControlRegion];
            region.Add(new TextBlock { Text = "Item #1" });
            region.Add(new Button { Content = "Item #2" });
        }
    }
}

RegionNames.cs

namespace Applications.Testing.Wpf.RegionCreationTester
{
    public static class RegionNames
    {
        public const string itemsControlRegion = "collectionRegion";
    }
}

Bootstrapper

App.xaml

<Application x:Class="Applications.Testing.Wpf.RegionCreationTester.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"/>

App.xaml.cs

namespace Applications.Testing.Wpf.RegionCreationTester
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            new RegionCreationTesterBootstrapper().Run();
        }
    }
}

RegionCreationTesterBootstrapper.cs

namespace Applications.Testing.Wpf.RegionCreationTester
{
    public class RegionCreationTesterBootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
            => new MainWindow();

        protected override void InitializeShell()
        {
            base.InitializeShell();

            (Application.Current.MainWindow = (Window)Shell).Show();
        }
    }
}

Once all the region population has occurred and the application is about to run I get a Prism.Regions.UpdateRegionsException containing an InnerException with the message "Region with the given name is already registered: collectionRegion" on the line in App of new RegionCreationTesterBootstrapper().Run(). The last line in my code I am able to get a breakpoint hit for is the new MainWindow() in CreateShell after the call to the constructor for MainWindow has exited. Why am I being told the region is already registered when I am only trying to register it once? I have set breakpoints in the MainWindow's constructor to indeed confirm that it only being created once and even if it weren't, the RegionManager to which it is scoped should prevent this exception from occurring. What have I missed?

UPDATE

I have just commented out the code within PopulateItemsControl and found that the exception is thrown even if only one view is added to the region and stranger still, if no views are added to the region but the region is accessed (as done in the line: var region = RegionManager.Regions[RegionNames.itemsControlRegion];). Therefore the issue is now to do with accessing an existing region on a scoped RegionManager for View Injection in order to add views to it; I'm not sure why accessing a region from the RegionManager would change its state, this seems like a bug in Prism or perhaps something to do with lazy enumeration.


Solution

  • Well, you are doing it twice, you're just not aware. When you set RegionName attached property on your ComboBox, an event hanler that will create the region with given name is attached (that's static part of RegionManager). When the instance of RegionManager you instantiated in your VM tries to access the region collection, indexer first calls a static method on RegionManager class that raises the event. The global RegionManager instance that got the task of creating the region (when you used RegionName attached property) has not finished it's job - window hasn't been loaded when you try to access the region with your instance, the handler has not been removed and it's called again. If you called your PopulateItemsControl method after the window has loaded (say in MainWindow's Loaded event handler), you wouldn't get the exception, but your code would not work as you expect. That is because your instance of RegionManager is not "handling" your collectionRegion, global RegionManager is.

    Injecting RegionManager instance

    If you need a RegionManager instance in your VM, use constructor injection.

    public class MainWindowViewModel : BindableBase
    {
        private IRegionManager rm;
    
        public MainWindowViewModel(IRegionManager manager)
        {
            this.rm = manager;
        }
    
        public void PopulateItemsControl()
        {
            var region = rm.Regions[RegionNames.itemsControlRegion];
            region.Add(new TextBlock { Text = "Item #1" });
        }
    }
    

    Dependency injection container (Unity or whatever you're using) will resolve IRegionManager instance when creating the VM (PRISM is doing that job for you anyway, you're not instantiating it yourself).

    Region scopes

    RegionManager keeps a collection of regions and does not allow regions with same names. So, unless your window is going to have multiple of those ComboBoxes that all have a region named collectionRegion, the RegionManager (the global one) is fine. If your collectionRegion is going to have instances of same view class, that all define another region within itself, then you need region scopes - RegionManager instances with their own scope for those views. In that case, the Add method can create local instance of RegionManager for that view:

    IRegion collectionRegion = this.regionManager.Regions["collectionRegion"];
    bool makeRegionManagerScope = true;
    IRegionManager localRegionManager =
        collectionRegion.Add(view, null, makeRegionManagerScope);