Tuesday, October 19, 2010

Jounce Part 2: Getting Started (Tagging and Binding)

In this second installment about the Jounce framework, I'll share how to get started with Jounce and wire your first view bound to a view model.

The Jounce project is available online at http://Jounce.Codeplex.com.

Jounce injects itself as a service to the application. In my opinion, using IApplicationService is the right way to handle start up and shut down in a Silverlight application. It is also an opportunity to wire in configuration because a context is passed that contains the initialization parameters, as well as hook into events such as unhandled exceptions.

When you create a new application, you can blow away all of the junk you see in the App.xaml.cs code behind. Instead, you go into the XAML and insert the Jounce Application Service in a special section for application services:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:Services="clr-namespace:Jounce.Framework.Services;assembly=Jounce.Framework" 
             x:Class="Jounce.App"
             >
    <Application.ApplicationLifetimeObjects>
        <Services:ApplicationService/>
    </Application.ApplicationLifetimeObjects>
</Application>

That's all Jounce needs to register and plug into the Silverlight application lifecycle. When the appplication first starts, it will call this method:

public void StartService(ApplicationServiceContext context)
{
    var logLevel = LogSeverity.Warning;

    if (context.ApplicationInitParams.ContainsKey(Constants.INIT_PARAM_LOGLEVEL))
    {
        logLevel =
            (LogSeverity)
            Enum.Parse(typeof (LogSeverity), context.ApplicationInitParams[Constants.INIT_PARAM_LOGLEVEL], true);
    }

    _mainCatalog = new AggregateCatalog(new DeploymentCatalog()); // empty one adds current deployment (xap)

    var container = new CompositionContainer(_mainCatalog);

    CompositionHost.Initialize(container);
    CompositionInitializer.SatisfyImports(this);

    if (Logger == null)
    {
        ILogger defaultLogger = new DefaultLogger(logLevel);
        container.ComposeExportedValue(defaultLogger);
    }

    DeploymentService.Catalog = _mainCatalog;
    _mefDebugger = new MefDebugger(container, Logger);
}

Here, Jounce is doing several things. It sets a default level for logging, but also checks the initialization parameters and overrides the level to whatever is set. This allows you to control the logging level by adding the parameter to the Silverlight object host. In this example, Jounce is being configured to log verbosely:

<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
 <param name="source" value="ClientBin/Jounce.xap"/>
 <param name="onError" value="onSilverlightError" />
    <param name="initParams" value="Jounce.LogLevel=Verbose" />
 <param name="background" value="white" />
 <param name="minRuntimeVersion" value="4.0.50826.0" />
 <param name="autoUpgrade" value="true" />
 <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0" style="text-decoration:none">
   <img src="http://go.microsoft.com/fwlink/?LinkId=161376" alt="Get Microsoft Silverlight" style="border-style:none"/>
 </a>
</object><iframe id="_sl_historyFrame" style="visibility:hidden;height:0px;width:0px;border:0px"></iframe>

Jounce wires in a default aggregate catalog to manage various deployment catalogs (this is what allows for dynamic XAP loading). There are two key things that also happen. First, Jounce declares an import for the logger contract that looks like this:

[Import(AllowDefault = true, AllowRecomposition = true)]
public ILogger Logger { get; set; }

The "allow default" tells MEF that null is fine and don't complain if nothing is exported for the contract. If you decide to implement your own logger and export it, it will be imported here and Jounce will use your custom logger. However, if you don't provide one, that's fine - Jounce can check the property for a null value and supply it's own default logger that simply writes to the debugger. This is exported to the container so future requests for a logger will pull in the default logger supplied by Jounce.

The MefDebugger is also important because it wraps the MEF container with some debug code that spits out lots of diagnostic information to help you troubleshoot what's going on with the Managed Extensibility Framework. If you debug the default application shipped with Jounce, you'll see plenty of activity in the debug window.

0/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger :: MEF: Found part: Jounce.ViewModels.WelcomeViewModel
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::    With import: Jounce.Core.ViewModel.BaseViewModel.EventAggregator (ContractName="Jounce.Core.Event.IEventAggregator")
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::    With import: Jounce.Core.ViewModel.BaseViewModel.Router (ContractName="Jounce.Core.ViewModel.IViewModelRouter")
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::    With import: Jounce.Core.ViewModel.BaseViewModel.Logger (ContractName="Jounce.Core.Application.ILogger")
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::    With export: Jounce.ViewModels.WelcomeViewModel (ContractName="Jounce.Core.ViewModel.IViewModel")
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::       With key: ViewModelType = Welcome
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::       With key: ExportTypeIdentity = Jounce.Core.ViewModel.IViewModel
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::    With export: Jounce.ViewModels.WelcomeViewModel (ContractName="Jounce.Core.ViewModel.IViewModel")
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::       With key: ViewModelType = Welcome
10/19/2010 6:44:29 AM Verbose Jounce.Core.MefDebugger ::       With key: ExportTypeIdentity = Jounce.Core.ViewModel.IViewModel

