View Based Navigation in Silverlight 3

by Administrator 17. September 2009 01:28

 

Silverlight 3 ships with a Navigation mechanism.  A control that extends Page can be stored inside the Frame control as content, and Navigation from Page to Page is done by specifying the URI of the target Page.  This mechanism includes navigation history and the ability to go forwards and backwards through said history.  I would, however, like a slightly different programming model around application navigation.

Design goals:

  1. I would like to be able to navigate to a new “screen” from within various places such as Presenters or Commands
  2. I do not want to have to configure each new View I add to the system
  3. I don’t want to have to map Views to URIs to use the Frame
  4. I need to be able to pass parameters to Views as they are navigated to
  5. I need to support having more than one View implementation in the system for a given View interface type
  6. I need an inspect-able navigation history that I can also configure in terms of how much history to keep and so forth
  7. I would like to support animated transitions from View to View, this is Silverlight after all

My sample application will be an “Audi Fan Site” in Silverlight.

Views & ViewModels

I have a Silverlight “guidance” Solution called HandWaver.AG; I will use some concepts from this solution to build the navigation framework and post the entire solution’s source code. 

Pattern: For separation of concerns, I’ll use the Model-View-ViewModel pattern.

I’ll start with a humble IView base interface:

namespace HandWaver.AG.PresenationModel
{
    public interface IView
    {
        string Title { get; }
        void Activate(Action complete);
        void DeActivate(Action complete);
    }
}

Nothing complicated here.  The Activate and Deactivate methods can be called to inform a IView that it is about to be “started” or shut down.  The Action callbacks will facilitate animation later.

Now I need a ViewModel base class that does a little bit of change notification work for me.

namespace HandWaver.AG.PresenationModel
{
    public abstract class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propName)
        {
            if (null != PropertyChanged)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
    }
}

I will expand on these concepts in a little bit, for now assume that I will have Controls of some kind that implement IView and bind to ViewModels.

Navigation

For the most simple case, I want to be able to just go to an IVIew.  I’ve created an INavigationService interface.  This is the calling convention that I’ll use.

namespace HandWaver.AG.PresenationModel.Navigation
{
    public interface INavigationService
    {
        /// 
        /// Display the first IView implementation we find
        /// 
        /// 
        void NavigateTo< T >() where T:IView;

At this point I can go ahead and create my sample application and my “Home” screen.  I’m going to re-use the Frame and Page concepts from Silverlight.  I have some navigation buttons that are disabled, a ComboBox that will contain my navigation history, and some HyperlinkButtons up at the top to link to the various IViews I will create.

navapp0

The grey area is my navigation Frame.  What I want to do now is Navigate to the Home screen when the app is loaded.  As you can see from the INavigationService code above, I’m just going to do this:

Navigation.NavigateToIHomeView>();

Adding this now displays my Home screen in the navigation frame…

navapp1

… but how did we get there?  The first item is the discovery of IViews.

Discovering Views

In order to meet my design goals, I need to figure out how to find IView implementations automatically without having to create some kind of View Registry.  In my implementation of the INavigationService interface, reflection is the key.  It’s worth going over this code as everything else depends on it:

private void MapViewTypes()
{
    _viewMap = new Dictionary<Type, List<Type>>();
    //Note: we're making a giant assumption here that all Views are in this assembly.  MEF could help?
    
    var types = _viewAssembly.GetTypes();
    var viewType = typeof(IView);
    var pageType = typeof(Page);

    foreach (var t in types)
    {
        if (t.ImplmentsInterface(viewType) && t.IsSubclassOf(pageType))
        {
            var viewImplType = t;
            //if we have a sub-class of IView, use that instead
            var subViewType = (from v in viewImplType.GetInterfaces() where v.InterfaceExtends(viewType) select v).FirstOrDefault();
            if (null != subViewType)
            {
                EnsureList(subViewType);
                _viewMap[subViewType].Add(t);                        
            }
            else
            {
                continue;
            }

        }
    }
}

ImplementsInterface and InterfaceExtends are extension methods I wrote for System.Type.  When this class is created, I’m reflecting the implementing assembly and finding all classes that implement a descended of IView and storing them in a List.  The implementation for the most basic NavigateTo(), then, just looks like this:

public void NavigateTo() where T : IView
{
    var viewType = _viewMap[typeof(T)][0];
    var view = (IView)Activator.CreateInstance(viewType);

    InitializeView(view);
}

In the most basic case, I am just using the first IView type that I find.  As for how the navigation actually happens, I will have to expose a few more implementation details now.

namespace HandWaver.AG.PresenationModel.Navigation.Modules
{
    [Export(typeof(INavigationService))]
    public class URIFrameNavigationService : INavigationService
    {
        public URIFrameNavigationService(string rootNamespace, Assembly va)
        {
            _viewAssembly = va;
            MapViewTypes();
        }

