4
Vote

Navigation and ScopedRegions

description

Hi,
 
It would be helpful if Prism provided support from out of the box for Region Navigation using Scoped Regions.
 
This topic has been discussed with a possible workaround in the following thread (http://compositewpf.codeplex.com/discussions/279224)
 
Thanks,
 
Agustin Adami
http://blogs.southworks.net/aadami

file attachments

comments

james1301 wrote Nov 22, 2011 at 9:12 AM

Needed for navigation to be at all useful for applications that aren't extremely simple. I don't understand how this has been overlooked. Workaround seems a bit of a mess.

aadami wrote Nov 23, 2011 at 8:04 PM

Hi,

We thought of an alternative approach which I believe might be a more elegant solution. The approach consist of obtaining the region manager returned by the call of the Region.Add method, through the NavigationResult passed in the navigation callback from the RequestNavigate method instead of using a shared dictionary to save it.

As the result you will be able to navigate with scoped regions, like in the following code snippet:
this.regionManager.RequestNavigate(
            "MainRegion", 
            new Uri("HelloWorldView?createRegionManagerScope=true", UriKind.Relative), 
            (result) =>
            {
                var myRegionManager = result.ExtractRegionManager();
                myRegionManager.RequestNavigate("NestedRegion", new Uri("View1", UriKind.Relative));
            });
Below you will find the details of this implementation:

To achieve this scenario, the first thing we needed was a NavigationResult that allows us to pass the region manager instance. For this we created a CustomNavigationResult class that inherits from NavigationResult, but with another constructor which added an IRegionManager parameter, that sets its RegionManager property.
Next, to obtain the region manager returned by the call of the Region.Add method, we needed the LoadContent method in the RegionNavigationContentLoader to return a Tuple instead of only an object view. Therefore we created a CustomRegionNavigationContentLoader and its corresponding interface. Also we changed the name of the parameter passed with the view when navigating to "createRegionManagerScope" and verify if its value is “true” to specify if a new region manager must be created (in case scoped regions are used).

This changes can be seen in the following code snippet:

public Tuple LoadContent(IRegion region, NavigationContext navigationContext)
    {
(…)
if (view != null)
        {
            return new Tuple(view, region.RegionManager);
        }

        view = this.CreateNewRegionItem(candidateTargetContract);

        bool createRegionManagerScope = navigationContext.Parameters["createRegionManagerScope"] == "true";

        var rm = region.Add(view, null, createRegionManagerScope);

        return new Tuple(view, rm);
    }
(…)

Also, as the ExecuteNavigation method in the RegionNavigationService is the one that calls the LoadContent method, we had to create a CustomRegionNavigationService, to let it receive the returned tuple. In this same method instead of passing the default NavigationResult to the navigationCallback we pass our CustomNavigationResult with the returned region manager as a parameter.

You can find this modifications in the following code snippet:

private void ExecuteNavigation(NavigationContext navigationContext, object[] activeViews, Action navigationCallback)
    {
(…)
Tuple tuple = this.regionNavigationContentLoader.LoadContent(this.Region, navigationContext);
            // Raise the navigating event just before activing the view.
            this.RaiseNavigating(navigationContext);

            this.Region.Activate(tuple.Item1);

            // Update the navigation journal before notifying others of navigaton
            IRegionNavigationJournalEntry journalEntry = this.serviceLocator.GetInstance();
            journalEntry.Uri = navigationContext.Uri;
           this.journal.RecordNavigation(journalEntry);

            // The view can be informed of navigation
            InvokeOnNavigationAwareElement(tuple.Item1, (n) => n.OnNavigatedTo(navigationContext));

            navigationCallback(new CustomNavigationResult(navigationContext, true, tuple.Item2));
(…)
}

Finally to avoid modifying the prism library we used the export attribute at the top of our custom classes (CustomRegionNavigationContentLoader and CustomRegionNavigationService) which allows the MefBootstrapper to provide these classes as a default implementation.

How to use:

If you apply this changes, then you will be able to call the RequestNavigate method with a delegate method as the navigation callback, which will receive a NavigationResult. The only problem is that you will have to cast the NavigationResult to our CustomNavigationResult class, in order to obtain the desired regionManager. Hence we created an extension method called ExtractRegionManager in the NavigationResultExtension class, which will make things easier.

For those interested, we prepared a sample application that portrays the aforementioned modifications. This sample shows two instances of a view being navigated to using the RequestNavigate method, inside a region in a TabControl. Each of these views has an inner region, and because this region names will be duplicated they must be defined as scoped regions. You can find the application sample attached under the name NavigationWithScopedRegionSample.

I hope you find this useful

Agustin Adami
http://blogs.southworks.net/aadami

james1301 wrote Nov 28, 2011 at 11:01 AM

Thank you, this is very useful. I'm probably saying something very simple here, but how would you pass the new regionmanager to the newly scoped viewmodel? There isn't a way to get this from the navigationcontext is there?

I am wanting to navigate within the scoped viewmodel changing the region within.

aadami wrote Nov 30, 2011 at 8:00 PM

James,

I'm glad you find this useful. Regarding your question, you might find this blog post useful (http://blogs.southworks.net/aadami/2011/11/30/prism-region-navigation-and-scoped-regions/), where we implemented the aforementioned suggestions and also added the posibility to pass the new RegionManager to the corresponding ViewModel. To achieve this we used the RegionManagerAwareBehavior proposed in the following blog post from Damian Cherubini (http://blogs.southworks.net/dcherubini/2011/11/10/regions-inside-datatemplates-in-prism-v4-using-a-region-behavior/).

I hope you find it helpful

Agustin Adami
http://blogs.southworks.net/aadami

ENikS wrote Mar 30, 2012 at 4:37 PM

I would like to offer another workaround for this problem.
As I see it this issue could be divided into two separate problems: letting know the loader to add view to the region using scope, and accessing correct RegionManager in scoped view.
To indicate requirement for scoped region we could use interface of custom attribute on the view type. This will allow us to add three extra lines to content loader and accomplish the result:
view = this.CreateNewRegionItem(candidateTargetContract);
// Check if scoped region is required

IProvideRegionScopeInfo info = (view as IProvideRegionScopeInfo)
?? (ScopedRegionManagerAttribute)view.GetType()
                                     .GetCustomAttributes(typeof(ScopedRegionManagerAttribute), false)
                                     .FirstOrDefault();
if (null == info)
region.Add(view);
else
region.Add(view, info.ViewName, info.CreateRegionManagerScope);

return view;


Accessing the correct manager is even easier. When view is being added to the region RegionManager.RegionManager attached property is added and updated with reference to instance of the manager responsible for that view. So if the view is added with scope this property will have reference to correct scoped instance of the manager.
All we have to do is to bind to that attached property, this is what they are for, and uses the manager in View or ViewModel.
I’ve modified sample project posted earlier to implement described method.
If you need more explanation please see it here: http://www.codeproject.com/Articles/320673/PrismScopedRegions

ENikS wrote Mar 30, 2012 at 9:35 PM

Adding sample project to illustrate solution

du999 wrote Oct 22, 2013 at 4:44 AM

Hi Agustin,Thanks for the solutions. I tried to use it with Unity, but no luck.

Our requirement is something similar to the problem raised by user JanWaiz in following discussionI mainly used Prism with Unity and I know nothing about MEF. The project I am currently working on is in final stage but only one critical section require same view with 2 regions load into a tab container multiple times. Changing to MEF is not an option due to time limitaion.

Please help ! Would you be able to help to convert this NavigationWithScopedRegionSample.zip to use with Unity ?

GOstrowsky wrote Oct 24, 2013 at 6:50 PM

Hi du999,

You can find the NavigationWithScopedRegionSample solution using UnityCootstrapper attached.

The main differences between both containers that would need to be changed are related on how the Modules and Items are being registered.While MEF uses the export attribute to automatically register the Views and Services, Unity needs explicit registration from the Bootstrapper and the Module's Initialize() methods.

You may see the aforemention registrations on the following sample's methods:

```C#Bootstrapper.cs

protected override IModuleCatalog CreateModuleCatalog(){ return new ConfigurationModuleCatalog();}

protected override void ConfigureModuleCatalog(){ Type helloWorldModuleType = typeof(HelloWorldModule.HelloWorldModule); ModuleCatalog.AddModule(new ModuleInfo(helloWorldModuleType.Name, helloWorldModuleType.AssemblyQualifiedName));}

protected override void ConfigureContainer(){
 RegisterTypeIfMissing(typeof(IRegionNavigationService), typeof(CustomRegionNavigationService), false);     RegisterTypeIfMissing(typeof(ICustomRegionNavigationContentLoader), typeof(CustomRegionNavigationContentLoader), false);

 base.ConfigureContainer();}```
C#HelloWorldModule.cs

public HelloWorldModule(IRegionViewRegistry registry, IRegionManager regionManager, IUnityContainer container){ this.container = container;...}

public void Initialize(){ this.container.RegisterType("HelloWorldView"); this.container.RegisterType("View1"); this.container.RegisterType("View2");...}


I hope this helps,

Gabriel Ostrowskyhttp://blogs.southworks.net/gostrowsky