Reputation: 155
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.
Upvotes: 0
Views: 899
Reputation: 452
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.
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).
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);
Upvotes: 1