        static Action nullCallback = () => { };

        [Import(AllowRecomposition=true)]
        public Frame TargetFrame { get; set; }

Note that, for now, I am using MEF as an IoC container.  URIFrameNavigationService uses the Export attribute to tell MEF that it implments the INavigationService contract.  When I construct the instance I pass in some basic information and the class uses MEF to import an instance of the Frame class from Silverlight navigation.  So for the most basic kind of navigation, I’m instantiating a new instance of a Page implementing an IView interface and directly setting the Content of the Frame rather than giving it a URI.

Transitions

Next, I’m going to build the “Models” screen of my Audi fan application; in this case we’re talking about Audi cars and not the usual “Model”.  We can go ahead and look at the implementation for InitializeView() now:

void InitializeView(IView v)
{
    Action go = () => 
    {
        TargetFrame.Content = v;
        v.Activate(URIFrameNavigationService.nullCallback);
        History.Navigated(v);
    };

    if (null != TargetFrame.Content && TargetFrame.Content is IView)
    {
        ((IView)TargetFrame.Content).DeActivate(go);
    }
    else
    {
        go();
    }
}

If the Frame is currently displaying an IView, call it’s deactivate method.  When that method is done, the go action is called which activates the new IView.  We also do something with a History object here which I’ll get to in a little bit.  By using callbacks with Activate and Deactivate we’re able to incorporate animations into the navigation mix.  Here is the Activate implementation for Models.xaml:

public void Activate(Action complete)
{
    var trans = (Storyboard)App.Current.Resources["ExpandIn"];
    trans.Stop();
    Storyboard.SetTarget(trans, LayoutRoot);
    trans.Begin();
    EventHandler sbDone = null;
    sbDone = (o, e) =>
    {
        complete();
        trans.Completed -= sbDone;
    };
    trans.Completed += sbDone;
}

I pull a common animation out of resources and call the giving Action when the animation is complete, being sure to unhook event handlers.  In my case, I’d like to use the same Enter and Exit transitions everywhere in the application and I don’t want to keep copy & pasting this code.  I can move this code to a helper class and invoke it this way everywhere else:

public void Activate(Action complete)
{
    Transition.Animate("ExpandIn", LayoutRoot, complete);
}

public void DeActivate(Action complete)
{
    Transition.Animate("ShrinkOut", LayoutRoot, complete);
}

We could, in future refactoring, create some kind of IViewTransitionManager module that automatically does transitions everywhere, and save the Activate and Deactivate methods for other application specific uses.

You can now run the sample application (link at the end of the article) to see the transitions working from the Home screen to the Models screen.

navapp2

 

Passing Parameters

The View Details Hyperlink buttons, above, should navigate to a special screen showing even more  information about the chosen vehicle model.  When this screen loads, then, the chosen Model will need to be available.  We could set some global User State variable specifying what model of Audi was chosen but it would be nicer to navigate to the chosen IView and directly pass parameters to it.  What’s more, it would be ideal if this parameter passing could be strongly typed such that we can determine the types of parameters using Intellisense.

We have already seen how the INavigationService implementation locates and instantiates an IView implementation.  Adding another overload the the interface implementation meets all stated design goals.

/// <summary>
/// Navigate to a View, passing parameters to said view using the supplied Action
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="paramSetter"></param>
void NavigateTo<T>(Action<T> paramSetter) where T : IView;

It may not be apparent at first look what this allows us to do, but consider the current example of choosing a vehicle model from a list.  The ViewModel of the screen we are coming from knows what the selected VehicleModel is.  Using the excellent Generics implementation in C# we can navigate to an IView and easily see what parameters we might be able to pass to it via Intellisense.

protected void ViewDetailsClick(object s, RoutedEventArgs e)
{
    VehicleModel mdl = null;
    var lnk = (HyperlinkButton)s;
    mdl = (VehicleModel)lnk.Tag;
    Navigation.NavigateTo< IModelDetailView>((v) => v.TargetModel = mdl);
}

I am cheating a little bit here, at least it feels like cheating.  My list of vehicle models is implemented as a listbox and I am using (abusing?) the Tag property that Controls expose in Silverlight:

<HyperlinkButton Grid.Column="5" Style="{StaticResource MenuLinkStyle}" Content="View Details" VerticalAlignment="Center" Click="ViewDetailsClick" Tag="{Binding}">                                        
</< span>HyperlinkButton>

Since we can use Bindings for Tag, the actual data type for  each VehicleModel is stored with the Hyperlinkbutton and we can pull it in the Click event handler.  The real power of what we are now able to do is demonstrated when we see the intellisense available when navigating to IModelDetailView and exploring the overloads available:

navapp4

navapp3

Since we have a strongly-typed mechanism, we can create a lambda expression that takes the IView instance and we can inspect the list of public properties available on IModelDetailView.  In my opinion this makes for a perfectly discoverable mechanism for passing parameters directly to an IView.  If you need further help, you could create a class something like RequiredViewParameter;  having such a type in the Intellisene dropdown might help clients determine which properties must be set in order for a View to function.  Obviously, we an set as many properties as we want using the lambda expression.

 

Choosing Between Multiple IView Implementations

Often, there may be only one implementation for a given IView interface at run time and compile time.  For example, there may likely be only a RealFooView (the real production visual) and a MockFooView (used only for unit testing related logic) for a given IFooView interface.  In some cases, though, multiple implementations for a given IView interface may be present in the real run-time system.  For example, there may be “basic” and “expert” views of the same data and these views my be different enough that it doesn’t make sense to use DataTemplates and configuration properties to set up the View.  We’ve already seen that we are keeping a list of IView in the INavigationService, we can now add another overload to this interface to elegantly handle situations where more than one IView implementation is available.

You may have noticed in the various screenshots that there is a CheckBox on the UI with accompanying text “I am a Fanboy”.  The goal here is that if users check this item, we will show them a more rich set of data when they choose View Details.  By passing a sort of Predicate to the INavigationService, we can easily accomplish this goal:

navapp5

For now, the Fanboy checkbox sets a global variable we can access from within Models.xaml.cs.  I’ll use this value from within this Navigate code to create a selector function.

protected void ViewDetailsClick(object s, RoutedEventArgs e)
{
    bool isUserFanboy = App.UserIsFanboy;
    VehicleModel mdl = null;
    var lnk = (HyperlinkButton)s;
    mdl = (VehicleModel)lnk.Tag;
    Navigation.NavigateTo<IModelDetailView>((v) => v.TargetModel = mdl,
        (v) => v.IsFanboyView == isUserFanboy);
}

I can now create two implementations of IModelDetailView, a basic page and the “fanboy” page displaying more detailed information.  Based on this CheckBox option, Navigation will send the user to a completely different IView.

Regular View:

navapp6

Fanboy View, with extra data:

