Composite applications typically compose their user interfaces (UIs) from various loosely coupled visual components, otherwise known as views, that are defined in the application's modules. To the user, the application presents a seamless user experience and delivers a fully integrated application. For example, the Stock Trader Reference Implementation (Stock Trader RI) is composed from multiple views, as illustrated in Figure 1.
To compose your user interface, you need an architecture that allows you to create a layout composed of loosely coupled visual elements at run time and that provides strategies for these visual elements to communicate in a loosely coupled fashion. Specifically, you need to define strategies for the following:
- View composition
The next sections describe each of these in greater detail.
In composite applications, views from multiple modules have to be displayed at run time in specific locations within the application's UI. To achieve this, the developer needs to define the locations where the views will appear and how the views will be created and displayed in those locations.
The developer determines where views will appear by defining a layout with named locations within which the views will be displayed at run time. Views can be created and displayed in these locations either programmatically or automatically. The former is achieved through "view injection," and the latter is achieved through "view discovery." These two techniques determine how individual views are mapped to named locations within the application's UI.
Views are usually implemented using a separated presentation pattern, such as Model-View-Presenter, PresentationModel, and Model-View-ViewModel, which separates presentation and business logic from the UI. Views that are implemented using a separated presentation pattern will interact with a presenter component that implements presentation logic. During view composition, the developer can choose view-first composition or presenter-first composition.
A layout defines named locations within the user interface where views will appear at run time. A layout can evolve independently without affecting the modules that are adding views to the layout. This allows a user interface designer or developer to modify the application's UI layout without affecting the underlying functionality.
The shell of the application defines the application's layout at the highest level, for example by specifying the locations for the main content and the navigation content, as illustrated in Figure 2.
Layout within these high level views is similarly defined, allowing the overall UI to be recursively composed.
Named locations are defined by assigning a location name to a control that will host the corresponding views at run time. These controls act as "placeholder" controls and define the layout strategy that will be used to arrange the views; for example, a tab control will lay out child views in a tabbed arrangement. The module that defines the view and view being displayed does not have any specific knowledge of how it will be displayed in the named location.
With the view discovery approach, modules can register views (or presentation models) against a particular named location. When that location is displayed at run time, any views that have been registered for that location will be automatically created and displayed within it.
Modules register views with a registry. The parent view queries this registry to "discover" the views that were registered for a particular named location. After they are discovered, the parent view places those views on the screen, as appropriate, by adding them to the placeholder control.
After the application is loaded, the composite view is notified to handle the placement of new views that are added to the registry.
Figure 3 illustrates the view discovery approach.
The Composite Application Library defines a standard registry, RegionViewRegistry, to register views for these named locations.
In the Stock Trader RI, the TrendLineView is registered by the market module for the Research region so that it can be displayed when the Research region placeholder control is displayed.
In the view injection approach, views are programmatically added or removed from a named location by the modules that manage them. To enable this, the application contains a registry of named locations in the UI and a module can look up one of the locations using the registry and then programmatically inject views into it.
To make sure locations in the registry can be accessed similarly, each of the named locations adheres to a common interface used to inject the view. Figure 4 illustrates the view injection approach to layout.
The Composite Application Library defines a standard registry, RegionManager, and a standard interface, IRegion, for access these locations. For more information about when to use view discovery versus view injection, see the UI Composition technical concept.
View-First and Presenter-First Composition
Views are usually implemented using a separated presentation pattern, which separates presentation and business logic from the UI. Using a separated presentation allows presentation and business logic to be tested independently of the UI, makes it easier to maintain code, and increases re-use opportunities. For more information about separated presentation patterns, see Separated Presentation.
A view that uses a separated presentation pattern will interact with a Presenter or PresentationModel component that implements the view's presentation logic. The view and presenter are hooked up during view composition.
In view-first composition, the view is logically created first, followed by the presenter on which it depends. In presenter-first composition, the presenter is logically created first, followed by the view on which it depends. Either way, after the view and presenter are created and initialized, the view displays in the specified location.
The view discovery approach lends itself most naturally to view-first composition because the view that is registered for a named location will be automatically created, followed by its presenter. The view injection approach provides more programmatic control over view and presenter creation and can be used equally effectively for view-first or presenter-first composition.
In a composite application, separated presentation patterns are used for decoupling the view from the presentation logic. In this case, user actions within the view have to be routed to appropriate handlers outside of the view. In addition, the UI elements associated with those actions often have to be enabled or disabled based on state changes within the application.
Windows Presentation Foundation (WPF) introduces the concept of commands to support these kinds of interactions. UI elements can be bound to a command, which handles the handler execution logic. The UI element can then execute the command as the user interacts with it in the UI. The UI element will also be automatically enabled or disabled as the underlying command becomes enabled or disabled.
The default WPF RoutedUICommand mechanism requires event handlers to be defined in the receiving UI element or in parent UI elements above it in the visual tree. In a composite application, command routing cannot follow the visual tree. Additionally, there are complex scenarios in composite applications where the handling of a command is delegated to child commands.
To overcome this constraint, you can use WPF to create custom ICommands so that you can directly route the command to handling logic independently of the visual tree. Two common approaches are delegation and composition.
The command delegation approach uses a command that delegates its handling logic, either through events or delegates, where it can be handled externally by a class such as a presenter, service, or controller. This provides the benefit of making this handling logic more testable, by not having code in the view's code-behind. The command requires two handlers: one for the Execute method and one for the CanExecute method. Whenever the Execute or CanExecute methods are called on the command, the handlers are called either through the event being raised or the delegate being invoked. Figure 5 illustrates the delegation approach to commanding.
The command composition approach is a variation of command delegation. In this approach, a composite command delegates its handling logic to a set of loosely coupled child commands. This is useful where the application implements a shared command that individual subscribers may want their command execution to participate in, such as with a Save All command. The composite command needs to provide a way for the child commands to be registered. Executing the composite command executes the children commands. The composite command's CanExecute returns false, unless all the children return true.
The Composite Application Library introduces the DelegateCommand<T> and CompositeCommand classes to support the two commanding approaches described above. For more information about commands, see the Commands technical concept or Commanding QuickStart. Figure 6 illustrates the composition approach to commanding.
In a composite application, components, such as presenters, services, and controllers, residing in different modules, often need to communicate with one another based on state changes or application logic. This is a challenge because of the decoupled nature of a composite application because the publisher has no direct connection to the subscriber. Additionally, there may be threading issues because the publisher is on a different thread than the subscriber.
The Publish/Subscribe pattern addresses these challenges. There are several ways to implement the pattern. Two approaches used in the Composite Application Guidance are event services and event aggregation.
With event services, an application-specific service raises standard .NET Framework events. To add new events, the service and service interface need to be modified. This service is registered and made available to different modules in the system. The publisher and the subscriber reference the service interface and do not directly depend on one another. Using this approach, the subscriber needs to manually handle any thread marshaling concerns and handle unregistering itself from the event so that it can be garbage collected. Figure 7 illustrates the event services approach.
The event aggregation approach uses a generic event aggregator service that holds a repository of event objects. The event object itself uses delegates instead of .NET Framework events. One advantage of this is that these delegates can be created at the time of publishing and immediately released, which does not prevent the subscribers from being garbage collected. Each event object contains a collection of subscribers it will publish to. New events can be added to the system without modifying the service. The event object can also automatically handle marshaling to the correct thread.
The EventAggregator service and CompositePresentationEvent<T> class are implementations that exist in the Composite Application Library. For more information, see the Event Aggregator technical concept and Event Aggregation QuickStarts. Figure 8 illustrates the event aggregation approach.
For more information about UI composition in the Composite Application Library, see the following topics:
- UI Composition technical concept
- View Discovery Composition QuickStart
- View Injection Composition QuickStart
For background information about concepts that are important to understanding the Composite Application Guidance, see the following topics: