Wednesday, October 27, 2010

Jounce Part 4: Region Management

Now that we've explored the concept of Jounce navigation, the next logical step is to examine region management. This concept was introduced by the Prism framework and is very powerful and functional for both WPF and Silverlight applications.

Region management deals with a few moving pieces. A region is simply an area of the display that has been marked or tagged to hold a control. Regions are as flexible as their containers: they can be a fixed size or dynamic. They can hold a single control or a collection of controls. Typical containers include ContentControl for single controls, ItemsControl for multiple controls, and any type of panel including Grid and TabControl.

Regions are tagged with a region name. In Jounce, tagging a region is as simple as using the ExportAsRegion attached property, as you can see here:

<Controls:TabControl Grid.Row="1" Regions:ExportAsRegion.RegionName="TabRegion"/>
<ItemsControl Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
                Regions:ExportAsRegion.RegionName="AppRegion">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

Once a region is tagged, a view or control can be pointed at the region. In Jounce, you would use the ExportViewToRegion tag and pass the view's tag and the target region. In this example, the view is exported as we discussed in the previous post. An additional tag is added to route the view to the target region:

[ExportAsView(REQUEST_SQUARE, MenuName = "Request a Square")]
[ExportViewToRegion(REQUEST_SQUARE,LocalRegions.TAB_REGION)]
public partial class RequestSquare
{
}

So how does the view end up in the region? This is where the region adapter comes into play. The region adapter knows how to manage views in a specific region. For example, in Jounce, the ContentRegion adapter manages content controls, while the ItemsRegion adapter manages items controls. Because you can wrap these in a panel type, or override the ItemsControl to use any panel from Canvas to Grid or StackPanel as the container, these are the only adapters that ship with Jounce. We'll talk about creating your own adapters in a little while.

Here is the example from the Jounce quick start, showing the two regions and some controls:

Jounce region management

First, let's see how the view ends up in the region. The region is tagged and the RegionManager is notified. This manager knows about all regions and all region adapters. The region manager imports all of the view metadata for region routes:

[ImportMany(AllowRecomposition = true)]
public Lazy<UserControl, IExportViewToRegionMetadata>[] Views { get; set; }

It also exposes an indexer making it easy to find the region metadata given a view tag:

public IExportViewToRegionMetadata this[string viewType]
{
    get
    {
        return (from v in Views
                where v.Metadata.ViewTypeForRegion.Equals(viewType, StringComparison.InvariantCultureIgnoreCase)
                select v.Metadata).FirstOrDefault();
    }
}

When we navigate to the view, you'll remember the core Jounce framework wires the view to the view model, then raises the ViewNavigatedArgs message. This is what region management listens for:

[Export(typeof (IRegionManager))]
public class RegionManager : IRegionManager, IPartImportsSatisfiedNotification, IEventSink<ViewNavigatedArgs>
{
   ...
   public void HandleEvent(ViewNavigatedArgs navigationEvent)
    {
        if (navigationEvent.Deactivate)
        {
            DeactivateView(navigationEvent.ViewType);
        }
        else
        {
            ActivateView(navigationEvent.ViewType);
        }
    }
}

The activate and deactivate are similar. If this is the first time the region manager has seen the view, it stores the tag for the view, then finds the region adapter that manages the type of the view. It then informs the region adapter of the view so that it can be added to the region adapter's internal collection. Finally, regardless of whether the view has been "seen" before or not, all adapters that manage the view are called upon to activate the control.

var viewInfo = GetViewInfo(viewName);

if (viewInfo == null)
{
    return;
}

if (!_processedViews.Contains(viewName) && _regions.ContainsKey(viewInfo.Metadata.TargetRegion))
{
    // add any views that were waiting for this region to become available 
    var region = _regions[viewInfo.Metadata.TargetRegion];
    var regionAdapterInfo = GetRegionAdapterForType(region.GetType());
    if (regionAdapterInfo != null)
    {
        var regionAdapter = regionAdapterInfo.Value;
        regionAdapter.AddView(viewInfo.Value, viewName, viewInfo.Metadata.TargetRegion);
        _processedViews.Add(viewName);
    }
                
}
            