 navapp7

In this case, the Fanboy view could probably have been done without an extra view implementation, but you can likely see how this would be extremely useful.

What Explodes?

As more than one friend has pointed out to me, when building classes to fit into a framework mechanism there can often be an Explosion of classes, enums, interfaces, delegates etc.  What explodes under this approach, or under MVVM in general?

For every Screen in your application you could potentially have a new IView interface, a new ViewModel, and possibly a new Presenter or Commands.

Preventing View Explosion

If a particular View does not need extra methods, you can prevent View explosion by using Generics.  Generics are the answer to everything.  If your View needs a ViewModel and nothing else special, use something like this:

public interface IView : IView where T : ViewModel
{
    T ViewModel { get; set; }
}

This allows you to implement Views, gain the navigation benefits, but not create a new IView interface.

namespace HandWaver.AG.NavDemo.Screens
{
    public partial class SampleView : Page, IView<MainViewModel>
    {

 

Preventing ViewModel Explosion

Given the previous example, you may be worried about your ViewModel classes exploding, one per IView interface.  I may have mentioned before that generics are the answer to everything, sometimes lambda expressions are part of the answer but the answer definitely always involves generics.  You might, for example, need a ViewModel that does nothing but encapsulate an already existing Data Type from your domain.  There’s a generic type for that:

/// 
/// If the ViewModel can be satisfied by a pre-existing data type, 
/// just use that
/// 
/// 
public class ViewModel : ViewModel where T : INotifyPropertyChanged 
{
    public ViewModel(T source)
    {
        Payload = source;
    }

