UI Composition
In composite applications, views from multiple modules are displayed at run time in specific locations within the application's user interface (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 decoupling of the view and the location in the UI where it will be displayed allows the appearance and layout of the application to evolve independently of the views that appear within the region. For more information about views, see the Shell and View technical concept.
Layout and Regions
The developer defines where views will appear by defining a layout with named locations, known as regions, which act as placeholders within which one or more views will be displayed at run time. Modules can locate and add content to regions in the layout without exact knowledge of how and where the region is visually displayed. This allows the layout to change without affecting the modules that add the content to the layout.
Regions are defined by assigning a region name to a Windows Presentation Foundation (WPF) or Silverlight control. At run time, views are added to the named Region control, which then displays the view or views according to the layout strategy that they implement; for example, a tab control region will lay out its child views in a tabbed arrangement. Regions can be accessed by their region name and support the addition or removal of views. Views can be created and displayed in regions either programmatically or automatically. In the Composite Application Library, the former is achieved through "view injection" and the latter through "view discovery." These two techniques determine how individual views are mapped to the named regions within the application's UI.
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 1. Layout within these high level views is similarly defined, allowing the overall UI to be recursively composed.
Figure 1
A template shell
Frequently, regions are used to define locations for views that are logically related. In this case, the region control (typically, this is an ItemsControl-derived control) will display the views according to the layout strategy that it implements, such as in a stacked or tabbed layout arrangement.
Regions can also be used to define a location for a single view, for example, by using a ContentControl. In this case, the region control displays only one view at a time, even if more than one view is mapped to that region location.
The Stock Trader Reference Implementation (Stock Trader RI) shows the use of both the single-view and the multiple-view layout approaches. Both can be seen in the shell for the application, which defines locations for the application's high-level views. Figure 2 illustrates the regions defined by the Stock Trader RI shell.
Figure 2
Stock Trader RI shell regions
The regions for the shell are defined in the Shell.xaml file and use the RegionName-attached property from the Composite Application Library. The following code example from the Shell.xaml file shows how the RegionManager.RegionName attached property is used to define the regions for the Stock Trader RI.
<ItemsControl x:Name="MainToolbar"
cal:RegionManager.RegionName="MainToolBarRegion" …>
...
</ItemsControl>
...
<Controls:AnimatedTabControl
...
cal:RegionManager.RegionName="MainRegion" ... />
...
<Controls:ResearchControl cal:RegionManager.RegionName="ResearchRegion" />
...
<ContentControl cal:RegionManager.RegionName="ActionRegion" />
<ItemsControl x:Name="MainToolbar"
Regions:RegionManager.RegionName="MainToolBarRegion" ...>
...
<Controls:AnimatedTabControl
Regions:RegionManager.RegionName="MainRegion" .../>
...
<Controls:ResearchControl Regions:RegionManager.RegionName="ResearchRegion">
...
<ContentControl Regions:RegionManager.RegionName="ActionRegion">
A multiple-view layout is also demonstrated in the Stock Trader RI when buying or selling a stock. The Buy/Sell area is a list-style region that shows multiple buy/sell views as part of its list, as shown in Figure 3.
Figure 3
ItemsControl region
Displaying Views
Regions allow a developer to specify where views will be displayed in the application's UI. The next step is to create and display the views by mapping them to their corresponding regions. The UI Composition design concept introduces two approaches to displaying views in a region, view discovery and view injection. The library implements both approaches so that you can choose the most appropriate approach for your application.
View Discovery and View Injection
Views can be created and displayed in these locations either automatically, through "view discovery," or programmatically, through "view injection." These two techniques determine how individual views are mapped to named locations within the application's UI.
In view discovery, you set up a relationship in the RegionViewRegistry between a region's name and the type of a view. When a region is created, the region looks for all the ViewTypes associated with the region and automatically instantiates and loads the corresponding views. Therefore, with view discovery, you do not have explicit control over when the regions' corresponding views are loaded and displayed.
In view injection, your code obtains a reference to a region and programmatically adds a view into it. Typically, this is done when a module initializes or as a result of a user action. Your code will query a RegionManager for a specific region by name and then inject views into it. With view injection, you have more control over when views are loaded and displayed; you also have the ability to remove views from the region. With view injection, it is not possible to try to add a view to a region that has not yet been created.
When to Use View Discovery vs. View Injection
View discovery is a simpler approach to composing views and getting them displayed in a region. In general, you should use view discovery, but you can use view injection if you need one of the following:
- Explicit or programmatic control over when a view is created and displayed, or when you need to remove a view from a region, for example, as a result of application logic.
- To display multiple instances of the same views into a region, where each view instance is bound to different data.
- To control which instance of a region a view is added (for example, if you want to add customer detail view to a specific customer detail region). Note that this scenario requires scoped regions described later in this topic.
Working with Regions
Regions are enabled in the Composite Application Library through a region manager, regions, and region adapters. This next section describes how they work together.
Region Manager
The RegionManager is responsible for maintaining a collection of regions and creating new regions for controls. The RegionManager finds an adapter mapped to a WPF or Silverlight control and associates a new region to that control. Figure 4 illustrates the relationship between the region, control, and adapter set up by the RegionManager.
Figure 4
Region, control, adapter relationship
The RegionManager also supplies the attached property that can be used for simple region creation from XAML. To use the attached property, you must load the Composite Application Library namespace into the XAML and then use the RegionName attached property. The following example shows using the attached property for a window with a tab control.
<Window x:Class="MyApp.Shell"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="https://www.codeplex.com/CompositeWPF">
...
<TabControl cal:RegionManager.RegionName="MainRegion" />
...
</Window>
<UserControl x:Class="MyApp.Shell"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x=https://schemas.microsoft.com/winfx/2006/xaml
xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;assembly=Microsoft.Practices.Composite.Presentation"
xmlns:RegionAdapters="clr-
…
<TabControl cal:RegionManager.RegionName="MainRegion" />
/>
…
</UserControl>
The RegionManager can register regions directly without using XAML. You can also specify explicitly the RegionManager instance into which you want to register the region. This is useful if you want to move the control around in the visual tree and do not want the region to be cleared when the attached property value is removed. The following code shows registering a new region with the RegionManager located in a container.
IRegionManager regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
RegionManager.SetRegionManager(someControl, regionManager);
RegionManager.SetRegionName(someControl, regionName);
IRegion
A region is a class that implements the IRegion interface. The region is the container that holds content to be displayed by the control. The following code shows the IRegion interface.
public interface IRegion : INotifyPropertyChanged
{
IViewsCollection Views { get; }
IViewsCollection ActiveViews { get; }
IRegionManager Add(object view);
IRegionManager Add(object view, string viewName);
IRegionManager Add(object view, string viewName, bool
createRegionManagerScope);
void Remove(object view);
void Activate(object view);
void Deactivate(object view);
object GetView(string viewName);
IRegionManager RegionManager { get; set; }
IRegionBehaviorCollection Behaviors { get; }
}
Showing a View in a Region Using View Discovery
To show a view in a region, register the view with the region manager, as shown in the following code example. You can directly register a view type with the region, in which case the view will be constructed by the dependency injection container and added to the region when the control hosting the region is loaded.
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", typeof(MyView));
Optionally, you can provide a delegate that returns the view to be shown. The region manager will display the view when the region is created.
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", () => this.container.Resolve<ordersPresentationModel>().View);
Showing a View in a Region Using View Injection
To add a view to a region using view injection, get the region from the region manager, and call the Add method, as shown in the following code. With view injection, the view is displayed only after the view is added to a region, which can happen when the module is loaded or based on a user action.
// View injection
IRegion region = regionManager.Regions["MainRegion"];
var ordersPresentationModel = container.Resolve<IOrdersPresentationModel>();
var ordersView = ordersPresentationModel.View;
region.Add(ordersView, "OrdersView");
region.Activate(ordersView);
Region Context
The Composite Application Library provides multiple approaches to communicating between views, depending on your scenario. The region manager provides RegionContext as one of these approaches. For information about other approaches and guidance on communicating between views, see the Communication technical concept.
RegionContext is useful when you want to share context between a parent view and child views that are hosted in a region. RegionContext is an attached property. You set the value of the context on the region control so that it can be made available to all child views that are displayed in that region control. The region context can be any simple or complex object and can be a data bound value. The RegionContext can be used with either view discovery or view injection.
Note
The DataContext property in Silverlight and WPF is used to set the local data context for the view and allows the view to use data binding to communicate with a local presenter or model. RegionContext is used to share context between multiple views and is not local to a single view. It provides a simple mechanism for sharing context between multiple views.
The following code, located in the EmployeesDetailsView.xaml file, shows how the RegionContext attached property is used in XAML.
<TabControl AutomationProperties.AutomationId="DetailsTabControl"
cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}"
cal:RegionManager.RegionContext="{Binding Path=SelectedEmployee.EmployeeId}"
...>
You can also set the RegionContext in code, as shown in the following example.
RegionManager.Regions[“Region1”].Context = employeeId;
To retrieve the RegionContext in a view, the GetObservableContext static method of the RegionContext class is used; it passes the view as a parameter and accesses its Value property, as shown in the following code.
private void GetRegionContext()
{
this.Model.EmployeeId = (int)RegionContext.GetObservableContext(this).Value;
}
The value of the RegionContext can be changed from within a view by simply assigning a new value to its Value property. Views can opt to be notified of changes to the RegionContext by subscribing to the PropertyChanged event on the ObservableObject that is returned by the GetObservableContext method. This allows multiple views to be kept in synchronization when their RegionContext is changed. The following code example demonstrates subscribing to the PropertyChanged event.
ObservableObject<object> viewRegionContext =
RegionContext.GetObservableContext(this);
viewRegionContext.PropertyChanged += this.ViewRegionContext_OnPropertyChangedEvent;
...
private void ViewRegionContext_OnPropertyChangedEvent(object sender,
PropertyChangedEventArgs args)
{
if (args.PropertyName == "Value")
{
var context = (ObservableObject<object>) sender;
int newValue = (int)context.Value;
}
}
Note
The RegionContext is set as an attached property on the content object hosted in the region. This means that the content object has to derive from DependencyObject. In the preceding example, the view is a visual control, which ultimately derives from DependencyObject. If you choose to use WPF or Silverlight data templates to define your view, the content object will represent the PresentationModel. If your presentation model needs to retrieve the RegionContext, it will need to derive from the DependencyObject base class.
Scoped Regions
Scoped regions are available only with view injection and should be used if you need a view to have its own instance of a region. Views defining regions with attached properties automatically inherit their parent's RegionManager. Usually, this is the global RegionManager that is registered in the shell window. If the application creates more than one instance of that view, each instance would attempt to register its region with the parent RegionManager. RegionManager allows only uniquely named regions, so the second registration produces an error. Instead, use scoped regions so that each view gets its own RegionManager and its regions will be registered with that RegionManager instead of with the parent RegionManager, as shown in Figure 5.
Figure 5
Parent and scoped RegionManagers
To create a local RegionManager for a view, specify that a new RegionManager should be created when adding your view to a region, as illustrated in the following code example.
IRegion detailsRegion = this.regionManager.Regions["DetailsRegion"];
View view = new View();
bool createRegionManagerScope = true;
IRegionManager detailsRegionManager = detailsRegion.Add(view, null,
createRegionManagerScope);
The Add method will return the new RegionManager that the view can retain for further access to the local scope.
Extending Regions
This section describes how to create your own adapters and behaviors.
Extending Region Adapters
Composite Application Library provides the following region adapters: ContentControlRegionAdapter, SelectorRegionAdapter, and ItemsControlRegionAdapter. These adapters are meant to adapt controls derived from ContentControl, Selector, and ItemsControl, respectively. There is an additional adapter, TabControlRegionAdapter, used in Silverlight because the Tab control does not derive from Selector as in WPF. Adapters can be replaced or new ones added for new controls, by adding to the RegionAdapterMappings for the RegionManager.
In the UnityBootstrapper, the RegionAdapterMappings is supplied to the RegionManager during application initialization, as shown in the following code.
protected virtual RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings regionAdapterMappings =
Container.TryResolve<RegionAdapterMappings>();
if (regionAdapterMappings != null)
{
regionAdapterMappings.RegisterMapping(typeof(Selector),
new SelectorRegionAdapter());
regionAdapterMappings.RegisterMapping(typeof(ItemsControl),
new ItemsControlRegionAdapter());
regionAdapterMappings.RegisterMapping(typeof(ContentControl),
new ContentControlRegionAdapter());
}
return regionAdapterMappings;
}
Region Behaviors
The Composite Application Library introduces the concept of region behaviors. These are pluggable components that give a region most of its functionality. Region behaviors were introduced to support view discovery, region context (described later in this topic), and create an API consistent across both WPF and Silverlight. Additionally, behaviors provide an effective way to extend region's implementation.
A region behavior is a class that attaches itself to a region to give the region some kind of functionality. This behavior is attached to the region and remains alive for as long as the region lives. For example, when an AutoPopulateBehavior is attached to a region, it automatically instantiates and adds any ViewTypes that are registered against regions with that name. For as long as the region is alive, it keeps monitoring the RegionViewRegistry for new registrations.
It is easy to add custom region behaviors or replace existing behaviors, either on a system wide or a per region basis.
Default Region Behaviors
This section describes the default behaviors added to all regions. One behavior, the SelectorItemsSourceSyncBehavior, is attached to only certain controls, as described later.
RegionManagerRegistrationBehavior
The RegionManagerRegistrationBehavior is responsible for making sure the region is registered to the correct RegionManager. When a view or control is added to the visual tree as a child of another control or region, any region defined in the control should be registered in the RegionManager of the parent control. When the control is removed again, the registered region should be also unregistered.
AutoPopulateBehavior
There are two classes responsible for implementation of the View Discovery pattern. One of them is the AutoPopulateBehavior. When it is attached to a region, it retrieves all view types that are registered against the name of the region. It then creates instances of those views and adds them to the region. After the region is created, the AutoPopulateBehavior monitors the RegionViewRegistry for any newly registered view types for that region name.
If you want to have more control over the view discovery process, consider creating your own implementation of the RegionContentRegistry and the AutoPopulateBehavior.
Region Context Behaviors
The region context functionality is contained within two behaviors: the SyncRegionContextWithHostBehavior and ForwardRegionContextToDependencyObjectBehavior behaviors. These are responsible for monitoring changes to the context made on the region and synchronizing it with a context dependency property attached to the view.
RegionActiveAwareBehavior
The RegionActiveAwareBehavior is responsible for notifying a view if it is active or inactive. The view must implement IActiveAware to receive these change notifications. This active aware notification is one-way to the view; the view cannot affect its active state by changing the active property on the IActiveAware interface.
SelectorItemsSourceSyncBehavior
The SelectorItemsSourceSyncBehavior is used only for controls that derive from Selector, such as a tab control in WPF. It is responsible for synchronizing the views in the region with the items of the selector and synchronizing the active views in the region with the selected items of the selector.
Extending Region Behaviors
You can easily create custom region behaviors or replace existing region behaviors and register them from your application's bootstrapper.
Creating a Region Behavior
You can create a custom region behavior by creating a class that derives from IRegionBehavior or from the abstract base class RegionBehavior. If you want to interact with the control that hosts the region, you should also implement IHostControlAwareBehavior.
Adding Region Behaviors for All Regions
After you create a behavior, or extend an existing one, you can register it so it will be added to all new regions. You can do this by overriding the ConfigureDefaultRegionBehaviors in the bootstrapper. The following code example shows how to add a custom behavior for all regions.
protected override IRegionBehaviorFactory ConfigureDefaultRegionBehaviors()
{
IRegionBehaviorFactory factory = base.ConfigureDefaultRegionBehaviors();
factory.AddIfMissing("MyBehavior", typeof(MyCustomBehavior));
}
Adding Region Behaviors for a Single Region
The following code example shows how to add a region behavior to a single region.
IRegion region = regionManager.Region[“Region1”];
region.Behaviors.Add(“MyBehavior”, new MyRegion());
Replacing an Existing Region Behavior
If you want to replace a default behavior with a different behavior, you can add it by overriding the ConfigureDefaultRegionBehaviors() method in your application-specific bootstrapper and registering your behavior with the same key value as the default behavior. The Composite Application Library adds a default region behavior only if a behavior with that key has not already been added.
Occasionally, you may want to add or a replace a region behavior to regions on a particular view. If those regions are defined in XAML, as most regions are, the region may not be initially available for attaching your custom behavior. You will need to monitor the availability of the region and attach your behavior when the region becomes available. The following code example shows how to replace the AutoPopulateBehavior with your custom version when the region becomes available.
public class MyView : UserControl
{
public MyView()
{
InitializeComponent();
ObservableObject<IRegion> observableRegion = RegionManager.GetObservableRegion(this.MyRegionHostControl);
observableRegion.PropertyChanged += (sender, args) =>
{
IRegion region = ((ObservableObject<IRegion>)sender).Value;
region.Behaviors.Add(AutoPopulateBehavior.BehaviorKey,
new CustomAutoPopulateBehavior());
};
}
}
Removing a Region Behavior
Although there is no way to remove an existing behavior after it is added, you can prevent a behavior from being added by overriding the ConfigureDefaultRegionBehaviors method in your application-specific bootstrapper.
Extending View Discovery
You may want to control how views are registered or created when using view discovery. The following are approaches to extending view discovery:
- Custom RegionViewRegistry. If you want to have extra control over registration of types, for example scoping the registry, or control over the creation of your types, you should derive from this class.
- Custom AutoPopulateBehavior. If you want to change where the region discovers its registered views (if you do not want to use the RegionViewRegistry) or if you want to change which views are actually added to the region (for example, if you want to provide the ability to filter), you can create a custom AutoPopulateBehavior for a single region or change the default for all regions.
More Information
For more information about UI composition in the Composite Application Library, see the following topics:
- UI Composition design concept
- View Discovery Composition QuickStart
- View Injection Composition QuickStart
- Region How-to topics:
- Composition patterns:
- Patterns in the Composite Application Library (see the section titled "Composite and Composite View")
- Separated Presentation patterns
For more information about other Composite Application Guidance technical concepts, see the following topics: