>

Data Moving SPA with a Context-Dependent Menu 2: Url based menu and SEO

Data Moving SPA routing is not based on string Urls but  on the “virtualReference”  JavaScript class. Each instance of a virtual link contains all information needed to load a new view: view name, module containing the view, view input, and also an optional string property called “role” that  has the purpose of giving  different names to different instances of the same view. More specifically, the developer has the option to store each view in a page store. This way when the user returns on a previous page he finds it in the same state he left it.: “roles” are needed to distinguish between different stored view instances that play different roles in the interaction with the user.

Usual links are substituted by virtual links containing a virtual references instead of a standard Urls. This solution avoid the burden of encoding all information needed to instantiate a view into a single Url. In fact, the main disadvantages of Url encoding are:

  1. Once view properties have been encoded in a string that is hardcoded in the application sources it becomes difficult to  locate them during some re-engineering of the code.
  2. As the application grows a more complex encoding might be required, but locating and substituting Urls spread all over the code is an almost impossible task.

 

The substitution of Urls with virtual references is acceptable since, usually, we don’t need a SPA be indexed by search engines. However, now the support of search engines for SPAs and for JavaScript is increasing. More specifically:

  1. There is a standard protocol to substitute the dynamic pages of a SPA with static “page snapshots” produced by the server  whenever the SPA is visited by a search engine. See here for more details.
  2. Now Google is able to run JavaScript, and to visit also SPA

 

The above circumstances makes SPA attractive also for standard Web Sites that needs search engines indexing. For this kind of SPAs Urls play the same role they have in standard Web Applications. Namely, they furnish a way to reach all pages that compose the application and, together with all semantic tags, they help search engines to classify properly all pages.

The Url based ko.routing router was conceived as an attempt to keep the SEO advantages of standard  Urls while overcoming all disadvantages of keeping information encoded into strings (Urls).

ko.routing may compute dynamically all Urls starting from all parameters that must be encoded in the Urls themselves. This result is obtained by “inverting” the routing rules. The router action method, and the related knockout.js action binding accept the parameters to be extracted from an Url and build the Url that would yield exactly those parameters. They are very similar to the Asp.net Mvc Url.Action method, and have the same purpose of keeping the code completely independent from the routing rules.

The 1.2 release of the Data Moving Controls suite added the support for ko.routing, and offers also SPA project templates based on ko.routing, namely: SPA_Mvc5_ko.routing.vsix and SPA_Mvc4_ko.routing.vsix

In this post I’ll revisit the Data Moving SPA with a Context-Dependent Menu tutorial with the new ko.routing based visual studio templates. The main difference is that now all menu items will contain actual links with actual Urls.

Building the basic project

You may repeat exactly the same steps of the Data Moving SPA with a Context-Dependent Menu tutorial till the “Defining a Menu to Navigate our SPAViews” section(not included), with the following two differences:

  1. Select the WebApi 2/Mvc 5/ko.routing Single Page Application project template instead of the WebApi 2/Mvc 5/Single Page Application  template. Attention: you must install both vsix in VS 2013 since only the second vsix contains the SPAView and SPAModule item templates.
  2. In all Page1.cshtml…Page5.cshtml dynamic JavaScript files supply an appropriate document title: vm._title = “View 1”…vm._title=”View 5”.

Defining a Menu to Navigate our SPAViews

We will add the “Menu” SPAView to the Basic module that is loaded as soon as the application starts, since our menu is needed also in the first loaded page.  Right click on the Views/Templates/Basic folder and add a new SPAView called: “Menu”:

In the previous tutorial we built the object tree containing all information to display in the menu on the server side and then we serialized it in JavaScript, in order to take advantage of the Data Moving SimpleMenuBuilder class. Now we can’t do the same since we are going to build all menu Urls with the help of the JavaScript ko.routing action method to keep our code independent from the client side routing rules.

