Saturday, October 23, 2010

Jounce Part 3: Basic Navigation

Navigation is always an interesting topic and something I often see people struggle with in Silverlight applications. I am not a fan of navigation methods that force you to change the way your views behave, which is why I am not a fan of the navigation framework that is built into Silverlight (you suddenly have to use pages instead of user controls and understand frames and interact with special classes). Jounce takes a less intrusive approach that decouples how navigation works from how navigation is requested.

In the core Jounce framework you'll notice there are two classes, a ViewNavigationArgs and a ViewNavigatedArgs that derives from the first class. If you take a look at the code:

public class ViewNavigationArgs : EventArgs
{
    public ViewNavigationArgs(Type viewType)
    {
        ViewType = viewType.FullName;
    }

    public ViewNavigationArgs(string viewType)
    {
        ViewType = viewType;
    }

    public bool Deactivate { get; set; }

    public string ViewType { get; private set; }

    public override string ToString()
    {
        return string.Format(Resources.ViewNavigationArgs_ToString_ViewNavigation,
                                Deactivate
                                    ? Resources.ViewNavigationArgs_ToString_Deactivate
                                    : Resources.ViewNavigationArgs_ToString_Activate, ViewType);
    }
}

You can see there is not much to the class except a "view type" which is simply what we tagged our view with (the string that we discussed in the last post) along with a flag to indicate whether we are deactivating the view. To start the navigation cycle for Jounce, all you have to do is simply publish a ViewNavigationArgs message. Pretty simple, no?

Of course, I know you might not want your navigation to work the same way I like mine, so Jounce doesn't make any assumptions about how navigation will take place, it only provides the framework for notifying you about the event. When the event is notified, Jounce will take care of instancing the view (but remember it will not place it in the visual tree) and wiring the view model to the view (this happens in the loaded event, so the wiring also won't trigger until the view is in the visual tree). You must actually place the view, or use region management and a region adapter will place the view (more on that later).

There are several reasons why I chose to include the view tag for the navigation event. First, it is consistent with the same tag you use to bind views to view models so it gives a common "name" to a view you would like to see, without having to understand how the view is implemented. Second, it abstracts the type and implementation of the view. For a view model to "navigate" it simply publishes an event with a tag, and doesn't have to reference user controls or pages or any other navigation construct - in fact, you can build a completely new view with a completely different style, tag it the same, and have the navigation work.

Another reason why this is effective is because many applications have dynamically loaded modules. When the XAP file is loaded dynamically, the calling assembly does not have access to the type in the dynamically loaded assembly (until after it is loaded). I never felt that navigation should have to be aware of whether it is triggering a module load or not, which is why I built the ViewXapRoute class.

The view XAP route simply informs Jounce that a view with a particular tag can be found in a different XAP file. The class looks like this:

public class ViewXapRoute
{
    private ViewXapRoute()
    {
    }
        
    public static ViewXapRoute Create(string viewName, string viewXap)
    {
        return new ViewXapRoute {ViewName = viewName, ViewXap = viewXap};
    }

    public string ViewName { get; private set; }

    public string ViewXap { get; private set; }

    public override string ToString()
    {
        return string.Format(Resources.ViewXapRoute_ToString_View_Route_View, ViewName, ViewXap);
    }
}

The RegionManagement solution has an example of how this dynamic navigation works. If you take a look at RequestSquare.xaml.cs, you can see that a route is exposed for the view tagged "Dynamic" indicating it can be found in the XAP file called "RegionManagementDynamic.xap". The code looks like this:

[Export]
public ViewXapRoute DynamicRoute
{
    get
    {
        return ViewXapRoute.Create("Dynamic", "RegionManagementDynamic.xap");
    }
}

With this hint, Jounce can easily navigate now even if it means loading the XAP. Now take a look at two events. Both load a view, but one loads a view that is in another XAP file. Can you tell which one?

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
    EventAggregator.Publish(new ViewNavigationArgs(Square.SQUARE));
}

private void ButtonDynamic_Click(object sender, System.Windows.RoutedEventArgs e)
{
    EventAggregator.Publish(new ViewNavigationArgs("Dynamic"));
}

