Activate existing view with Event driven design

Topics: Prism v4 - WPF 4
Oct 12, 2012 at 5:11 PM

Hi,

Goal

If a view has already been injected (eg Customer = 15) then make that view visible instead of injecting a new view.

Concern

We're using event driven design with view injection only.  No INavigation patterns are being used.

Example

The Shell has a tab control region for main workspaces.

There is a search view injected into the shell.

User types in a keyword, presses search, this raises a Search event.

The search controller subscribes to that event, injects a search results workspace which shows as a new tab in the shell.

Other modules subscribe to events from the search process and if they find a match they inject their search results into the search workspace region (using scoped regions).

Example: customer module matched the customer name on a keyword search and injects a results view into the search workspace (tab in the shell's main workspace region).

The customer search results view has a button that when clicked raises the event CustomerSearchResultEditButtonClicked(payload = customerID 15).

The customer controller subscribes to that event and injects a new customer edit view into the shell's main workspace, which shows up as a new tab.

Up to this point everything is working as expected.

If the user goes back to the search results tab, click's the same customer search result button again I want to make the existing customer view active (make the tab active) instead of injecting a duplicate of the customer edit view.

I've looked at some navigation methods but being fully event driven the event raised has no knowledge of what region the target view will end up in.  That is the responsibility of the controller.  The event only cares about what happened (user clicked a button), not what's going to happen.  The navigation methods seem to need to have some knowledge about what's going to happen and where.

At this point I'm experimenting with IActiveAware and managing a list of injected views in the controller (iterate the list, find an existing view that matches the customer ID and activate that view somehow, if it doesn't exist then inject a new view).

Once I get something working my next challenge is to get it working for deeply nested views.

For example:

  • Customer Workspace contains an edit view and a CustomerInfo region as a tab control
  • Many modules inject their own view into the CustomerInfo region as new tabs (eg. Orders, History, Contact info etc)
  • In each of those injected views they may contain a list of records.
  • Clicking on a single record may open up a secondary item with nested views and other lists.  eg. Click on an Order, that injects Order Details into the Orders view, expanding the Order Details shows a list of order detail items.  Clicking a single order detail item would inject the order detail edit view.  Think of it like being able to drill down many levels into more and more detail.

Then, perhaps from the search results view I want to link to a single order details record.

I'd like to see the customer workspace injected, the orders list injected and populated, the parent order click simulated, the order details view and list injected and the correct order detail record clicked to bring up its edit view.

Any thoughts or suggestions?

Developer
Oct 12, 2012 at 8:20 PM

Hi,

Based on my understanding of your scenario and the architecture you were using in your previous uploaded samples, where the controllers injected the corresponding views by raising events, I believe leaving the responsibility to activate the already existent views or injecting a new one if not present to the corresponding controllers seems like a reasonable approach.

In my opinion, for the views in the inner regions you could follow a similar approach like the one you mentioned for the customer views, from example when your customer controller injects or activate an specific customer view, you could raise another custom event for each type of the inner views required based on the search results. This way you could have a custom controller for each of those types that could handle these events. And based on the payload passed (corresponding scoped region manager and for example an orderID in which case this would be used by the orders controller), similarly like above, in the handler of the event you could iterate the existing views  and decide to activate the corresponding view or inject a new one. Following this you should be able to use the sane approach for the different levels of your composite views.

Take into account that how to implement this will depend mostly on your personal preferences and the requirements of your scenario, In my opinion to achieve this kind of scenarios you will need to design a highly structured architecture to support the deeper regions, which could be based on the one you used in your samples.

Best regards,

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

Oct 17, 2012 at 9:22 PM

Thanks for that info.

I've been experimenting with some things and want to clarify.

It appears I will have to handle and implement code to activate all views from the nested view up through all parent views ultimately arriving at the top level view to make the nested view visible.

And, that there is nothing "built-in" to Prism that will do that for me?

For example:

  • In my shell I have top level views as tabs, I can activate top level views easily to force them to become the visible tab (active) if they are not currently visible
  • If I have a nested view inside one of these top level tabs but the top level tab is not currently visible (active), I have to activate every view from the nested view upwards ending with the top level tab view.  If I don't, calling region.Activate(view) will succeed, but will not do anything to cascade activating parent views to the point they are actually visible.