This is just one example of a "part" found and the resulting imports and exports. This is very valuable to track down why imports or exports might not be behaving the way you expect. Take a look at the source for the MefDebugger. It spins through all parts in the container, but then hooks into an event so that any time the container is rewired (such as when a dynamic XAP file is loaded) it will dump the changes for you as well.

The next important lifecycle event is the Starting method, called after the initialization but before the visual tree is completely wired. Take a look here:

public void Starting()
{
    Application.Current.UnhandledException += Current_UnhandledException;

    var viewInfo = (from v in Views where v.Metadata.IsShell select v).FirstOrDefault();

    if (viewInfo == null)
    {
        var grid = new Grid();
        var tb = new TextBlock
                        {
                            Text = Resources.ApplicationService_Starting_Jounce_Error_No_view
                        };
        grid.Children.Add(tb);
        Application.Current.RootVisual = grid;
        Logger.Log(LogSeverity.Critical, GetType().FullName,  Resources.ApplicationService_Starting_Jounce_Error_No_view);
        return;
    }

    Application.Current.RootVisual = viewInfo.Value;
    Logger.LogFormat(LogSeverity.Information, GetType().FullName,
                        Resources.ApplicationService_Starting_ShellResolved, MethodBase.GetCurrentMethod().Name,
                        viewInfo.Value.GetType().FullName);
    Logger.Log(LogSeverity.Information, GetType().FullName, MethodBase.GetCurrentMethod().Name);            

    EventAggregator.Publish(viewInfo.Metadata.ExportedViewType.AsViewNavigationArgs());
}

Jounce hooks into the unhandled exceptions and provides a method that basically raises a custom message. This is done using the Event Aggregator (more on that later) but makes it easy to subscribe to exceptions and even handle them in your code.

When you tag a view in Jounce, you can mark a view as the shell. This should only be done with one view. Jounce looks for that view and sets it to the root visual, or creates an error message and displays that instead.
The view metadata is imported to the application service like this:

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

Jounce then logs some messages and raises a navigation event. We'll cover navigation in Jounce later - the important thing to know is that this navigation event is what informs Jounce that it should go out and find any related view models and bind them to the view.

How did we get the view to appear as the shell?

[ExportAsView("Welcome",IsShell = true)]
public partial class Welcome
{
    public Welcome()
    {
        InitializeComponent();            
    }
}

Jounce gives you plenty of flexibility to tag your views and view models. The only rule is that each tag should be unique. Here, we're just using a "magic string" but in most applications (and you'll see the examples in the quick starts) constants can be defined instead. In this case, the view is tagged with the tag "Welcome" and set as the shell. That's all it takes to make that view the first view that appears.

Of course, the whole point of the framework is MVVM. In the Jounce default example, we create a simple view model that handles a welcome message but also kicks off an animation when loaded. The view model looks like this:

[ExportAsViewModel("Welcome")]
public class WelcomeViewModel : BaseViewModel 
{
    public WelcomeViewModel()
    {
        Title = "Welcome to Jounce!";            
    }

    private string _title;

    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            RaisePropertyChanged(()=>Title);
        }
    }

    public override void _Activate(string viewName)
    {            
        GoToVisualState("WelcomeState",true);
    }
}

This view model is also tagged like the view. It can have a different tag than the view, because Jounce allows for the same view model to be bound to multiple views. What's important here is that it is tagged.

The base view model offers several services, including methods you can override when the view model is created for the very first time (_Initialize), anytime the view it is bound to becomes activate (_Activate) and anytime the view is deactivated (_Deactivate).

When the navigation event for a view is raised, Jounce intercepts this call and binds the view model and calls the appropriate methods.

Take a look at the view router. It is an event sink for the view navigation event - this allows it to receive any global messages sent for navigation:

public class ViewRouter : IPartImportsSatisfiedNotification, IEventSink<ViewNavigationArgs>

The view router in turn calls the ActivateView method on the ViewModelRouter. The method looks like this:

public bool ActivateView(string viewName)
{
    Logger.LogFormat(LogSeverity.Verbose, GetType().FullName, Resources.ViewModelRouter_ActivateView,
                        MethodBase.GetCurrentMethod().Name,
                        viewName);

    if (HasView(viewName))
    {
        var view = ViewQuery(viewName);

        var viewModelInfo = GetViewModelInfoForView(viewName);

        if (viewModelInfo != null)
        {
            var firstTime = !viewModelInfo.IsValueCreated;

            var viewModel = viewModelInfo.Value;

            if (firstTime)
            {
                viewModel.GoToVisualState =
                    (state, transitions) =>
                    JounceHelper.ExecuteOnUI(() => VisualStateManager.GoToState(view, state,
                                                                                transitions));
                _BindViewModel(view, viewModel);
                viewModel.Initialize();
            }
            viewModel.Activate(viewName);
        }

        return true;
    }
    return false;
}

