>

Data Moving SPA with a Context-Dependent Menu

The Data Moving  Menu Control may be used to navigate among the virtual pages of a Single Page Application, too. In this step by step tutorial I will show you how to configure the menu control, and how to adapt it to the authorizations of the currently logged user .

The full working code is available in the “ContextDependentMenu” file on this site and on 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.

The result that you’ll obtain is shown in this Video:

 

As a first step let select the SPA Mvc 5/WebApi 2 SPA project template in visual studio. All SPA visual studio templates may be downloaded from the  Data Moving Examples Codeplex Site.

Select File->New->Project in VS 2013:

ProjectSelection

Then name the new project:, SPAMenuTutorial:

ProjectName

The result should be this one:

FullProject

Now you need to install the Data Moving Plug-in for Mvc 5. Right click on the “References” and select: “Manage Nuget Packages…”:

SelectLocalFeed

In the window that appears, select the local Nuget source on your hard disk where you copied all Data Moving Plug-in Nuget packages:

 

DMPMVC5

Now select the “Data Moving Plug-in 5”, and install it.

Before running the initial SPA that the visual studio template created for you, your Product Key must be placed in the application root Web.Config, in the AppSettings section:

  1. <appSettings>
  2.     <add key="webpages:Version" value="3.0.0.0" />
  3.     <add key="webpages:Enabled" value="false" />
  4.     <add key="ClientValidationEnabled" value="true" />
  5.     <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  6.     <add key="DMPDefaultStyle" value="Bootstrap" />
  7.     <add key="DMPProductKey" value="P-ZzKk-****..........." />
  8.   </appSettings>

 

Now you may run the project:

CourtesyPage

An essential SPA with just an home page, and Login, Logout, and Unauthorized page is created.

All  these  SpaViews are placed in the “Basic” SPA Module that is loaded as soon as the application starts:

InitialViews

The Application folder contains just two dynamic JavaScript files:

  1. Application/MainJs. It contains the SPA start-up code and the definition of the application ViewModel.
  2. Application/AuthorizationJs. This JavaScript file is called by the previous Application/MainJs file and contains all authorization rules for the application SPAViews. Initially the rule set is empty, and the file contains just a commented-out example that shows how to define a rule:

 

  1. @using MVCControlsToolkit.Controls
  2. <script>
  3.     (function () {
  4.         //Add here authorization rules for each virtual page
  5.         //(rules may include also the page role)
  6.         //as the one commented out below
  7.         @*
  8.         @(SPAAuthorizations.New().Claims("logged")
  9.             .Roles("<role1>", "<role2>")
  10.             .AddTo("<module name>", "<view name>",
  11.                     "<role name (optional)>"))
  12.         *@
  13.  
  14.     })();
  15. </script>

 

All SPA templates use bootstrap for styling, and install a bootstrap Nuget package that provides just the “core” CSS without any specific theme. In order to improve, the appearance of our demo we may download the default bootstrap theme from the bootstrap web site:  “bootstrap-theme.min.css “and “bootstrap-theme.css”. Just add these two files to the web site “Content” folder, and then add  "~/Content/bootstrap-theme.css", to the "~/Content/css" style bundle definition in the App_Start/BundleConfig.cs file that contains all bundle definitions:

  1. bundles.Add(new StyleBundle("~/Content/css").Include(
  2. "~/Content/bootstrap.css",
  3. "~/Content/bootstrap-theme.css",// <-Add new file reference here
  4. "~/Content/MVCControlToolkit.ControlsExt.Mobi-" + version + ".min.css",
  5. "~/Content/Site.css"));

 

Without the addition of a theme the appearance of the menu we are going to build would be quite “bad”.

Our plan is:

  1. Defining some users and roles in the application security Database
  2. Defining a new SPA Module with 5 simple SPAViews
  3. Declaring 3 authorization rules that allow just users with the “admins” role to access  SPAView 3, 4  and 5
  4. Defining a Menu to navigate our SPAViews
  5. Adapting the Menu to the current page and to the current logged user.

Defining the security Database

