Matching contract names and view names in Prism region navigation

I’ve seen some questions and issues in the Prism forum, mentioning that they receive exceptions such as “View already exists in region” when trying to navigate back to a view in a region. This error seems to happen when exporting a view with a contract name (string) different than the name of the view’s type.

Also, there have been some discussions where people confuse the contract type with the name a view is added to a region with, and so they try to retrieve it using the Region.GetView method, which misleadingly returns null.

In this post I’ll propose a possible solution in the form of a custom region navigation content loader, which might address both concerns.

Contract name vs type name

When navigating back to a view in a region, once the navigation request is confirmed, the RegionNavigationContentLoader.LoadContent method should return the view that is target of the navigation request, which should then be activated by the navigation service (this can be seen in the RegionNavigationService.ExecuteNavigation method; you can check more about the region navigation pipeline in the following blog post from Karl Shifflett).

To find the correct view, the content loader calls the GetCandidatesFromRegion method, which returns the set of candidates by comparing the requested contract name with the type Name or FullName of the views present in the region. I speculate this is because, for a given instance of an object, it’s not possible out of the box to obtain the contract name with which it has been registered in the container.

image

Hence, when registering views in the container with a contract name that differs from the view type Name or FullName, no matches will be found, and a new instance will be retrieved from the container. As a result it’s common to receive the “View already exists” exception (raising the NavigationFailed event) if your views were defined as singletons, or navigate to a new instance of the view if it weren’t; both of them are probably not the desired results.

1 protected virtual IEnumerable<object> GetCandidatesFromRegion(IRegion region, string candidateNavigationContract) 2 { 3 if (region == null) throw new ArgumentNullException("region"); 4 return region.Views.Where(v => 5 string.Equals(v.GetType().Name, candidateNavigationContract, StringComparison.Ordinal) || 6 string.Equals(v.GetType().FullName, candidateNavigationContract, StringComparison.Ordinal)); 7 }

Contract name vs view name in region

When you navigate to a new view (i.e. a view that is not already present in the region), an instance of the requested view will be retrieved from the container based on the supplied contract name (which is passed in the form of a uri). The result of this navigation request will be that the view will be added to the region. This is achieved by calling this overload of the Region.Add method (inside the RegionNavigationContentLoader.LoadContent method), which adds the view without specifying a name. Therefore if you later try to retrieve the view instance that was added to the region using a specific name through the Region.GetView method, the result will be null.

image

It is a common misconception to believe that, for a view that was navigated to, its name inside a region is equal to its contract name. These are in fact two separate concepts: the former is the name that identifies a view inside a region, where the latter is the name that identifies an object registration in the container.

1 public object LoadContent(IRegion region, NavigationContext navigationContext) 2 { 3 (...) 4 5 var view = acceptingCandidates.FirstOrDefault(); 6 7 if (view != null) 8 { 9 return view; 10 } 11 12 view = this.CreateNewRegionItem(candidateTargetContract); 13 14 region.Add(view); 15 16 return view; 17 }

Yet, in some cases, they’re both referring to a view. So why not make them the same?

Proposed modifications

Taking these problems into account, we realized that matching the view’s name inside a region with the contract name it was registered with might serve as a solution for both of them. This can be done by using a custom RegionNavigationContenLoader that replaces the default implementation provided by Prism.

As mentioned above, the LoadContent method will create a new view and add it to the region if none of the views in the region can be the target of the navigation request, hence we modified this method to add the view to the region also with a view name. This was implemented by calling this overload of the Region.Add method, which adds a view to a region with a name. We use the contract name (extracted from the Navigation Context) as the view name inside the region added.

While this is useful in case of adding only one instance of a view in a region through navigation, this might cause problems in scenarios where you need to navigate to multiple instances of a view with the same contract name (which would cause an error since there can’t be two views with the same name in a given region). So, to avoid this we decided to add an additional parameter named “matchname” to the Uri target argument, used in the RequestNavigate method, to decide whether we want to include a name for the view being added in the region. Also, we included two more parameters (“prefix” and “suffix”), which allow to add a prefix or suffix to the view name.