As you can see, a view model is found to match the view and is bound (the view model router will look for LayoutRoot and bind the view model there ... if it can't find it, it will bind it to the parent control's DataContext). You can also see that Jounce binds an action called GoToVisualState that executes on the UI thread. This allows the view model to fire a visual state transition without referencing the view. In our example, the _Activate method is overridden to call:

public override void _Activate(string viewName)
{            
    GoToVisualState("WelcomeState",true);
}

This triggers a visual state transition and animations the "Welcome to Jounce" message.

There is one final piece missing. We never specified how to know that the welcome view model belongs with the welcome view. In Jounce, I decided against a traditional locator pattern to provide more flexibility. Instead, a binding is exported that maps a view model to a view. What's nice about this approach is that the binding can be consolidated in a class that holds all bindings for the application, or localized to the view so it is clear what view model the view is binding to. You could also write your own interface, such as a fluent or convention-based interface, and export bindings to the container to achieve the same result.

In the Welcome view, we mark the binding by exporting the tag for the view model and the tag for the view here:

[Export]
public ViewModelRoute Binding
{
    get
    {
        return ViewModelRoute.Create("Welcome", "Welcome");
    }
}

That's all there is to it! From your perspective, you had to:

  1. Add the Jounce Application service to the App.xaml
  2. Build your view model and tag it
  3. Build your view and tag it, then mark it as the shell
  4. Export a binding for the view model and the view

Jounce took care of all the rest.

The Jounce project is available online at http://Jounce.Codeplex.com.

Jeremy Likness

11 comments:

  1. Your framework is very elegant ! I have not try it yet, but wonder if it was "Blendable" too : Is the xaml's DataContext property feeded, and does the ViewModel properties appears in the "DataContext Pane" ?

    ReplyDelete
  2. Thanks! The answer is YES. It was VERY important to me that the framework was design-time friendly, and in the quick starts you'll see several examples of designer-friendly view models.

    ReplyDelete
  3. How do you specify the authentication type (windows/forms) with your implementation?

    Normally you would do something like

    appsvc:FormsAuthentication
    or
    appsvc:WindowsAuthentication

    ReplyDelete
  4. That's really part of the ASP.NET host, not the Silverlight plugin.

    ReplyDelete
  5. but normally in the app.xaml.cs constructor you would do something like this when using RIA services with silverlight

    // Create a WebContext and add it to the ApplicationLifetimeObjects
    // collection. This will then be available as WebContext.Current.
    var webContext = new WebContext {Authentication = new WindowsAuthentication()};
    ((WindowsAuthentication)webContext.Authentication).DomainContext = new AuthenticationContext();
    ApplicationLifetimeObjects.Add(webContext);

    How/where would you go about wiring this up now?

    ReplyDelete
  6. I like the framework, but do have a question concerning it's implementation.

    I thought that to be "proper MVVM", the ViewModel had to know nothing of the view? If that's the case, then surely this breaks that rule by using the GoToVisualState method?

    How do you test this with VisualState in place as wouldn't this fail without a UI?

    ReplyDelete
  7. The ViewModel absolutely has to know about the view - it's whole purpose is to synchronize view state. When you have a boolean that is data-bound and uses a converter to modify visibility, that is knowledge of a stae that the view model has.

    The visual states are similar. You are enforcing that the view has a visual state. And it works the same way as the boolean example - if the boolean is not data-bound, it is never used. Likewise, if the target visual state doesn't exist, then there is no exception thrown - it is absorbed by the runtime.

    Both of these are decisions you have to make and say, "This view should have this binding" and "this view should have these states."

    ReplyDelete
  8. PS - to address the concern about failing without a UI, not true. The GoToVisual state is a delegate. In testing, you can mock it to any method without and UI and test that it is being called without having a UI to invoke it on. It is wired to the actual VSM as an implementation detail by the framework, not inside the view model hierarchy itself, so it remains safe for testing.

    ReplyDelete
  9. Jeremy - Thanks for showing us such an elegant framework. I have one question about View/ViewModel binding. In the first article, you mentioned the framework supports binding multiple views to a view model. However it seems the current implementation only supports one to one binding here: in the function ViewModelRouter.ActiveView(), the _BindViewModel() method only get called if the view model is initialized first time. Is it right ? Thanks.

    ReplyDelete
  10. Yes, that is a bug and will be fixed in the next release. The workaround for now is to wrap the nested view model - not ideal, but gets you there until I release the built-in support. I.e.

    SecondViewModel (use Router to grab first view model).

    ReplyDelete
  11. SO I was rushing through and cramming on Jounce and SL for upcoming project....

    I saw "When you create a new application, you can blow away all of the junk you see in the App.xaml.cs code behind"

    so I did and then added the "Services" lines into the App.xaml and ran... and going the SL loading spinner, and just sat on it... then downloaded and opened the quickstarts (I had installed using NuGet) and found the App.xaml.cs actually does need some basic code in it...

    just a comment... liking it so far but only been playing for couple hours so far while multi tasking on other items.

    ReplyDelete