The Models/ApplicationDbContext.cs file contains a Database seeding function that defines the “admins” role and two users: an administrator and a normal user. In an actual application we should modify this code to adapt it to our needs. In this simple tutorial we may use the default users and roles.

The seeding function is  “activated “ by a few lines of code in the Global.asax that are initially commented-out:

  1. protected void Application_Start()
  2. {
  3. ....
  4. ....
  5. ....
  6. //Uncomment to prepopulate db with sample users
  7. //See ApplicationDbContext.cs
  8. /*
  9. Database.SetInitializer
  10.    <SPAMenuTutorial.Models.ApplicationDbContext>
  11.     (new SPAMenuTutorial.Models.ApplicationDbContextInitializer());
  12.     */
  13. }

 

Let uncomment them, and run the application. Now we may login as “admins” with the credentials:

username: admin, password: padmin, as shown in the previous video.

Defining a new SPA Module with 5 simple SPAViews

Now, in visual studio, right click on the Views/Templates folder and select “new folder”. Name the new folder:  “MenuExample”.This folder will contain all files of the new MenuExample SPA Module that we are going to define.

Now, in visual studio right click on the “MenuExample” folder and select: “New Item”:

SPAModule

Select “SPAModule”, and then give to the new SPA module the same name of the previous folder: “MenuExample”:

ModuleName

We should get the following files structure:

ModuleStructure

The two Main and MainJs files contain the logic to assemble all SPAViews we are going to define into an unique SPAModule. More specifically, Main.cshtml contains the logic to assemble all templates, and to route any incoming View request to the right SPAView template, while MainJS.cshtml contains the logic to assemble the AMD code associated to all SPAViews into the unique JavaScript file associated to the whole “MenuExample” module, and the logic to route any SPAView request to the right chunck of JavaScript code  that was previously aggregated to the “MenuExample” module.

Since ,as in most of the cases, we don’t need to modify the default routing rules you don’t need to modify Main, and MainJs.

Now we need to define the Action method that will deploy the “MenuExample” module to the client. Go to the “Controllers/TemplatesController.cs” file,  copy the example “MyTemplate” action method…

ActionMethod

…paste it, and rename any occurrence of “MyModule” into “MenuExample”:

  1. public ActionResult MenuExample(bool isJs)
  2. {
  3.     string module = "MenuExample/Main";
  4.     if (isJs) return this.MinifiedJavascriptResult(module + "Js");
  5.     return PartialView(module);
  6. }

 

Now we may define all SPAViews.

Right click on the “MenuExample” folder, and select again “New Item”. However this time, choose “SPAView”. Repeat the same operation five times, and give to the SPAViews the names:

“Page1”, “Page2”, “Page3”, “Page4”, and “Page5”:

AllPages

Now open each Page1.cshtml…Page5.cshtml file:

  1. @*@model MyViewModel*@
  2.  
  3.  
  4.   <divid="@Html.PrefixedId("pageRoot")"
  5.                 data-helper-prefix="@Html.PrefixedId("")">
  6.         <h3 >@*View title if any*@</h3>
  7.  
  8. </div>

 

Then add the “main-page” Css class  and a different header to each page:

  1. @*@model MyViewModel*@
  2.  
  3.  
  4.   <div class="main-page"id="@Html.PrefixedId("pageRoot")"
  5.                 data-helper-prefix="@Html.PrefixedId("")">
  6.         <h3 >Page 1</h3>
  7.  
  8. </div>

 

You don’t need to define a page ViewModel, since we will use all these pages just to test our menu.

Since pages have no ViewModel wee need to comment-out the instruction that prepare the SPAView JavaScript model in each PagexJs.cshtml file. In each PagexJs.cshtml file locate the instruction:

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

 

