Creating a multple window app like Outlook

Topics: Prism v4 - WPF 4
Dec 8, 2014 at 1:10 PM
Hi,

I'm wondering if Prism V5's Interactivity feature can be used to create an Outlook-like app where the user can click "New Email" button on the shell and a new email window pops up. The user can have as many email window as he likes. The email window itself can create a new window through the "New" button in the backstage view.

I read the Multiple instances of a window (view) discussion, but the Matias Bonaventura's zip file seems to be broken.

I also read the Multiple windows in a shell discussion where someone said that it could also be done by creating a region adapter for the Window class.

What is the most appropriate way to design such an application?

Thanks
Dec 19, 2014 at 8:42 PM
You should check out my Building IG Outlook tutorial. It's a little old, but it solves the problem you are seeking to solve.

http://brianlagunas.com/?s=IG+Outlook
Dec 20, 2014 at 1:50 PM
Hi Brian,

Thanks for the info. I have watched the videos. But the problem I wanted to solve is "multiple shells". Your tutorial is excellent, but the app is still single-windowed. The closest I found is how to build an outlook style application by Erwin van der Valk. But it introduces another layer of abstraction such as "Use Case" and "Application Model" and so is harder to understand.

Our team is now considering an approach with multiple Bootstrappers. If I simply call new Bootstrapper().Run() on a button click, a new window is displayed with all dependencies duplicated, which is not good because some of the dependencies need to be a singleton in the app. So, we are creating a secondary Bootstrappers to solve the issue. Second, third, and n-th window seem to work fine with exactly the same behavior as the main window so far.

Is there anything bad looming with this approach? Do you have any further suggestions?

By the way, I have also watched your Pluralsight's two tutorials on Prism. They were very helpful.

Thanks
Jan 12, 2015 at 1:51 PM
Sorry, but I never got a notification that you responded. DO NOT use two bootstrappers. You would simply use scoped regions, and use a dialog service to show your multiple windows.

Here is a version of IG Outlook that is more complete with the functionality you are looking for:

http://www.infragistics.com/sample-applications/infragistics-wpf-igoutlook

You can download the source code form here:

http://www.infragistics.com/samples/wpf/application-samples

I hope this helps.
Jan 12, 2015 at 1:51 PM
Edited Jan 12, 2015 at 2:51 PM
Duplicate post.
Jan 15, 2015 at 3:12 PM
Edited Jan 15, 2015 at 3:50 PM
Hi Brian,

I carefully studied the IG Outlook code and applied a similar approach (scoped regions and a dialog service) to a reasonably complex Prism app (Mike Taulty's Email client app (ported to WPF)) that has been so far single-shelled. I encountered the following issues:
  1. Calling regionManager.RequestNavigate() in a secondary shell's view model classes actually changes the view in the very first shell. This is because the view model class's constructor asks the Unity container to inject the IRegionManager, which is the one for the very first shell. I would need to add some code for every view model class that calls the RequestNavigate() method to get the scoped region manager that is dedicated for that particular shell. A shared service with a dictionary could do it, but it would certainly complicate the software because those multiple scoped region managers need to be carefully managed.
  2. When I created a new Shell and called the scopedRegionManager.RequestNavigate() with the main content region, I had to also issue RequestNavigate() to all nested regions that are defined inside the main content region to show all views. In the single-shelled version, those were done in each module's InitializeModule() method. But now, I need to know those implementation details beforehand.
  3. With the scoped region manager, the RegisterViewWithRegion() does not work. The app has a lot of "static" area where the view discovery works just fine. The IG Outlook code never uses it because of the scoped region manager, I presume. I had to change to RequestNavigate() calls, which has an additional burden of registering the view classes with the Unity container as "object". With the view discovery, we don't need to register views to the Unity container.
  4. The EventAggregator also needs to be scoped. I probably should use the "filter" parameter for the Subscribe() method and provide the scoped region manager as the key for the filter. But again, I don't know how to get the scoped region manager in view model classes.
The secondary bootstrapper approach has none of those issues with the view discovery, region navigation, and event aggregation because all of those services are dedicated to the newly created Shell only, thanks to the also duplicated Unity container. I created and registered a separate EventAggregator in the main bootstrapper, named it "Global", and then passed it to every secondary bootstrapper as a singleton. This "global" event aggregator is used to create a new instance of the Shell. I can easily add [Dependency("Global")] attribute before the event aggregator parameter in the view model constructors so that the secondary Unity can inject the global one, rather than its own one. So far, it's been working beautifully. Very little code was needed to support multiple shells (again, so far).

You used rather strong words - "DO NOT use two bootstrappers". Would you please explain why it is such a bad idea?

Thanks
Jan 15, 2015 at 4:27 PM
You have just completely over-complicated your entire app architecture. You get around the scoped region navigation issue by creating a simple IRegionManagerAware custom region behavior. This solves just about all of your problems with navigating within scoped regions. It also allows any View or VM to get access to the proper RegionManager by implementing a simple interface.

Right now, you are using a work around that will just cause you to have make more workarounds for things that are normally supported, just like your "Global" EventAgregator, because you broke the ability to use the EventAggregator by having two bootstrappers. Everything that gets created in the bootstrapper has now been duplicated as separate instances, and when you click the button to show another shell, you have three, click it again and you have four, and so on. This include modules being unnecessarily loaded multiple times, type registrations, RegionAdapter mappings, etc. I am pretty sure all of that is staying in memory as the singleton objects have been rooted in your app. There are a number of other possible issues you may run into that isn't obvious at the moment.

By all means, use two bootstrappers if you like. Maybe your app is simple enough to where you won't run into many problems. But, I personally would never advise any of my clients to take that approach.
Jan 18, 2015 at 1:14 AM
Edited Jan 18, 2015 at 1:19 AM
I completely missed the IRegionManagerAware interface with the RegionManagerAwareBehavior from the IG Outlook sample. So, instead of requesting the DI container to inject the IRegionManager, the idea is to let the behavior to “inject” the view model class the scoped region manager behind the scene. I got that.

That completely solved the issue no.1. The issue no. 4 was also solved once the scoped region manager was available in the view model classes.

But the issue no. 2 still remains. A commenter (naskew) in Stack Overflow also asked the same question with no answers provided.

At this point, our team decided to embark on the uncharted territory with the multiple bootstrapper approach until we encounter “a number of other possible issues you may run into that isn't obvious at the moment.” This is because a lot less modifications were needed to change a single-shelled app into multiple shelled than using the scoped region manager approach taken in the IG Outlook sample. And thanks to you, we now know how to change the code to work with the scoped region managers.

Memory footprint may be an issue, but most of the users would open just several windows, rather than hundreds.

Anyways, thank you very much for your invaluable suggestions. I learned a lot about Prism.
Jan 18, 2015 at 4:44 AM
Actually, loading dependent views is done with a simple region behavior and a custom attribute. This is all it take me to accomplish this task. I simply attribute my view with all the views that must be loaded into their respective regions. Then when this view is navigated to, all the dependent views are also injected into their regions. Oh, and if they implement a certain interface, they will also share the same data context (ViewModel).
[DependentView(typeof(ViewBTab), "RibbonTabRegion")]
[DependentView(typeof(ViewA), "SubRegion")]
[DependentView(typeof(ViewC), "SubRegion2")]
public partial class ViewB : UserControl, ISupportDataContext
{ ... }
Multiple bootstrappers not needed :0)
Jan 19, 2015 at 2:36 PM
I see how that would work after looking at the IG Outlook’s XamRibbonRegionBehavior and RibbonTabAttribute code. Once the custom RegionBehavior knows the region and view names, I could call Region.RegionManager.RequestNavigate() from within the custom RegionBahavior class.