Assuming I'm always starting from the nested view (it starts by receiving an event) and need to activate up the parent tree, I'll need a re-usable pattern so it will work in any injected/nested view combination.  It simply needs to continually activate parent views until there are no more to activate.  That should ensure the nested view will be visible to the user no matter where the request to view it came from.

Does that summarize it the way you are thinking it needs to work given my situation?

Developer
Oct 18, 2012 at 9:49 PM

Hi,

As far as I know, Prism does not provide functionality to activate the parent views when an inner view is activated. In fact, with Regions, Prism aims to compose the user interface without needing the views to know each other. Hence, Prism does not even provide any functionality to obtain a view's parent.

Based on this, climbing up the visual tree and activating corresponding views would require to implement your own custom functionality not only to activate the views; but to find them, their Regions and their RegionsManagers too. Therefore, I believe it would be simpler to activate the top view first and then start activating the inner views up to the required one. In order to do this, you would need to have the information to know which views you need to activate. Then, for example, you could activate the top view and check the views of its Regions to check if it contains a view that needs to be activated. This inner view that was activated could repeat the process with its inner views and so on.

On the other hand, if you want to have a re-usable functionality that could be used to activate directly an inner view and their parents, regardless of the cost of implementing this functionality, I believe you could use an approach like the following one:

First you will need some infrastructure in your views / view models to support this kind of scenario. For this example, we will use the ViewModelBase class that I saw in one of your previous samples which defines a BaseRegionManager property where the scoped RegionManager is stored. Also, we will define an interface (lets call it IActivateVisualTree) that defines an ActivateVisualTree method. This interface could be implemented in your views.

Second, you will need a service in charge of activating the view and its corresponding tree. This service could be either a simple static class or a shared service registered in the container. How to implement it would depend mostly of your personal preferences. This service could for example expose a single static method that could then be invoked when needed to do the job. In this example, it will be invoked inside the implementation of the ActivateVisualTree method that we defined before for each view:

// Some view 's code...
ActivateVisualTree()
{
    // The service...
    VisualTreeActivator.Activate(this);
}
Hence, when you need to activate an specific view, you could cast the view to an IActivateVisualTree object and invoke the ActivateVisualTree method. By doing this, neither the code that requests the view to be activated or the view itself need to know how to activate the visual tree; that will be the responsibility of the service.

The complex part here is the implementation of the aforementioned service. Based on my understanding the service will need to do something similar to the following steps to activate the visual tree involved:

  1. Taking the view passed as a parameter, start climbing up the visual tree until you find a element with a DataContext of type ViewModelBase. Then, obtain the RegionManager contained in the BaseRegionManager property. This should be the RegionManager that contains the Region where the view is injected. (I believe you could find the code in the RegionManagerRegistrationBehavior class provided by Prism useful to implement your own logic to climb up the visual tree.)
  2. Again, taking the view as a base, start climbing up the visual tree until you find an element that has a RegionManager.RegionNameProperty attached to it. You can use the GetRegionName static method of the RegionManager class to check for the aforementioned property. Once found, save the Region's name. It should be the name of the Region where the view is injected.
  3. Obtain the Region from the RegionManager found in step 1. To do so, use the Region's name obtained in step 2. With this you should have the Region where the view is injected.
  4. Activate the view using the Activate method of the Region obtained in step 3.
  5. Finally, in order to activate the parent views, climb up the visual tree again until you find an element that implements our IActivateVisualTree interface. Once found, stop climbing and invoke the ActivateVisualTree method of this element.

So, when you need to activate an specific view and their parents, you would only need to invoke its ActivateViewTree method. This method consumes the service that will activate the view and invoke the ActivateVisualTree method on the parent view, activating all the views "recursively" until no parent that implements the IActivateVisualTree interface is found.

Take into account that we haven't tested this approach, and that it's only one possible approach to implement the functionality you are mentioning. Again, how to implement this will depend mostly of your personal preferences and the requirements of your scenario.

Finally, I would like to mention that this approach might not be useful if you have DataTemplates in your visual tree, as a DataTemplate is not a FrameworkElement and it has no Parent property you can use to climb up.

I hope this helps,

Damian Cherubini
http://blogs.southworks.net/dcherubini