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

13 comments:

  1. Excellent stuff...really like your blog :)

    ReplyDelete
  2. I've been following your posts and I like that I'm able to see how I can use it for my projects.

    ReplyDelete
  3. Can you make a quick example without using tab adapter and make it more like the navigation example but just using regions like you mention in your comment?

    "The shell just hosts the navigation and the views. Normally I'd use regions for this"

    Can you do a quick one using regions instead like you recommend? I think this will help bring region and navigation together for beginner.


    - Thanks

    ReplyDelete
  4. That sounds like a great suggestion - a sort of "before/after" regions. I like it! I'll add it to my "to do" list.

    ReplyDelete
  5. Jeremy,

    Do you have an example of how transitions are supposed to work in regions assuming I am trying to reproduce the page/frame transition effect? Even a simple example, say using a contentcontrol for a region, when sending individual views to the region, by default they are only activated the 1st time, and never deactivated. The states are only called by default on activation and deactivation. So If I wanted to reproduce a typical example where 1 page/view fades in, while the current one fades out, how would I go about doing this the easy way? I thought I would be able to just publish the view, but it only activates and calls the show state the first time, and by default never seems to deactivate or go to the hide state?

    Even in the region example you have to specifically close the circle to deactivate, and I don't see any examples off hand to produce the simple page transition sample above.

    I was trying to throw a quick site together with the framework to get a feel for it but am currently searching for the easy way of accomplishing simple page transition, what am I missing?

    Thanks,

    ReplyDelete
  6. Sure thing - take a look at the latest source download. Don't use the downloads tab, because it's not released as of me posting this, but if you go to the Source tab and then click download, you'll get a SimpleNavigationWithRegion. This is the SimpleNavigation refactored for regions but also shows visual transitions.

    ReplyDelete
  7. Jeremy,

    Thanks for this it makes it really clear! However I still have the problem where when activated the view immediately pops to the end of the show state after the initial activation. The first time it activates and plays the animation in the show state fine. After the initial activation it seems to never play the show state again from beginning and just show the view. Deactivation seems to always play the animation that is part of the state. Any idea why this is happening?

    Try putting a .5 - 1 second animation for show and hide for circle and square and switch between the 2, after the 1st time the show state doesn't seem to play anymore. I'd send my repro but don't know where to send to.

    ReplyDelete
  8. I've seen this before and has something to do with the way the visual states are set up - I'll look into it and see if I can get you more detailed information.

    ReplyDelete
  9. you find anything out on this visual state issue? I was unable to make this work using the framework. thnx

    ReplyDelete
  10. Hi Jeremy

    Nice framework. One small question: In the RegionAdapterBase's AddRegion method you provide the region as an object. Why not change the method's signature to:

    public virtual void AddRegion(TRegionType region, string targetRegion)

    --> No more 'if (!(region is TRegionType))'
    --> Regions.Add(targetRegion, region);

    Cheers

    ReplyDelete
  11. Because I wanted a generic interface so I wouldn't have to close the generic when it is called by the region manager.

    ReplyDelete
  12. Jeremy,
    I'm just starting to play around with Jounce and I am looking at creating a region adapter for the Telerik RadRibbonBar.

    Basically what I'm hoping to accomplish is to create a View that gets injected into a RadRibbonBar region. I have the view defined as a RadRibbonTab and not a UserControl and this seems to be causing some problems with Jounce in the ViewModelRouter. This method in particular.

    public UserControl ViewQuery(string name)
    {
    var v = _GetViewInfo(name);
    return v == null ? null : v.Value;
    }

    It appears that it only deals with UserControls. Is my thinking off on the RadRibbon or is this a limitation of Jounce? It may also be that I need to wire my ViewModel and View up differently instead of using this method return ViewModelRoute.Create(typeof(ViewModels.RibbonBarViewModel).ToString(), typeof(RibbonBarView).ToString());.

    I'll keep playing with it and let you know if I get it figured out, but I thought I'd ask the expert to see if this is what you had intended in Jounce.

    Thanks,
    Garry

    ReplyDelete
  13. I've had some concerns raised about this in the past as well. It probably makes sense to change the UserControl restriction to be more loose and derive from Control instead - just something that hasn't been a compelling requirement in the past. You should be able to swap to Control and get the desired functionality, and we are tracking it as an item for the future release.

    ReplyDelete