How to do multiple shells in my Prism app (like MS Office)? How to do multiple shells in my Prism app (like MS Office)? wpf wpf

How to do multiple shells in my Prism app (like MS Office)?


Creating multiple shells is the correct idea. You just need to take care of the details appropriately.

When and how to create a new Shell

The Prism way is, of course, to have a DelegateCommand handle the creation of a new shell. Considering that this command does not strictly belong to any particular ViewModel (I 'd say it has an application-wide scope), it feels better to me to have a public static class ApplicationWideCommands with a CreateNewShellCommand static property. You can then either bind to it from XAML with {x:Static} or execute it from code-behind as needed.

This command would need to take care of two things:

  1. Create the new Window (actually, a Shell)
  2. Instantiate a new IRegionManager for the new shell, so that there is no conflict in region names between the regions in the existing shell and those in the new shell
  3. Instruct the regions in the new shell that they belong to the new IRegionManager

I 'll tackle this last-to-first, because it's easier to explain.

Giving your new Shell a new RegionManager

When declaring a region in Prism you can declare the region manager to use in addition to the region name. Normally you don't need to do this, but here we need to choose which RegionManager to use because region names must be unique in the scope of a single region manager. Since the region names are hardcoded inside the XAML of the Views, and it would be a major pain to assign them another way, we need to change the other half of the equation: the region manager instance used by each shell. So inside Shell.xaml there might be something like this:

<ContentControl  regions:RegionManager.RegionManager="{Binding RegionManager}"  regions:RegionManager.RegionName="ExampleRegion"/>

This will instruct the "WorkspaceRegion" in each shell that it belongs to the IRegionManager provided by the binding. Since the shell usually has no DataContext, we can declare the RegionManager property in the shell class itself:

public partial class Shell : Window{    public Shell(IRegionManager regionManager)    {        this.RegionManager = regionManager;        InitializeComponent();    }    public IRegionManager RegionManager { get; private set; }}

So now we just need to make sure that each Shell instance gets its own RegionManager. For the "first" shell, this will be done by the BootStrapper. (The code below uses the DI container to resolve objects, and the examples use the UnityContainer. If you use MEF for dependency injection just mentally translate to the equivalent code.)

protected override DependencyObject CreateShell(){    // I am assuming you have a reference to the DI container    var regionManager = this.Container.Resolve<IRegionManager>();    return new Shell(regionManager);}

For the other shells, it will be done by the CreateNewShellCommand:

private static ExecuteCreateNewShellCommand(){    // I am assuming you have a reference to the DI container   var regionManager = this.Container.Resolve<IRegionManager>();   ver newRegionManager = regionManager.CreateRegionManager();   var shell = new Shell(newRegionManager);   // The rest is easy, for example:   shell.Show();}

There is an important caveat here: The RegionManager is registered into the container as a singleton. This means that whenever you resolve IRegionManager you will be getting back the same instance. For this reason, we create a new instance by calling the IRegionManager.CreateRegionManager method (this applies Prism v4; I 'm not sure about v2).

At this point, you know how to create any number of new Shell instances and wire up the regions accordingly.

UI composition details

The final detail you need to take care of is that all regions hosted in each shell, no matter how deep in its visual tree, have to bind to the same RegionManager.

This means that you have to explicitly set the region manager to use like we did in the ContentControl example above for all regions in all Views in your application. Fortunately, this is done quite easily because:

  1. All Views will end up being descendants of a Shell in the visual tree
  2. The Shell already exposes the correct RegionManager as a property, so we can bind to that

You would do so like this:

<ItemsControl  regions:RegionManager.RegionManager="{Binding RegionManager, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Shell}}}"  regions:RegionManager.RegionName="AnotherRegion"/>

All set!

You now should be ready to go.


Here the implementation that I now use to create multiple Shells with multiple EventAggregators. I pass the container in the bootstrapper. In my special case, the sub-module listens to the event in order to get the path parameter on which this Shell operates. The sub-module can then create the node data file at the path location or load from the path location depending on the parameter details.

public static class AppCommands    {        public static Container;        public static ICommand NewCommand = new DelegateCommand(CreateShell);        private static void CreateShell(object state)        {            var regionManager = Container.Resolve<IRegionManager>();            var newRegionManager = regionManager.CreateRegionManager();            var neweventAggregator = new EventAggregator();            Container.RegisterInstance<EventAggregator>(neweventAggregator);            var shell = new Shell(newRegionManager, neweventAggregator);            shell.Show();            SomeEventParameter parameter = new SomeEventParameter ();            //Add sth to the parameter here            neweventAggregator .GetEvent<SomeEvent>().Publish(parameter);        }    }