I've yet to cover all the features of nRoute in depth, but since a lot of people are downloading the framework and the demo app, I thought let me step through code in the demo app to at least provide a rounded overview of the features in nRoute. To structure the content, I will headline each significant part and highlight the relevant aspects in the code. Further, to make some distinctions clear, this post is divided into two parts, one relates to the Infrastructure in the demo app and the other relates to how the consumable/actionable content integrates with the infrastructure, using nRoute of course. Moreover, to get the most out of this long-long post please familiarize yourself with the basics of nRoute, using my series of earlier posts.
PART I: INFRASTRUCTURE SETUP
The Application Class
Right off the bat, in the app.xaml.cs we can see that the application class inherits from a custom type defined in nRoute called StatefullApplication. I've mentioned this before, but the idea of deriving off a custom application type is to primarily give home to a default container. The default container is an application-wide navigation container hook-up point, wherein any navigation without a specified container is handled - think unhandled exception handling as a mental model.
public partial class App
: nRoute.Navigation.Application.StatefullApplication { .. }
Now we have several types of navigation containers build into nRoute, and equally you can define you own custom ones. In this instance we are using a "Statefull Navigation Container" and its companion application class derivate (the StatefullApplication type) - I had
earlier defined the Statefull Container as follows:
This one basically stores the state of the page when you leave it, and restores it when you navigate onto it. This doesn't have a stack of back or forward pages history, it keeps one list of states and applies it whenever you navigate onto the page. So simply your last state is restored once you navigate to the page, in either back/forward or direct navigation manner.
The StatefullApplication type provides a number of features, one that is pertinent here is the NavigationContainer property which allows us to set any navigation container as the application's default container - and as you will read ahead that is exactly what we do.
The Workspaces/Blades Model
Before we can dig deeper into the application class, I want to explain one of the core aspect of the UI - which is the blades/workspaces model that provides one or more tab-like containers for the application. Each workspace/blade is basically a StatefullContainer which can collapse or expand into full view, and features a title bar to its left (see Workspace.xaml). I've exposed all the interaction functionality of the workspace through an interface called IWorkspace, nothing special really - basically think of it as a specialized tab. Further, in order to show the blades/workspaces I created a custom panel called WorkspacesStackPanel - essentially it is like a horizontal accordion, specialized to only show one active workspace and vertical title-bars for all the other collapsed workspaces.
To manage the workspaces there is the WorkspacesViewModel type, which should have really been called the WorkspacesController type. And like my suggestion, this is basically a simple application-wide controller for managing all the workspaces. Again nothing special, however for your own use you might want to amend this to allow adding/removing workspaces, with perhaps a more streamlined workspace-interaction pattern. This controller is exposed on the Application class using the read-only WorkspacesViewModel property.
The Application Startup
The fun starts in the startup event of the application, we do a couple of things here:
- We set the root visual to the MasterPage.xaml [Line 3]
- We then create the workspaces, which initializes the mentioned WorkpsacesViewModel type [Line 5]. Note you can create your own strategy to customize the number of workspaces, in the demo app's case we try and read the "workspaces" querystring key for values between 1 and 9 (try appending "?workspaces=7" to the Url, before the bookmark)
- We set the DataContext of the root visual to the WorkpsacesViewModel, this cascading'ly hooks up the visuals [Line 6]
- Next, we register for browser integration, this functionality is availed through the inherited base application class. Note, while registering we also specify the default Url, which in this case we read from the browser's current bookmark or use the specified default value ("Home/") [Line 8]
- Next, like before we set the active workspace's Initial Url by reading the browser's Bookmark or use the Default value ("Home/") [Line 9] - this allows for the deep-linking feature
- And also we set up a handler that updates the Browser's Title when the active workspace changes [Line 11]
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MasterPage();
CreateWorkspaces(3); // delegates to a helper function
((FrameworkElement)this.RootVisual).DataContext = _viewModel;
base.RegisterBrowserNavigationHooks(base.GetCurrentBookmarkOrDefaultUrl("Home/"));
_viewModel.ActiveWorkspace.InitialUrl = BrowserIndexedNavigationBridge.RemoveBookmarkIndexerInfo(GetCurrentBookmarkOrDefaultUrl("Home/"));
_viewModel.ActiveWorkspaceChanged += (s, e2) =>
base.SetBrowserWindowTitle(this.NavigationContainer != null ? this.NavigationContainer.Title : null);
}
Browser Integration
One of the side-effect of the multiple blades/workspaces is that it complicates the browser-integration strategy, because the browser only keeps a single history-stack whereby we need multiple of these on a per workspace/blade basis. Not to worry though, because like everything else in nRoute the browser integration strategy is totally configurable. The solution in this case is that we append the bookmark with an indexer that identifies which workspace the bookmark belongs too (it looks like ¶3 or ¶1 postfix). The integration is achieved by overriding the CreateBrowserNavigationBridge method in the application class, and providing a custom implementation of type WorkspaceBrowserBridge. When the "bridge" adds to the browser history our custom implementation appends the bookmark with the active workspace index, and when the browser navigates back or forward it parses the workspace index and accordingly sets the active workspace and the Url. This allows us to have a single history-stack for multiple workspaces/bridges integrated within the browser, with full deep-linking support. As a notable, the browser integration behaviour is based on a simple interface of type IBrowserNavigationBridge.
Master Page
The master page (or shell if you prefer) is actually simpler than it appears, it is basically a three column by two row grid (as shown by the red lines), and three borders fill the grid area to define the rounded shape of the visual (as shown by the green borders). We also dynamically clip the grid as per the edges of the borders. The Icons on the left, are placed in a canvas and aligned to the bottom. The bar at the bottom is also a grid, and on the right we have stack panel that block-by-block fills in the side bar.
The main content is an ItemsControl element which is data bound to the workspaces list, and uses the aforementioned WorkspacesStackPanel as the ItemsPanelTemplate.
<!-- WORKSPACEs -->
<ItemsControl ItemsSource="{Binding Workspaces}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cnt:WorkspacesStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Visual Effects
Or the lack off, should be title as I absolutely took a minimalistic approach in recreating the reference UI. Since I made the app in SL2, I purposely deferred using visual goodies in favour of the goodness on offer in SL3 - particularly the GPU based shader effects and Perspective 3D. The one thing I did attempt was the hover-effect as without the visual-highlighting the navigation-links wouldn't be self-apparent. The hover-effect uses attached behaviours with an extensible model so that you can extend it to your custom controls, still it is quite comprehensive. The use template is also pretty simple, have a look:
<Border Style="{StaticResource SideButtonBorderColourBrush}"
hvr:Hover.HoverBackground="{StaticResource SideButtonHightlightBkgBrush}">
In the snippet above, we attach the behaviour to a border and say on hover (mouse over event) change the background to the brush identified. Depending on the visual element you can change the background, foreground and the border on hover. Note, you can adopt this behaviour for use elsewhere, but consider using storyboards to give transitions a visually engaging lift.
PART II: CONSUMABLE & ACTIONABLE CONTENT
Content Composition
To this point we have had the application's infrastructure basically all spelt out, without even having to delve into the actual content - which is good because we want minimal intermingling of the content and the infrastructure that surrounds it. In fact this is the forte of nRoute, that we can keep the content and containers loosely coupled from each other using only Urls - and nRoute takes care of the composition for you. Read about the basics of navigation-based composition in my previous post.
Content Mapping
Now, for the composition to occur we need to mark the content using Urls as their identifier, and to that end we have a couple of way build-in into nRoute - though yet again, you have a flexible pass in that you can create your own loading/mapping strategy. My preference is normally to use attribute-based mapping system in nRoute owing to its simplicity (and which again like everything else you can customize/extend). Below, are some relevant snippets from the code-behinds of various content files, using attribute based mapping:
// Mapping in HomePage.xaml.cs
[MapNavigationContent("Home/", "Home Page")]
public partial class HomePage : UserControl { .. }
// Mapping in InformationPage.xaml.cs
[MapNavigationContent("Pages/AboutInformation", "About")]
public partial class InformationPage : UserControl { .. }
// Mapping in FuturePageViewModel.cs
[MapViewModelViewNavigation("Pages/FuturePage/{Name}",
typeof(FuturePageViewModel), typeof(FuturePage))]
public class FuturePageViewModel { .. }
Using the MapNavigationContent attribute is one the simplest way to annotate content as Urls; as shown above we put it on the content type (in this case the HomePage class), and specify it's Url Identifier ("Home/") and optionally a static title ("Home Page") associated with that Url. The other attribute shown is MapViewModelViewNavigation which as evident is helpful when using the M-V-VM pattern, and therein we can associate the Url with the View type (in this case the FuturePage type) and its associated View-Model type (FuturePageViewModel type). Further, using this attribute the View-Model type is automagically injected into the View, by setting it as it's DataContext when the Url is requested for. Do note, you have flexibility as to where you put these attributes, see my other
posts for examples.
The other build-in way to register is akin to what is present in ASP.NET 3.5sp1 or MVC, which is via statically registering against the NavigationService static class. Below, is an abbreviated snipper from the application class in the demo app:
protected override void RegisterNavigationsRoutes()
{
NavigationService.MapLoadedAssembliesRoutes();
NavigationService.MapRoute("Images/HeartYourWeb",
new NavigationResourceHandler("Images/HeartYourWeb.xaml", NavigationResourceHandler.XamlLoader));
...
}
Here the first call is actually to enable attribute based mapping (for all loaded assemblies), and there after we specifically map the route for a loose-xaml file using the Url Identifier "Images/HeartYourWeb". And as this is a resource in the assembly we use the NavigationResourceHandler to help with that. For more information on
mapping navigation routes please see my other
blog posts, however the big-picture take-away should be that mappings serve the function of abstraction, in this case abstraction of resources as Urls.
Content Navigation
Given the mappings, we can use the registered Urls to navigate to the content; and for most purposes I recommend using the extensive-set of behaviours in nRoute to avail navigation. Throughout the MasterPage you will see exclusive use of the navigation behaviours for content navigation, show below are some examples:
<!-- NAVIGATE ON MOUSE DOWN ON TEXTBLOCK -->
<TextBlock .. nav:MouseDown.NavigateUrl="Pages/FuturePage/Folders" />
<!-- NAVIGATE ON Ctrl+H KEY DOWN, note this is part of a Behaviours Collection -->
<bhv:KeyUpNavigate NavigateUrl="Home/" Key="H" KeyModifiers="Control" />
In the first case here, we attach the navigate behaviour to a TextBlock which on the (LeftButton)MouseDown event will navigate to the specified Url. And in the second case, we have attached another behaviour on the UserControl, that on recognizing the Cntrl+H key combo will navigate to the "Home/" Url. As you can see for the most part this is all pre-defined, however it doesn't have to be, you can also use the Url Address bar at the bottom of the MasterPage to manually navigate to any valid Url (try about:blank). I hope you can see how we at first abstracted the content via Urls, and using the same Urls also realized them visually.
State Management
In order to use the state management facilities in nRoute, one has to opt in using a simple interface called ISupportNavigationState - which is the case with the FuturePageViewModel type. As mentioned earlier this is the View-Model class backing the FuturePage.xaml, and the logic is trivially simple - all it does is provide a random brush (see OnChangeColour which is exposed by the ChangeColourCommand) from the application resources, and also saves/restores both the selected brush and any text entered by the user. It also makes use of tokenized Urls, as specified in the MapViewModelViewNavigation attribute, have a look:
[MapViewModelViewNavigation("Pages/FuturePage/{Name}",
typeof(FuturePageViewModel), typeof(FuturePage))]
public class FuturePageViewModel
: ISupportNavigationState, INotifyPropertyChanged { .. }
In the Url registered we have the {Name} token, which gives us the UI's title to display; this token-value pair is availed in the InitializeState method via its "state" parameter (a name-value dictionary). So a Url like "Pages/FuturePage/LightUpTheWeb/" will display "Light Up The Web" as its title in the page, try
here. This is rather vain, but you could easily get in your OrderId or CustomerId via the Url and do something more constructive.
The second key construct here is the SaveState method that basically takes the two key properties (the selected colour and entered text) and returns it in a dictionary - which represents the state of the page. Now when you navigate onto the page (via either Back/Forward/New navigation manner) the StatefullContainer will restore the state by passing back the dictionary you saved earlier (as seen in the RestoreState method), and in this case we just use that to restore the two key properties. It is really very straightforward, and the benefit being that the visuals (and the View-Model in this case) don't need to be persisted in the memory, only the relevant state is stored and restored as required by the container.
Action De-Composition
In addition to the content composition and navigation, nRoute also features a set of functionality that allows you to address actionable code via Url identifiers; please refer to my actions related post for detailed information. In the Demo app, you will find trivial examples of how to use the Action infrastructure, the snippet below (from WorkspaceNextPreviousActions.cs) shows an action handler that allows you to select the next or previous workspaces/blade:
[MapActionHandler("Workspace/Actions/Previous")]
[MapActionHandler("Workspace/Actions/Next")]
public class WorkspaceNextPreviousActions : IActionHandler
{
public bool CanHandle(IActionDefination action)
{
return true;
}
public void Handle(IActionDefination action)
{
var _viewModel = ((App)Application.Current).WorkspacesViewModel;
var _newIndex = 0;
.. // implementation details
// we select a new workspace, note the use of the dispatcher
action.Request.Dispatcher.BeginInvoke(() => _viewModel.ActivateWorkspace(_newIndex));
}
}
The idea behind actions is to decompose your application's reusable code into a set of independent handlers, which are addressable in a loosely coupled fashion using Url. In the case above we mark the IActionHandler implementation with two Url Identifiers (see the MapActionHandler Attributes) that either select the previous workspace or the next workspace as per the requested Url. Also observe that Actions are consumed off the UI thread, hence the use of the dispatcher [Line 18]. What makes the investment of creating IActionHandler worthwhile is the structured but loosely-coupled way of consuming the exposed functionality, as show below:
<!-- NEXT PREVIOUS ARROWS -->
<TextBlock ToolTipService.ToolTip="Select Previous Workspace" ...
acn:MouseUp.ActionUrl="Workspace/Actions/Previous" />
<TextBlock ToolTipService.ToolTip="Select Next Workspace" ...
acn:MouseUp.ActionUrl="Workspace/Actions/Next" />
As you can see here, we have basically attached behaviours to the TextBlocks which on (LeftButton)MouseDown event execute the action identified by the Url (without any direct reference to the handler). Another point to note if you try and execute an Action Url that isn't registered, it will raise an exception - this is design, because Actions per se have no visual component and without any indicator to their existence, calls might go unheeded which is an unacceptable outcome. So Urls in nRoute might be very loosely-coupled locators, but they are also structured and unambiguous as content/action providers (i.e. not magic-strings).
PART III: SUMMARY
Even though the demo app lacks substantial content, it demonstrates a range of features available in nRoute - however, beyond the technical ins-and-outs I hope what is also apparent is what nRoute brings to the table. From my perspective, nRoute sits in-between the infrastructure code and the actual consumable/actionable content, it bridges the chasm between the two - which in turn makes your code more adaptable and equally your infrastructure more changeable, largely independent of each other.
EDIT: added the State Management section