Accordingly as a first step  we define a JavaScript menuBuilder  class to build our JavaScript object tree with an easy to use fluent interface. We may place this definition at the beginning of the MenuJs.cshtml file:

  1. <script>
  2.     
  3.     (function () {
  4.         
  5.         function menuBuilder(templateSelection) {
  6.             var currLevel = [];
  7.             var levels = [];
  8.             this.add = function (text, link, target, tSelection) {
  9.                 if (mvcct.utils.isObject(link))
  10.                     currLevel.push({
  11.                         Text: text || "",
  12.                         Params: link || null,
  13.                         Link: "/"+ko.routing.action(link),
  14.                         Target: "",
  15.                         UniqueIdentifier: null,
  16.                         Children: null,
  17.                         templateSelection:
  18.                             tSelection || templateSelection
  19.                     });
  20.                 else
  21.                     currLevel.push({
  22.                         Text: text || "",
  23.                         Params: null,
  24.                         Link: link || null,
  25.                         Target: target || "",
  26.                         UniqueIdentifier: null,
  27.                         Children: null,
  28.                         templateSelection:
  29.                             tSelection || templateSelection
  30.                     });
  31.                 return this;
  32.             };
  33.             this.get = function () {
  34.                 return currLevel;
  35.             };
  36.             this.down = function () {
  37.                 if (!currLevel.length) return this;
  38.                 var father = currLevel[currLevel.length - 1];
  39.                 levels.push(currLevel);
  40.                 father.Children = currLevel = [];
  41.                 
  42.                 return this;
  43.             };
  44.             this.up = function () {
  45.                 if (!levels.length) return this;
  46.                 currLevel = levels.pop();
  47.                 return this;
  48.             };
  49.         };

 

The add method adds a new menu item. If just the Text field is passed we define a not clickable Item (typically a father menu item with sub items). If the link target is a string it is interpreted as an Url, otherwise, if it is an object it is interpreted as the set of parameters to be passed to the action method to yield the actual Url. Since all default routing rules patterns contain only the hash part of the Urls(something like #….)  and since strings starting with # are interpreted as custom JavaScript click handlers  instead of Urls by the menu control we add a leading “/”. The Target parameter is taken into account only for string Urls, since it has no interpretation for the SPA Urls.

Each menu item stores both the computed Url and the original parameters used to compute it, since the parameters are needed to verify if a menu item Url refers to a SPAView  the current user has the right to visit or not. Moreover, the original parameters help in verifying if the current SPAView is an instance of the SPAView referenced by an item menu, with some possible input parameter more(simple string comparison between two Urls is able to verify only if both Urls reference  the same SPAView with exactly the same input).

The client template used to render each menu item is selected by calling a template selection function that receives the menu data item as argument. In order to ensure the maximum flexibility each menu item has its own template selection function. However if no item specific function is passed, the default template selection function passed as an argument to the menuBuilder constructor is taken.

The down method moves the fluent interface to the definition of the sub-items of the current item, and the up method returns to the definition of the father item.

Finally the get method yields the final object tree.

Now we are ready to build the Menu ViewModel:

  1. var templateSelection = function (x) {
  2.     if (x.Children.peek()) return 'Basic_MenuItem2';
  3.     var url = x.Link ? x.Link.peek() : null;
  4.     if (url && url.charAt(0) != '@@' && url.charAt(0) != '#') return 'Basic_MenuItem1';
  5.     return 'Basic_MenuItem0';
  6. };
  7. var menuItems = new menuBuilder(templateSelection)
  8.     .add("Home", { module: "Basic", view: "Home" })
  9.     .add("Page 1", { module: "MenuExample", view: "Page1" })
  10.     .add("Page 2", { module: "MenuExample", view: "Page2" })
  11.     .add("Private Area")
  12.         .down()
  13.             .add("Page 3",
  14.                 { module: "MenuExample", view: "Page3" })
  15.             .add("Page 4",
  16.                 { module: "MenuExample", view: "Page4" })
  17.             .add("Page 5",
  18.             { module: "MenuExample", view: "Page5" })
  19.         .up()
  20.         .add("External Links")
  21.         .down()
  22.             .add("Data Moving Controls",
  23.                 "http://www.mvc-controls.com")
  24.             .add("knockout.js",
  25.                 "http://knockoutjs.com/", "_blank")
  26.         .up()
  27.     .get();
  28. var viewModelContent = { Children: menuItems};

 

The viewModelContent is the put in place by the standard scaffolded code below:

  1. vm.Content = ko.mapping.fromJS(viewModelContent);

 

that creates all needed knockout observables an put the new modified model in the  ViewModel Content property.

The default template selection function selects 3 different templates: Basic_MenuItem2 for father items with children items, Basic_MenuItem1 for all items that do not contain an Url, and Basic_MenuItem0 for all items containing an Url. Each template name is obtained by chaining a base name declared with the .TemplatesBaseName method of the Menu fluent interface with the the sequence number of the template defined with the Menu fluent interface. Below the content of the Menu.cshtml file:

  1. @model MVCControlsToolkit.Controls.SimpleMenuItem
  2.  
  3. @{
  4.     var builder = Html.ExtendedClientMenuFor(
  5.         m => m.Children, m => m.Link,
  6.         new { @class = "mainmenu" });
  7.     if (HttpContext.Current.Request.Browser.IsMobileDevice)
  8.     {
  9.         builder = builder.ActiveOnlyOnClick();
  10.     }
  11.     builder.Target(m => m.Target)
  12.     .Radiuses(0.5f, 1f, 0.5f)
  13.     .ItemSelection(structurePathSelectClass: "light-select")
  14.     .TemplatesBaseName("Basic_MenuItem")
  15.     .TemplateSelector(
  16.         "function(x){return x.templateSelection(x);}")
  17.     .AddRowType()
  18.         .StartColumn(m => m.Text)
  19.             .CustomColumnClass(GenericCssClasses.NoWrap)
  20.             .CustomTemplateFixed(TemplateType.Display,
  21.             @<text>
  22.                 <a href="javascript:;">@item._D(m=>m.Text)</a>
  23.             </text>
  24.             )
  25.         .EndColumn()
  26.     .EndRowType()
  27.     .AddRowType()
  28.         .StartColumn(m => m.Text)
  29.             .CustomColumnClass(GenericCssClasses.NoWrap)
  30.             .CustomTemplateFixed(TemplateType.Display,
  31.             @<text>
  32.                 @item.LinkFor(m => m.Text, m => m.Link)
  33.             </text>
  34.             )
  35.         .EndColumn()
  36.     .EndRowType()
  37.     .AddRowType()
  38.         .StartColumn(m => m.Text)
  39.             .CustomTemplateFixed(TemplateType.Display,
  40.             @<text>
  41.                 <a href="javascript:;">@item._D(m=>m.Text)</a>
  42.             </text>
  43.             )
  44.         .EndColumn()
  45.         .ChildCollection(m => m.Children)
  46.     .EndRowType();
  47.  
  48. }
  49. @builder.Render()

 

The ExtendedClientMenuFor Html<T> extension declares the root menu items collection, the property that contains the level 0 menu items, and the menu data item property that contains the “action” to execute when the menu item is selected.Then, ExtendedClientMenuFor  returns a fluent interface to continue the menu configuration. The root Menu ul tag contains the mainmenu Css class  that defines some <ul> and <li> structural properties, and that removes underlining from all menu links.

The above definitions must be appended to the Content/Site.css file. You may experiment by changing some of them.

As a default sub-menus appear when the mouse hover their father menu item, but If the client device is a mobile device the call to ActiveOnlyOnClick() change this behavior, and sub-menus are open on “click” or “tap”.

Then we declare which item property contains the target where to open links, and the various menu items radiuses (see the picture that shows the three radiuses here ).

The way each menu item is rendered is defined by 3 templates, that are configured inside 3 AddRowType()-EndRowType() blocks. All templates have an unique column based on a custom template. The addition of the GenericClasses.NoWrap Data Moving predefined Css class prevents menu item titles from wrapping. The ChildCollection call in the last template causes the recursive rendering of all children sub items contained in the Children property. All AddRowType blocks define knockout client templates. Their names are obtained by adding the postfixes ‘0’ and ‘1’ and ‘2’ to the template base name “Basic_MenuItem” declared with TemplateBaseName. It is good practice to give names of the type <module name>_<a name> to all client templates used by controls declared inside SPAViews.

The selection of the right template is performed by the template selection function declared with TemplateSelector. In our case a function declared on line recalls the default template selection method previously defined in each menu data item.

That’s enough to see the menu working! Now you may run the application and enjoy your Menu!

Adapting the Menu to the Current Page and to the Current Logged User

Now, our last objective is to adapt the menu to the logged user and to the actual SPAView that is in the main host. More specifically:

  • Each time the user changes the menu must display just the items the new user has the right to access, that is: if all sub-menus of a father menu item  are not visible and if the father menu item itself doesn’t connect to a SPAView the user may access, the father menu item  must be invisible
  • The menu item that connects to the actual SPAView that is in the main host must appear in a “selected state”.

Both requirements may be achieved by letting the content ViewModel of our menu implements an interface, say,IContextDependent with two methods:

  • authorize(), that adapts the SPAView (our menu SPAView) to the authorizations of the current user.
  • select(x), that informs the SPAView (our menu SPAView) that the SPAView x is currently in the main host. Where x is a string with the same format of the Link property of our menu data items.

In general we may handle several SPAViews that implements the IContextDependent interface in a modular way as follows:

  1. We define a IContextDependent  resource handler with methods to register and unregister IContextDependent implementations for being properly handled by the application.
  2. Each time a SPAView that implements IContextDependent, is loaded it registers itself to the IContextDependent  resource handler, and then it unregisters itself when it is unloaded.
  3. Whenever the current user changes, the IContextDependent  resource handler is notified and calls the authorize() method of all registered IContextDependent   implementations.
  4. Whenever the current SPAView in the main host changes the the IContextDependent  resource handler is notified and calls the select(x) method of all registered IContextDependent   implementations.

We may define the class that implements the IContextDependent  resource handler in the Views/Templates/Application/MainJs.cshtml file next to the definition of the uiBlockInterface class:

  1. var iContextDependentHandler = function () {
  2.     var registrations = [];
  3.     this.register = function (x) {
  4.         registrations.push(x);
  5.     };
  6.     this.unregister = function (x) {
  7.         var index = -1;
  8.         for (var i = 0; i < registrations.length; i++) {
  9.             if (registrations[i] == x) {
  10.                 index = i;
  11.                 break;
  12.             }
  13.         }
  14.         if (index > -1) {
  15.             array.splice(index, 1);
  16.         }
  17.     };
  18.     this.select = function (x) {
  19.         for (var i = 0; i < registrations.length; i++) {
  20.             var item = registrations[i];
  21.             if (item.select) item.select(x);
  22.         }
  23.     };
  24.     this.authorize = function () {
  25.         for (var i = 0; i < registrations.length; i++) {
  26.             var item = registrations[i];
  27.             if (item.authorize) item.authorize();
  28.         }
  29.     }
  30. }

 

Then, we add an instance of this class to the applicationModel.interfaces property, so that it is available within the applicationModel itself and to all SPAViews:

  1. applicationModel.interfaces.uiBlock = new uiBlockInterface();
  2. applicationModel.interfaces.contextHandler = //<-add here
  3.             new iContextDependentHandler();

 

The IContextDependent.select method must be called each time the router selects a new SPAView is placed in the application main host. Accordingly, let open the Views/Templates/Application/RoutingJs.cshtml file that contain the definition of all routing rules and let add the .select call immediately after the the start of a new SPAView loading:

  1. (function () {
  2.     ko.routing.defaultAction(function (obj, level, hasChildren) {
  3.         vl = new mvcct.ko.dynamicTemplates
  4.                 .virtualReference(obj.module, obj.view, obj.role);
  5.             vl.input = obj;
  6.             applicationModel.MainContent(
  7.                 mvcct.ko.dynamicTemplates.defaultPageStore.get(vl));
  8.             setTimeout(function () {// <-- added line
  9.                 applicationModel.interfaces.contextHandler.select(obj)
  10.             });
  11.     });
  12.     ko.routing.mainRouter = applicationModel.mainRouter =
  13.         new window.ko.routing.router()
  14.         .route("#!/:module/:view")
  15.         .notFound({ module: "Basic", view: "NotFound" });
  16. })();

 

Since all default routing rules use the default action function our call must be placed only there. The default action uses the view, module and role parameters to create a virtual reference and then passes all parameters extracted by the Url as input to the SPAView (Input property of the virtual reference). Then the virtual link is used to fetch the SPAView from the default page store. If an instance of the required SPAView with the specified role(if any) is already stored it is returned, otherwise a new fresh copy is created  (and stored) by the page store possibly downloading the required module from the server. Finally, the newly fetched SPAView is inserted in the main page host (applicationModel.MainContent).

The .select call is wrapped inside a setTimeOut to ensure it be executed only after that all IContextDependent implementations (in our case just the menu) have been loaded when the SPA starts.

The right place where to call IContextDependent.register is the processInput method that is called each time a SPAView is loaded, while the right place to call IContextDependent.unregister is the beforeRemove method that is called before the SPAView is unloaded:

  1. vm.processInput = function (nodes) {
  2.     @* Code here is executed each time the page is loaded.
  3.         Virtual page inputData must be processed here *@
  4.     vm._interfaces.contextHandler.register(vm.Content);
  5.     modifyDomOnLoad(vm, nodes);
  6. };
  7.  
  8. vm.beforeRemove = function () {
  9.     @* Insert here clean up code to be  executed
  10.         when the page is unloaded*@
  11.     vm._interfaces.contextHandler.unregister(vm.Content);
  12.     cleanupDomOnUnload(vm);
  13. };

 

Now our vm.Content ViewModel must implement  the IContextDependent  interface. You may place the interface definition immediately before the vm.processInput method definition:

  1. vm.Content.select = function (obj) {
  2.     if (!obj) {
  3.         if (currentSelected)
  4.             mvcct.html.menu.selected(currentSelected, false);
  5.         currentSelected = null;
  6.         return;
  7.     }
  8.     var res =
  9.         selectMenu(
  10.             ko.utils.unwrapObservable(vm.Content.Children),
  11.             obj);
  12.     if (!res && currentSelected)
  13.         mvcct.html.menu.selected(currentSelected, false);
  14.     currentSelected = res;
  15. };
  16. vm.Content.authorize = function () {
  17.     authorizeMenu(
  18.         mvcct.ko.dynamicTemplates.authorizationManager(),
  19.         vm.Content.Children);
  20. }
  21. vm.Content.authorize();

 

Both methods call two private recursive functions that traverse the menu data items hierarchy to do their job. They are exactly the same of the ones defined in the Data Moving SPA with a Context-Dependent Menu tutorial.

The currentSelected private variable contains the menu data item that is currently selected, if any, otherwise, null. If a null action is passed, currentSelected is set to null and any previously selected menu item is unselected by calling mvcct.html.menu.selected(currentSelected, false).

Otherwise the reclusive selectMenu function tries to locate a menu data item matching the action string. If such a data item is found the menu item it is bound to is set in the selected state by calling: mvcct.html.menu.selected(item, true)and the data item itself is returned in the res variable, so it may substitute the previous currentSelected. If the selectMenu recursive search fails, null is returned in res, and any previously selected item is unselected by calling mvcct.html.menu.selected(currentSelected, false).

The authorize method immediately calls the recursive private function authorizeMenu passing it the current authorization manager, and the level 0 menu data items. The authorizeMenu function traverses the menu data items hierarchy and checks possible links to SPAView against the authorization manager to verify if the SPAView may be accessed by the current user. In case the SPAView referred by a menu data item cannot be accessed by the user, the data item rendering is prevented by setting its _destroy property to true. If a menu item x has the only purpose of showing its children sub-items, and if all its children have _destroy set to true also the _destroy property of x is set to true, since it is completely un-useful.

The private functions selectMenu  and authorizeMenu may be placed in the read-only private members area immediately before the mvcct.core.moduleResult call. They differ from the analogous function of the Data Moving SPA with a Context-Dependent Menu tutorial only in the way references to SPAViews are compared:

  1. function selectMenu(children, obj) {
  2.     for (var i = 0; i < children.length; i++) {
  3.         var item = children[i];
  4.         var toTest = ko.utils.unwrapObservable(item.Params);
  5.         var match = true;
  6.         for (var prop in obj) {
  7.             if (!toTest || ko.utils.unwrapObservable(obj[prop])
  8.                 != ko.utils.unwrapObservable(toTest[prop])) {
  9.                 match = false;
  10.                 break;
  11.             }
  12.         }
  13.         if (match) {
  14.             mvcct.html.menu.selected(item, true);
  15.             return item;
  16.         }
  17.         var iChildren = ko.utils.unwrapObservable(item.Children);
  18.         if (iChildren) {
  19.             var child = selectMenu(iChildren, obj);
  20.             if (child) return child;
  21.         }
  22.     }
  23.     return false;
  24. }
  25. function authorizeMenu(authorizationManager, obsChildren) {
  26.     var children = ko.utils.unwrapObservable(obsChildren);
  27.     if (!children || children.length == 0) return false;
  28.     var hasChildren = false;
  29.  
  30.     var aChange = false;
  31.     for (var i = 0; i < children.length; i++) {
  32.         var item = children[i];
  33.         var action = ko.utils.unwrapObservable(item.Params);
  34.         var virtualLink = action;
  35.         var authorized = false;
  36.         if (virtualLink) {
  37.             authorized =
  38.                 !authorizationManager.verifyAuthorization(
  39.                     action
  40.                     );
  41.         }
  42.         else authorized = ko.utils.unwrapObservable(item.Link);
  43.         authorized = authorized || false;
  44.         if (authorized) authorized = true;
  45.         var childrenAuthorized =
  46.             authorizeMenu(authorizationManager, item.Children);
  47.         authorized = authorized || childrenAuthorized;
  48.         if ((item._destroy || false) != !authorized) {
  49.             item._destroy = !authorized;
  50.             aChange = true;
  51.             if (item._destroy) mvcct.ko.unfreeze(item);
  52.         }
  53.         if (authorized) hasChildren = true;
  54.     }
  55.     if (aChange) obsChildren.valueHasMutated();
  56.     return hasChildren;
  57. }

It is worth to point out that each time we make invisible a menu item we call the mvcct.ko.unfreeze to release any possible cached template, since most of Data Moving controls cache templates instances to improve performance.

The selectMenu function compares the set of parameters of each menu item with the set of parameters that identifies the current SPAView. If the current SPAView contains some parameters more, the comparison is considered successful since those parameters are further specifications of the input contained in the menu item.

Now the context dependent menu is ready. Whenever you navigate to a SPAView that is referenced in a menu item that menu item will become visible and will appear in a selected state:

If a permanently opened sub-menu is not acceptable in your application, you may customize the Css class that is added to all menu items on the path to the selected item by calling the ItemSelection method of the menu fluent interface. In the example below, a yellow border is added to all items on the path to the selected  item instead of leaving them permanently in the opened state:

  1. .Radiuses(0.5f, 1f, 0.5f)
  2. .ItemSelection(structurePathSelectClass: "light-select")//<-

 

  1. .light-select{
  2.     border:2px solid yellow !important;
  3. }

 

In this case the “selected color” on the menu item linking to “Page 4” becomes visible only if you hover the yellow-border “private area”:

You may also add a breadcrumb and modify the menu select method to update the breadcrumb, too.

That’s all for now! The full code is available in the “ContextDependentMenu_ko_routing” file that may be found here and in the Data Moving Plugin Examples Codeplex site download  area. The Visual studio solution must be activated by installing the  DataMovingPlugin5Examples.x.x.x.nupkg file you get with the product.

Francesco