The only difference is that one uses a magic string which could just as easily have been a constant instead (I wanted to demonstrate different ways). Notice that the request for the view is exactly the same in either case, which is what we want: the module requesting the view shouldn't have to know if it is dynamic or not. Somewhere else we provide the hint that the view exists in a different XAP file, and Jounce takes care of reading that hint and loading the XAP file.

We covered a little about what happens when the navigation is fired in the last blog post (the view model and view are wired and certain methods are called on the view model). Now let's take a full look at the event handler for the ViewNavigationArgs event that is in the ViewRouter class:

public void HandleEvent(ViewNavigationArgs e)
{
    if (e.Deactivate)
    {
        ViewModelRouter.DeactivateView(e.ViewType);
        EventAggregator.Publish(new ViewNavigatedArgs(e.ViewType){Deactivate = true});
        return;
    }

    var viewLocation = (from location in ViewLocations
                        where location.ViewName.Equals(e.ViewType, StringComparison.InvariantCultureIgnoreCase)
                        select location).FirstOrDefault();

    if (viewLocation != null)
    {
        DeploymentService.RequestXap(viewLocation.ViewXap,
                                        exception =>
                                            {
                                                if (exception != null)
                                                {
                                                    throw exception;
                                                }
                                                _ActivateView(e.ViewType);
                                            });
    }
    else
    {
        // just activate the view directly
        _ActivateView(e.ViewType);
    }
}

Notice that if the event is a deactivation, the view model is informed by calling the Deactivate method. This allows the view model to save state, release references, or do anything else that might need to be done when the view is being swapped out.

When the event is an activation, Jounce looks for a location hint. If it exists and the dynamic XAP file has not yet been loaded, it is loaded and the activation delayed until the dynamic XAP file is ready. Otherwise, it calls activation directly.

Also notice that at the end of Jounce's cycle of dealing with the event, it raises the ViewNavigatedArgs to indicate it has done the preliminary wiring of view models and XAP loading. This is something important to understand with Jounce navigation: the navigation is fired before Jounce does anything, and the navigated is fired afterwards, so if you are tapping into navigation and want to know that the view models have been activated, this is the event to listen for.

Again, Jounce is about being lightweight and guiding, not imposing. Because not everyone uses regions, the core Jounce framework does not include regions. Region management is packaged as a separate assembly that is optional. I'll talk about region management more in my next post, but it's important to know it is non-intrusive. If the region management assembly is included, the region manager will hook into the navigated event and use that event to move views into target regions.

Of course, we've now fired events, wired view models and even informed our view models that something has happened, but you are probably wondering, "Where is the navigation? So far I haven't seen a view actually do something."

Again, this is where Jounce gives you flexibility but I've included an example in the quick starts to help out. The SimpleNavigation project shows one way navigation can be handled.

In this sample project, several views are tagged with additional information. Jounce provides extra attributes for views that you can use to parse and categorize these. In our example, we're assuming that a view available from the top level navigation menu will have a category called "Navigation." We also want to provide a simple menu name for the view, as well as a description. One such view, a red square, is tagged like this:

[ExportAsView("RedSquare",Category="Navigation",MenuName = "Square",ToolTip = "Click to view a red square.")]
public partial class RedSquare
{
    public RedSquare()
    {
        InitializeComponent();
    }
}

Notice that these attributes are optional. The main navigation view, that should not also be an option itself, is simply tagged with a name:

[ExportAsView("Navigation")]
public partial class Navigation
{
    public Navigation()
    {
        InitializeComponent();
    }
}

The navigation view model pulls in the metadata for the views:

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

It then processes the views to create an observable collection with the tag for the view, the text that should display on the navigation button, and a tool tip for the button - note we are only looking for the navigation category:

foreach(var v in from viewInfo in Views where viewInfo.Metadata.Category.Equals("Navigation")
                    select Tuple.Create((ICommand)NavigateCommand,
                    viewInfo.Metadata.ExportedViewType, 
                    viewInfo.Metadata.MenuName, 
                    viewInfo.Metadata.ToolTip))
{
    _buttonInfo.Add(v);
}

Our navigation command does two things. It is only allowed if the command is for something other than the current view (we keep track of the current view in the CurrentView property). It also simply raises the view navigation event with the appropriate tag when fired. Also, notice the extension method that makes it easy to turn a string into a ViewNavigationArgs class. Here is the command:

NavigateCommand = new ActionCommand<string>(
view =>
    {
        CurrentView = view;
        EventAggregator.Publish(view.AsViewNavigationArgs());
    },
view => !string.IsNullOrEmpty(view) && !view.Equals(CurrentView));

And here is the XAML that binds the command:

<Button Margin="5" Command="{Binding Item1}" 
   CommandParameter="{Binding Item2}" Content="{Binding Item3}"
   ToolTipService.ToolTip="{Binding Item4}"/>

So basically the metadata for the views is used to generate a collection with information, and buttons are bound to those views that show the user-friendly name, provide a tool tip, and when fired, raise the view navigation event.

The shell for the project as a ContentControl that is bound to a property called CurrentView:

<ContentControl Grid.Row="1" Content="{Binding CurrentView}"/>

The view model listens for navigation events. When they happen, it uses the view model router to grab the view and bind it to the control. Because the view model router has to inspect all views in order to bind them to view models, it provides an indexer to make it easy to locate a view (without understanding the underlying details or implementation of the view). You simply pass the view tag and if the view has been found already, it will be returned. Take a look here - this is probably what you've been waiting for. It's an event that listens for navigation, and when a navigation event is raised, grabs the view so it appears in the content control:

public void HandleEvent(ViewNavigationArgs publishedEvent)
{
    if (!publishedEvent.ViewType.Equals("Navigation"))
    {
        CurrentView = Router[publishedEvent.ViewType];
    }
}

That's it - the button click raises the event, the view model listens for the event, cross-references the view and pushes it into a content control.

Of course, if that seemed like a lot of work, I agree, it was. This is on purpose: Jounce does not want to impose a navigation framework, so we are responsible for listening for publishing and receiving navigation events and doing something with them.

What's nice about this model is that navigation doesn't have to be a page. It can be an item, a sub-module, or even a tiny little control buried in the footer of the page. It is truly agnostic of your hierarchy, so it makes it easy to build composite applications because anywhere you need something to appear, you just raise an event with its tag.

If you want to make it even easier, forget about listening to the events and wiring in the views. We don't really want to deal with the views at all. We're developers, and we're focused on the code. So how do we take care of that, and let the views take care of themselves? We'll need to use a pattern introduced by Prism called "region management."

When I go into detail about region management in the next post, you'll start to see an emerging pattern with Jounce: tagging and binding. To get a view model bound to a view, we tag the view and view model, then give Jounce a hint to bind them. When we have a view in a different XAP file, we tag the view, then we export a binding that maps the view to the XAP. Region management is the same way. Instead of listening to navigation and manually grabbing views and setting them, region management lets us tag an area of the page and call it a region, then tag a view and let Jounce know the view belongs in that region.

We'll learn more about that next time ... but in the meantime, you can pull down the simple navigation project to explore these concepts further.

Jeremy Likness

4 comments:

  1. Very nice. Wondering about a slightly more complex but pretty realistic scenario: I want to allow multiple instances of a view, but limited to only 1 per domain instance. i.e., I want to allow a user to edit more than one InventoryItem at once, and leave them both open, but *not* allow them to open the *same* InventoryItem at the same time. Does Jounce handle this kind of thing or would that require a customization?

    ReplyDelete
  2. Jounce allows multiple instances of a view. It doesn't know what your definition of a single item is, so that is logic you would build in, but spinning up a view and adding a view model is not a problem - obviously this would be based on you generating a view rather than navigating to a view. I am considering a way to embed something in navigation parameters to control new view/viewmodel instances.

    ReplyDelete
  3. That would be great if you build something for this, it's something I almost always need in my apps. The problem is, of course, that the logic of whether or not a view needs to be spun up needs to reside in the framework, not in any view. Maybe you could add something like: change so that developers sub-class from ViewNavigationArgs and put their init parameters for each view in it - which would be how you'd distinguish one view instance from another (e.g. InventoryItemId) - then have them implement a common interface to compare 2 of them. If equal then don't spin up a new one and navigate to the existing match.

    Just an idea, you probably have something more elegant in mind. But very important to have, IMHO.

    Thanks

    ReplyDelete