and comment it out:

  1. mvcct.core.moduleResult(function (vm) {
  2.     if (!vm._initialized) {
  3.         vm._initialized = true;
  4.         //vm.Content = ko.mapping.fromJS(viewModelContent);

 

Done! Now you may navigate to each of the newly created SPAViews, by adding view and module parameters to the root application url that Visual Studio automatically set in you browser:

http://localhost:3291/?module=MenuExample&view=Page1

Declaring the authorization rules

As already announced pages 3, 4, 5 must be available only for users with the role admins. Open the Views/Templates/Application/AuthorizationJs.cshtml file and add the three rules below:

  1. @using MVCControlsToolkit.Controls
  2. <script>
  3.     (function () {
  4.         //Add here authorization rules for each virtual page
  5.         //(rules may include also the page role)
  6.         //as the one commented out below
  7.         @*
  8.         @(SPAAuthorizations.New().Claims("logged")
  9.             .Roles("<role1>", "<role2>")
  10.             .AddTo("<module name>", "<view name>",
  11.                     "<role name (optional)>"))
  12.         *@
  13.  
  14.         @(SPAAuthorizations.New().Claims("logged")
  15.             .Roles("admins")
  16.             .AddTo("MenuExample", "Page3"))
  17.  
  18.         @(SPAAuthorizations.New().Claims("logged")
  19.             .Roles("admins")
  20.             .AddTo("MenuExample", "Page4"))
  21.  
  22.         @(SPAAuthorizations.New().Claims("logged")
  23.             .Roles("admins")
  24.             .AddTo("MenuExample", "Page5"))
  25.  
  26.     })();
  27. </script>

 

In this simple example we specified access rules for each single SPAView, but in more complex applications you may write also something like: AddTo(“MenuExample”,  “*”), in which case the requirements apply to all SpaViews of the “MenuExample” module. Then, exceptions to this general rule may be applied to specific SPAViews.

Now when  you navigate, to Page3-5, the login form appears. Test it with Page4:

http://localhost:3291/?module=MenuExample&view=Page4

After you successfully login, Page4 appears.

Now,  try to login with username: normalUser. and password: pnormalUser;  you will see the following complaint page:

Unauthorized

Wow! We need only a “smart menu” to navigate our application.

Making Room for our Menu

All SPAViews are loaded in the Views/Home/Index.cshtml page. The code in the above page do the following:

  1. @{var h = Html.SendToClient(m => m, "applicationModel", "");}

 

Declares the application ViewModel, and get a client ViewModel aware HtmlHelper<ApplicationModel>, that is then used to declare the SPAViews hosts where all the SPAViews may be loaded:

  1. <div id="content">
  2.       
  3.  
  4.         @h.ClientTreeIteratorFor(m => m.MainContent, null,
  5.             "mainAfterRender",
  6.             "mainBeforeRemove",
  7.             wrapper: ExternalContainerType.div,
  8.             wrapperAttributes: new{id = "main_host"}
  9.         )
  10.        
  11.       
  12.    
  13.    
  14. </div>

 

Each SPAView host is bound to an applicationModel Property. The initial application ViewModel contains just a single property, called MainContent, used by the main content SPAView host, shown above.  mainAfterRender and mainbeforeRemove are applicationModel methods defined in Views/Templates/Application.MainJs.cshtml, that provide respectively content fade-in and content fade-out.

Since we need a further SPAView host for our menu we must add a new property to the application ViewModel:

  1. public class ApplicationModel
  2. {
  3.     //main virtual page host
  4.     public baseViewModel<SampleClass> MainContent { get; set; }
  5.     //add here further virtual page hosts
  6.  
  7.     //Main Menu Host
  8.     public baseViewModel<SampleClass> MenuContent { get; set; }
  9.  
  10.     //initial page to load
  11.     public externalSPALink InitialPage { get; set; }
  12. }

 

Now we may define our Main Menu host. We place it on top of the Main Content:

  1. <div id="content">
  2.       
  3.         @h.ClientTreeIteratorFor(m => m.MenuContent, null,
  4.             wrapper: ExternalContainerType.nav,
  5.             wrapperAttributes: new { id = "menu_host" }
  6.         )
  7.  
  8.         @h.ClientTreeIteratorFor(m => m.MainContent, null,
  9.             "mainAfterRender",
  10.             "mainBeforeRemove",
  11.             wrapper: ExternalContainerType.div,
  12.             wrapperAttributes: new{id = "main_host"}
  13.         )
  14.        
  15. </div>

 

We enclose the our main menu in a nav tag to improve the application accessibility. The Main Menu host doesn’t need fade-in, and fade-out capability, since it will never be removed, but only modified to adapt it to the context.

We need some Css to align the menu with the main content. Open the Content/Site.css file and append:

  1. /*Menu*/
  2. #menu_host{
  3.     width:800px;
  4.     margin: 10px auto 10px auto;
  5.     min-height: 50px;
  6.     border: none;
  7. }

 

Defining a Menu to Navigate our SPAViews

We will add the “Menu” SpaView to Basic module that is loaded as soon as the application starts Right click on the Views/Templates/Basic folder and add a new SPAView called: “Menu”:

MenuAdded

The Data Moving control suite urnish the default SimpleMenuItem class to be used as ViewModel for each menu item. We may decide to use it, inherit from it or to use a custom class. The definition of the SimpleMenuItemClass is:

  1. public class SimpleMenuItem
  2. {
  3.     public string Text { get; set; }
  4.     public string Link { get; set; }
  5.     public string UniqueName { get; set; }
  6.     public string Target { get; set; }
  7.     public List<SimpleMenuItem> Children { get; set; }
  8. }

 

When the Menu works in a “standard” Mvc application the Link property contains either an Url or the name of a JavaScript function, preceeded either by @, or #  to be executed when the menu item is clicked. Analogously the Target property contains the target where to open the Url.

In the case of a SPA we must supply a function that provides a custom interpretation of both Link and Target. In the simplest case a SPAView may be referenced with a string of the type <module name>.<view name> in the Link, while the host where to load the SPAView may be denotated by its index into an array of possible hosts.

Thus, for Instance, “MenuExample.Page4” denotes the SPAView  “Page4” contained in the “MenuExample” module. If needed, the notation may be extended to include also possible roles of the SPAView, and also an input object to pass to the SPAView:

<module>.<view>.<optional role>#<optional json representation of the SPAView input>

In our simple example the custom Link+Target processing function will accept just the simple notation <module>.<view> plus standard Urls:

  1. vm.Content.virtualNavigation = function (action, target) {
  2.     if (!action) return false;
  3.     if (action.indexOf("/") == 0 ||
  4.         action.indexOf("http") == 0 ) {
  5.         if (target) window.open(action, target);
  6.         else window.location.href = action;
  7.         return;
  8.     }
  9.     var parts = action.split(".");
  10.     var host = target ? parseInt(target) : 0;
  11.     vm._interfaces.mainHosts[host](
  12.         mvcct.ko.dynamicTemplates.defaultPageStore.get(
  13.         new mvcct.ko.dynamicTemplates.virtualReference(
  14.             parts[0], parts[1])));
  15. };

 

Standard Urls are processed first, otherwise we retrieve the SPAView specified in the action parameter from the default page store. The host  where to place the SPAView is decided by interpreting the target argument as an index in the array of all application root hosts that is inserted in the _interfaces property of the Menu ViewModel.

The _interfaces property is copied in each SPAView from the interfaces property of the application ViewModel by  default context rules that are run each time a SPAView is loaded.  All context rules are defined in the file: Script/Modules/contextRules.js. Below the default context rule that provide to each SPAView all interfaces needed to interact with the application:

  1. mvcct.ko.dynamicTemplates.addContextRule(function (data, cb) {
  2.     if (mvcct.ko.dynamicTemplates.isBaseViewModel(data)) {
  3.         data._interfaces = applicationModel.interfaces;
  4.     }
  5.     return false;
  6. });

 

The interfaces property of the application ViewModel is defined in the Views/Templates/Application.MainJs.cshtml file. Below the default definition:

  1. @* Put here all instances of the interface implementations
  2.     you need in the application *@
  3. @* They will be added to all ViewModel by the context rules*@
  4. applicationModel.interfaces = {};
  5.  
  6. applicationModel.interfaces.uiBlock = new uiBlockInterface();

 

As a default the interfaces property contains just an implementation of the interface used to “block” the content of a DOM during a communication with the server. We must add also the array of all hosts (in our case there are just 2  hosts):

  1. @* Put here all instances of the interface implementations
  2.     you need in the application *@
  3. @* They will be added to all ViewModel by the context rules*@
  4. applicationModel.interfaces = {};
  5.  
  6. applicationModel.interfaces.uiBlock = new uiBlockInterface();
  7. applicationModel.interfaces.mainHosts =
  8.     [applicationModel.MainContent, applicationModel.MenuContent];

 

In the same Views/Templates/Application.MainJs.cshtml file we must modify the code that loads the initial SPAViews:

  1. applicationModel.MainContent(
  2.     mvcct.ko.dynamicTemplates.defaultPageStore.get(initLink ||
  3.     new mvcct.ko.dynamicTemplates.virtualReference("Basic", "Home")));

 

…because we must load also our menu:

  1. applicationModel.MenuContent(
  2.     new mvcct.ko.dynamicTemplates.baseViewModel("Basic", "Menu"));
  3. applicationModel.MainContent(
  4.     mvcct.ko.dynamicTemplates.defaultPageStore.get(initLink ||
  5.     new mvcct.ko.dynamicTemplates.virtualReference("Basic", "Home")));

 

The Menu SPAView is not handled by any page store, since it will never be removed from its host.

The vm.Content.virtualNavigation method definition may be placed immediately after the ViewModel initialization in the MenuJS.cshtml file:

  1. mvcct.core.moduleResult(function (vm) {
  2.     if (!vm._initialized) {
  3.         vm._initialized = true;
  4.         vm.Content = ko.mapping.fromJS(viewModelContent);
  5.         @* Initialization code executed only after View
  6.             creation or View redirect  *@
  7.         vm.Content.virtualNavigation = function (action, target) {
  8.             ...
  9.             ...

 

Now we must define viewModelContent to bethe object hierachy that contains all Menu information.

Usually, the ViewModel content is initialized by serializing in JavaScript a .Net data structure, at the beginning of any …Js file:

  1. @*
  2. @Html.JavaScriptDefine("viewModelContent",
  3.     new <dotnet viewModel here>())
  4. *@

 

In our case the .Net data structure is a hierarchy of SimpleMenuItem objects that we may build easily with the helper SimpleMenuBuilder class:

  1. <script>
  2.     @{
  3.         var menuItems = new SimpleMenuBuilder()
  4.             .Add("Home", "Basic.Home")
  5.             .Add("Page 1", "MenuExample.Page1")
  6.             .Add("Page 2", "MenuExample.Page2")
  7.             .Add("private area")
  8.                 .Down()
  9.                     .Add("Page 3", "MenuExample.Page3")
  10.                     .Add("Page 4", "MenuExample.Page4")
  11.                     .Add("Page 5", "MenuExample.Page5")
  12.                 .Up()
  13.              .Add("External Links")
  14.                 .Down()
  15.                     .Add("Data Moving Controls",
  16.                         "http://www.mvc-controls.com")
  17.                     .Add("knockout.js",
  18.                         "http://knockoutjs.com/", null, "_blank")
  19.                 .Down()
  20.             .Get();
  21.     }
  22.     (function () {
  23.  
  24.     @Html.JavaScriptDefine("viewModelContent",
  25.         new SimpleMenuItem {Children=menuItems })
  26.         ...
  27.         ...

 

We added also two sub-items that navigate to external pages.

Now it’s finally time to use the Data Moving Menu control. Open the Menu.cshtml file, and substitute its content with:

  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.     .TemplatesBaseName("Basic_MenuItem")
  14.     .TemplateSelector(
  15.         @"function(x){
  16.                     return (x.Children.peek() ?
  17.                         'Basic_MenuItem1' : 'Basic_MenuItem0');
  18.                 }")
  19.     .CustomNavigate("Content.virtualNavigation")
  20.     .AddRowType()
  21.         .StartColumn(m => m.Text)
  22.             .CustomColumnClass(GenericCssClasses.NoWrap)
  23.         .EndColumn()
  24.     .EndRowType()
  25.     .AddRowType()
  26.         .StartColumn(m => m.Text)
  27.             .CustomColumnClass(GenericCssClasses.NoWrap)
  28.         .EndColumn()
  29.         .ChildCollection(m => m.Children)
  30.     .EndRowType();
  31.  
  32. }
  33. @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:

ul.mainmenu
{
    padding: 0 0 0 0 !important
    font-size:1.2em;
   

ul.mainmenu > li
{
  margin5px 7px 5px 7px !important;     
}
ul.mainmenu ul {border: none; background-color:transparent; }

 

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 or virtual SPAView references, 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 two templates, that are configured inside two AddRowType()-EndRowType() blocks. Both templates are based on the default menu row template, since no custom row template is defined, and both of them have an unique column that shows the menu item title. Each menu item title is contained in the Text property of the underlying data item. The addition of the GenericClasses.NoWrap Data Moving predefined Css class prevents menu item titles from wrapping. The only difference between the two templates is the ChildCollection call in the second declaration, that causes the recursive rendering o all children sub items contained in the children property. The two AddRowType blocks define two knockout client templates. Their names are obtained by adding the postfixes ‘0’ and ‘1’ to the template base name “Basic_MenuItem” declared with TemplateBaseName. It is good practice to give name of the type <module name>_<a name> to all client templates used by controls declared inside SPAViews.

The first template is used for the menu items that can’t have children (their Children property is a null observable) while the second template is used for the menu items that may have children (their Children property is a possibly empty observable array). The selection of the right template is performed by the template selection function declared with TemplateSelector. In our case the function is declared on line, but it may be also the name of a SPAView ViewModel method.

CustomNavigate declares the custom navigation function we have already defined in the MenuJs file.

That’s enough to see the menu working! Run the application and enjoy your first Data Moving SPA Menu!

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

Now, our last objective is adapting 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 classes 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.mainHosts =
  3.     [applicationModel.MainContent, applicationModel.MenuContent];
  4. applicationModel.interfaces.contextHandler = //<-add here
  5.     new iContextDependentHandler();

 

The iContextDependent.select method must be called each time a new SPAView is placed in the application main host. Accordingly, it may be called in the main host mainAfterRender that is defined as an applicationModel method in the Views/Templates/Application/MainJs.cshtml file:

  1. applicationModel.mainAfterRender = function (x, y) {
  2.     //start added code
  3.     if (y && mvcct.ko.dynamicTemplates.isBaseViewModel(y))
  4.         applicationModel.interfaces.contextHandler
  5.             .select(y.module + '.' + y.view)
  6.     else
  7.         applicationModel.interfaces.contextHandler
  8.             .select(null)
  9.     //end added code
  10.     $(mvcct.core.filterHtml(x)).fadeOut(0).fadeIn(400);
  11. };

 

The if checks that the module and view properties are actually defined, otherwise a null value passed to the select method simply de-selects any previously selected SPAView.

The iContextDependent.authorize method must be called each time the logged user changes. The instruction that initializes the authorization system in the Views/Templates/Application/MainJs.cshtml file accepts an array of observables, and functions that are automatically notified whenever the user changes. This is the right place for calling our authorize method:

  1. mvcct.ko.dynamicTemplates.enableAuthorizationManager(
  2.     "@Url.Content("~/api/Account/CurrentUser")",
  3.     "@Url.Content("~/api/Account/Logout")",
  4.     function () {
  5.         @* on logout *@
  6. },
  7. [applicationModel.MainContent,
  8. applicationModel.interfaces.contextHandler.authorize//<-add here
  9. ] @*all observables to refresh when the user changes*@
  10.     );

 

iContextDependent.register and iContextDependent.unregister must be called in the Views/Templates/Basic/MenuJs.cshtml JavaScript file associated with our Menu SPAView.

The right place 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. vm.beforeRemove = function () {
  8.     @* Insert here clean up code to be  executed
  9.         when the page is unloaded*@
  10.     vm._interfaces.contextHandler.unregister(vm.Content);
  11.     cleanupDomOnUnload(vm);
  12. };

 

Now our vm.Content ViewModel must implement  the IContextDependent  interface. You may place the interface definition immediately after the vm.Content.virtualNavigation method we defined before:

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

 

Both methods call two private recursive functions that traverse the menu data items hierarchy to do their job.

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 recusive 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:

  1. @*Insert private functions and private readonly variables here*@
  2. function selectMenu(children, action, target) {
  3.     for (var i = 0; i < children.length; i++) {
  4.         var item = children[i];
  5.         if (action == ko.utils.unwrapObservable(item.Link) &&
  6.             (!target ||
  7.             target ==
  8.             (ko.utils.unwrapObservable(item.Target) || "0"))) {
  9.             mvcct.html.menu.selected(item, true);
  10.             return item;
  11.         }
  12.         var iChildren = ko.utils.unwrapObservable(item.Children);
  13.         if (iChildren) {
  14.             var child = selectMenu(iChildren, action, target);
  15.             if (child) return child;
  16.         }
  17.     }
  18.     return false;
  19. }
  20. function authorizeMenu(authorizationManager, obsChildren) {
  21.     var children = ko.utils.unwrapObservable(obsChildren);
  22.     if (!children || children.length == 0) return false;
  23.     var hasChildren = false;
  24.  
  25.     var aChange = false;
  26.     for (var i = 0; i < children.length; i++) {
  27.         var item = children[i];
  28.         var action = ko.utils.unwrapObservable(item.Link);
  29.         var virtualLink = action &&
  30.             action.charAt(0) != '#' &&
  31.             action.charAt(0) != '@@' &&
  32.             action.charAt(0) != '/' &&
  33.             action.indexOf("http") != 0 &&
  34.             action.indexOf("mailto") != 0;
  35.         var authorized = false;
  36.         if (virtualLink) {
  37.             var parts = action.split('.');
  38.             authorized =
  39.                 !authorizationManager.verifyAuthorization(
  40.                     new mvcct.ko.dynamicTemplates
  41.                         .virtualReference(parts[0], parts[1])
  42.                     );
  43.         }
  44.         else authorized = action;
  45.         authorized = authorized || false;
  46.         if (authorized) authorized = true;
  47.         var childrenAuthorized =
  48.             authorizeMenu(authorizationManager, item.Children);
  49.         authorized = authorized || childrenAuthorized;
  50.         if ((item._destroy || false) != !authorized) {
  51.             item._destroy = !authorized;
  52.             aChange = true;
  53.             if (item._destroy) mvcct.ko.unfreeze(item);
  54.         }
  55.         if (authorized) hasChildren = true;
  56.     }
  57.     if (aChange) obsChildren.valueHasMutated();
  58.     return hasChildren;
  59. }
  60. @*End of private area*@
  61.         mvcct.core.moduleResult(function (vm) {
  62.             ...
  63.             ...
  64.             ...

 

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 to improve performance.

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:

SelectedMenu

When a menu item get the focus the focus style overrides its “selected color” (this is due to the way bootstrap handles the focus style), if you don’t like this behavior you may change it by appending the following Css rule to the Site.css file:

  1. .mvcct-menu-item-selected:focus{
  2.     background-color: #d9edf7 !important;
  3.     border:2px solid yellow !important;
  4. }

 

Where background-color must match “selected color” that in the case of  bootstrap is the “alert-info” Css class background-color.

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. @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.  
  15. .TemplatesBaseName("Basic_MenuItem")
  16. .TemplateSelector(
  17.     @"function(x){
  18.                 return (x.Children.peek() ?
  19.                     'Basic_MenuItem1' : 'Basic_MenuItem0');
  20.             }")
  21. .CustomNavigate("Content.virtualNavigation")
  22. .AddRowType()
  23.     .StartColumn(m => m.Text)
  24.         .CustomColumnClass(GenericCssClasses.NoWrap)
  25.     .EndColumn()
  26. .EndRowType()
  27. .AddRowType()
  28.     .StartColumn(m => m.Text)
  29.         .CustomColumnClass(GenericCssClasses.NoWrap)
  30.     .EndColumn()
  31.     .ChildCollection(m => m.Children)
  32. .EndRowType();
  33.  
  34. }
  35. @builder.Render()

 

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

 

AlternateSelect

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” file of 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