20

I try to create an application that has a window behaviour as MS Office, for example Word/Excel. The user opens the application and when clicking new, a completely new window shall appear with the look of the application.

The closest that I found so far is this: Link

However, here the shells are shown at application start. How to do this by command, or maybe there is a completely different way to achieve this?

Edit: I have also found the following now: Link, but where and how to call this code?

Jon
  • 428,835
  • 81
  • 738
  • 806
ps23
  • 339
  • 1
  • 4
  • 11

2 Answers2

34

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.

Jon
  • 428,835
  • 81
  • 738
  • 806
  • Thanks, was a great help, my implementation below – ps23 Mar 17 '11 at 14:08
  • 3
    There are some factual inaccuracies in this answer... The section "UI composition details" is unnecessary. When a region name is assigned to a control, a region is automatically created. The created region is automatically added to the region manager nearest to the region up the logical tree. This is how scoped region managers work. So all you need to do is ensure your new shell instance has its own region manager. Or create a uniquely named region in each shell, with a scoped region manager instance. Whichever floats your boat. – Mark Oct 25 '11 at 10:49
  • 1
    I can see how this would assign a new region manager to the shell, what is less clear to me is how this will help. My understanding is that a module will typically register its views with the region manager passed to it when it (the module) was constructed. I presume you will therefore need to initialise the correct modules for each shell passing in the uniquely created region manager. How does this work? – naskew Aug 17 '13 at 07:22
  • I followed the exact same steps as the @Jon mentioned but am having issues with the modules tied to shell2. In the module, I associate the module with a region. For example, public void Initialize() { var menuRegion = this._regionManager.Regions["MenuRegion"]; menuRegion.Add(new MenuView(), "MenuRegion"); }. This code automatically executes when I add module to module catalog. Unfortunately my 2nd shell is not ready/displayed then and this throws an exception. How do I take care of this situation? – Shreyas Jul 10 '14 at 16:09
  • Your solution will work, but only in case of single assembly. Also, it breaks IoC, as command singleton requires reference to shells. This way, it is not possible to split modules into seprate assemblies, as that would cause circural reference with shell assembly. To remove the coupling, command services managing new shells should be injected into modules. – DominicusPlatus Apr 10 '16 at 15:59
0

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);
        }
    }
ps23
  • 339
  • 1
  • 4
  • 11