foreach(var ra in GetRegionAdaptersForView(viewName))
{
    ra.ActivateControl(viewName, viewInfo.Metadata.TargetRegion);                
}             

For the built in content control, activation is simple: the content is updated ot the new control, overwriting the old one (it still remains in memory, but is not longer part of the content control as a view).

public override void ActivateControl(string viewName, string targetRegion)
{
    _ValidateControlName(viewName);
    _ValidateRegionName(targetRegion);

    var region = Regions[targetRegion];            
    region.Content = Controls[viewName];           
}      

First, it validates it has the view. Next, it validates it has the region. Finally, it grabs the region (which in this case is a content control) and sets the content to the control.

So how does the region manage the view? The RegionAdapterBase is typed to the region (i.e. ContentControl) and keeps track of regions (a dictionary with region names and the actual content controls) and views (a dictionary with the view tag and the actual view control). It has default behaviors for activating and deactivating a control, and then allows the adapter to override this. The adapter simply exports itself with the type of region it can handle:

[RegionAdapterFor(typeof(ContentControl))]
public class ContentRegion : RegionAdapterBase<ContentControl>
{
}

Note that it inherits from the base control, typed to the type it is handling, and also exports the type so the region manager can find it.

Because tab controls have headers, Jounce doesn't natively provide an adapter because you might want the header to behave differently than Jounce would expect. In the quickstart, an example tab adapter is provided. Here is the code:

[RegionAdapterFor(typeof(TabControl))]
public class TabRegion : RegionAdapterBase<TabControl>
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    private readonly List<string> _addedViews = new List<string>();

    [ImportMany(AllowRecomposition=true)]
    public Lazy<UserControl, IExportAsViewMetadata>[] Views { get; set; }               
        
    public override void ActivateControl(string viewName, string targetRegion)
    {
        _ValidateControlName(viewName);
        _ValidateRegionName(targetRegion);

        var region = Regions[targetRegion];

        if (!_addedViews.Contains(viewName))
        {
            _addedViews.Add(viewName);
            _SetupTabForView(region, viewName);
        }
                                   
        region.SelectedIndex = _addedViews.IndexOf(viewName);
    }

    private void _SetupTabForView(ItemsControl region, string viewName)
    {
        var metadata =
            (from v in Views where v.Metadata.ExportedViewType.Equals(viewName) select v.Metadata).FirstOrDefault();

        var header = metadata == null ? viewName : metadata.MenuName;
            
        var tabControlItem = new TabItem {Header = header, Content = Controls[viewName]};            

        region.Items.Add(tabControlItem);           
    }
}

In this case, the tab adapter keeps track of views added as well. It imports the metadata for the views because we'll use the extra fields such as "menu name" to build out our tabs. When the view is activated for the first time, the meta data is inspected and a TabItem is created with the menu name as the header and the control as the body. It is added to the tab control. Anytime the navigation event for the view is fired, the tab adapter sets the index to the correct tab so it is selected. This allows other triggers to navigate to the correct tab.

As you can see, region management greatly simplifies the problem of navigation by allowing easy, composite sections that can be nested as deep as necessary. Classes don't have to understand the layout or regions at all: there is no need to understand how to select a tab, because the region manager handles this. All that happens is the navigation event is raised, then the region manager steps in and takes care of the rest.

Here's a quick tip as well: you might want to have some fancy transitions that appear when you move from one page to the next. The content region prevents this because the old control is immediately overwritten. How would you handle this in Jounce?

It's easy. Use an ItemsControl instead and for each control, provide a visual state for showing the control and hiding it. That visual state can have any type of animation you like. Override the template panel for the control to be a Grid so all of the views are stacked on top of each other.

In the _Activate method of the view model, simply go to the visual state for the "show" state. In the _Deactivate, go to the visual state for the "hide" state. You can set up a class that listens for navigation events and keeps track of the last item. Whenever a new item is navigated, it will publish the "deactivate" event for the old item. This allows the old item to transition away while the new item transitions in. While the views are stacked in a region that handles multiple views, only the view with the "show" visual state will be visible at any given time.

Next post I'll share how you can integrate Jounce navigation with the built-in Silverlight navigation framework.

Jeremy Likness