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:
- Add the Jounce Application service to the
App.xaml
- Build your view model and tag it
- Build your view and tag it, then mark it as the shell
- 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.