1 public object LoadContent(IRegion region, NavigationContext navigationContext) 2 { 3 bool matchName = navigationContext.Parameters["matchname"] == "true"; 4 5 (...) 6 7 string candidateTargetContract = this.GetContractFromNavigationContext(navigationContext); 8 string candidateTargetName = matchName ? (navigationContext.Parameters["prefix"] + candidateTargetContract + navigationContext.Parameters["suffix"]) : candidateTargetContract; 9 10 (...) 11 12 if (matchName) 13 { 14 region.Add(view, candidateTargetName); 15 } 16 else 17 { 18 region.Add(view); 19 } 20 21 return view; 22 } 23 (...)

Now we are able to retrieve views by their name using the same contract name (and a prefix/suffix, if any) we specified when navigating, by using the custom content loader we’re mentioning. This can be achieved for example with the following code:

1 (...) 2 var parameters = new UriQuery(); 3 parameters.Add("matchname", "true"); 4 parameters.Add("prefix", "MyPrefix"); 5 6 this.regionManager.Regions["MainRegion"].RequestNavigate("View1" + parameters.ToString()); 7 var myView = this.regionManager.Regions["MainRegion"].GetView("MyPrefixView1"); 8 (...)

Now taking this into account, we can additionally modify the GetCandidatesFromRegion method to also retrieve navigation candidates based on the name of a view in the region. This will make it possible to navigate back to a view in a region without the view necessarily being exported in the container with a contract name equal to its type name. The below code shows this modification:

1 protected virtual IEnumerable<object> GetCandidatesFromRegion(IRegion region, string candidateNavigationContract) 2 { 3 var candidates = new List<object>(); 4 var candidateBasedOnName = region.GetView(candidateNavigationContract); 5 6 if (candidateBasedOnName != null) 7 { 8 candidates.Add(candidateBasedOnName); 9 } 10 11 if (region == null) 12 { 13 throw new ArgumentNullException("region"); 14 } 15 16 candidates.AddRange(region.Views.Where(v => 17 string.Equals(v.GetType().Name, candidateNavigationContract, StringComparison.Ordinal) || 18 string.Equals(v.GetType().FullName, candidateNavigationContract, StringComparison.Ordinal))); 19 20 return candidates; 21 }

Looking at the code, it can be seen that we now add a candidate based on the view’s name in the region, before adding the views with a matching type Name or type FullName. To obtain the candidateBasedOnName we call the region.GetView method with the contract name (plus prefix/suffix, if any) as the parameter. This is possible due to the prior modifications, since they allow previously navigated to views to have a view name inside the region.

Now, this might sound a little confusing altogether. Let’s show it in a sample!

Sample application

We created a little sample that implements all these modifications in the ModifiedRegionNavigationContentLoader class. We exported this class in the container, which replaces the default implementation of IRegionNavigationContentLoader provided by Prism.

We then navigate to our main view, which allows you to navigate to another view on the press of a button. This navigation is done passing a Uri target that contains a prefix parameter, the “matchname” parameter set to true and the contract name of the view. Also the navigate command will retrieve the previously navigated view using the Region.GetView method, and will inform about this. Once in “View1” you will be able to navigate back using the navigation journal.

image

You can find the aforementioned sample in my SkyDrive account, under the name ModifiedRegionNavigationContentLoaderSample.

This code is provided “AS IS” with no warranties and confers no rights.

I hope you find this helpful.



One Comment

  • Debashish Gupta says:

    Hi Aadami,

    I am impressed by the way to show the Diagrams.
    It seems that i am very much like you in my mind.
    A Very good teacher is hidden within you.

    Regards
    Debashish Gupta

Leave a Reply