    private T _Payload;

    public T Payload
    {
        get { return _Payload; }
        set
        {
            _Payload = value;
            OnPropertyChanged("Payload");
        }
    }
}

Combined with the previous example, you could get away without another IView or ViewModel implementation:

public partial class SampleView : Page, IView< ViewModel<VehicleModel> >

If VehicleModel satisfies all of your needs for a View, you don’t need to create any new classes or interfaces.

I’ll talk about Presenters and Commands in a future article.

Conclusion

While the current trends in MVVM may downplay the role of a View interface, IView can be a powerful part of your programming toolbox.  Since the View is an actor in this pattern, it makes sense to be able to treat Views a certain way.  In this article you’ve seen how you can build a flexible Navigation framework around Views and even support handy features like strongly typed parameter passing and selecting from multiple implementations when present.  You can do all this without manually creating any kind of mapping.

HandWaver.AG: I mentioned elsewhere I would be publishing the source for all of my Silverlight Guidance demos.  You can find it here, though it may be updated by future articles and look slightly differently than what you read here.

You can also run the Demo Application.

Future Tasks

There are a few things I had to leave out as this article was getting too long.  The first is the promised Navigation History module.  This is 99.9% done but must have its own article. 

Second, and not very relevant here, is the Photo Gallery page.  This will serve up a cool demo when I get to it.

Finally, If you read the source code closely you may have noticed my comments about the reflection-based IView discovery mechanism.   The mechanism makes the assumption that all IViews you can navigate to are in the initial startup assembly.  In most applications that might be a safe assumption, but MEF is working on changing that with dynamic XAP loading.  Once I’ve had a chance to cozy up with MEF more I’ll post an updated solution.  Also related to MEF, storing the composition catalog on the App class is probably not a best practice.

Tags:

Comments (4) -

samcov
samcov
9/17/2009 5:49:39 PM #

The code is missing.

Reply

Damon
Damon
9/17/2009 9:05:12 PM #

The link was missing the "http"; correct now.

Reply

David Roh
David Roh
9/18/2009 12:07:17 PM #

Very nice!

Thank you for sharing your hard work.

David

Reply

David Roh
David Roh
9/18/2009 12:15:59 PM #

When I try to open the project, VS 2008 says that "HandWaver.AG.Demo.Web.csproj" cannot be opened.  The project type is not supported by this installation.

I have all of the latest Silverlight 3 updates - in fact, the latest patch "3.0.40818.0" is possibly the problem.

David Roh

Reply

Pingbacks and trackbacks (3)+

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading


About the author

Damon Payne is a Microsoft MVP specializing in Smart Client solution architecture. 

INETA Community Speakers Program

Month List

Page List

flickr photostream