However, some modules require much more initializations than just calling a RequestNavigate(). I needed to resolve business classes, create view models with the resolved business classes, create corresponding views, and finally add them to the region to use view injection. In other words, I needed the DI container and needed to call a part of IModule.Initialize() of every module that is used inside the shell.

So, rather than creating a separate DialogService as is done in the IG Outlook sample, I assigned the responsibility to the Bootstrapper and created an interface for the module classes so that I can separate the IModule.Initialize() in two sections – one that registers types and the other that does everything else including initializing the regions with a given region manager.

This scheme seems to be working very well. Now, I don’t need the custom behavior. The part of the Bootstrapper code is as follows:
protected override void InitializeModules()
{
    var regionManager = this.Container.Resolve<IRegionManager>();
    this.Container.RegisterType<object, MainView>("MainView");
    regionManager.RequestNavigate(RegionNames.MainRegion, "MainView");

    var eventAggregator = this.Container.Resolve<IEventAggregator>();
    const bool KeepMeAlive = true;
    eventAggregator.GetEvent<CreateNewWindowEvent>().Subscribe(this.CreateNewShell, ThreadOption.UIThread, KeepMeAlive);

    base.InitializeModules();
}

private void CreateNewShell(string obj)
{
    var newShell = this.Container.Resolve<ShellView>();
    var regionManager = this.Container.Resolve<IRegionManager>();
    var scopedRegionManager = regionManager.CreateRegionManager();
    RegionManager.SetRegionManager(newShell, scopedRegionManager);
    scopedRegionManager.RequestNavigate(RegionNames.MainRegion, "MainView");

    this.InitializeModulesForTheNewShell(scopedRegionManager);

    newShell.Show();
}

private void InitializeModulesForTheNewShell(IRegionManager scopedRegionManager)
{
    foreach (var moduleInfo in this.ModuleCatalog.Modules)
    {
        var moduleType = Type.GetType(moduleInfo.ModuleType);
        var module = this.Container.Resolve(moduleType) as IModuleWithMultipleShells;
        if (module != null)
        {
            module.InitializeRegion(scopedRegionManager);
        }
    }
}
Since the above is done in the Bootstrapper, the ModuleCatalog is easily available. The InitializeModulesForTheNewShell() calls InitializeRegion() that is to be implemented in the IModule classes if need be. Below shows an example of such a module:
public void Initialize()
{
    this.container.RegisterType<object, ViewA>("ViewA");
    this.InitializeRegion(this.regionManager);
}

public void InitializeRegion(IRegionManager rm)
{
    rm.RequestNavigate(RegionNames.ViewARegion, "ViewA");
}
The IModuleWithMultipleShell interface looks like this:
public interface IModuleWithMultipleShells
{
    void InitializeRegion(IRegionManager scopedRegionManager);
}
With this implementation, all of my issues with the scoped region manager except for #3 are gone. I still miss view discovery because it’s simpler, but I can live with that.

With that said, our team will go with the multiple bootstrapper approach because it needs a lot less code and less confusing. When we see an unexpected problem related to this decision in future, we should be able to switch to the scoped region manager without much pain.

Again, thank you for your extremely helpful pieces of advice. I really appreciate it.
Jan 19, 2015 at 3:46 PM
Good luck and happy coding :0)