>

Data Moving Plug-in SPA View Engine: Handling Contextual Information and User Authorization

Data Moving Plug-in SPA View Engine

 

This is the second article about new Data Moving Plug-in SPA View Engine, so the previous article is a pre-requisite for reading it.

In any MVVM (Model View ViewModel)  SPA (Single Page Applications) dependencies among the View-ViewModel pairs, and dependencies of each View-ViewModel pair on the global state of the application may undermine the modularity and stability of the whole application:

  1. When a View-ViewModel pair is released  we MUST be sure that all pointers to data structures inside the pair are removed, otherwise we may cause memory leaks, and hard to find bugs. For instance, if an event handler maintains a reference to a disposed pair, it may trigger unexpected behaviors each time the event is fired.
    As a consequence  data structures inside a View-ViewModel pair can’t depend on observables that are outside of the pair. Accordingly, external dependencies can’t be updated dynamically by using the observer pattern, but must be copied inside the pair by framework components that are run ONLY on pairs that for sure have not been released. The best time to perform such an update operation is immediately before a pair is rendered since released pairs are not rendered anymore. However, this implies also that we need to force manually re-rendering of pairs displayed on the screen when a dependency changes.
  2. In theory each View-ViewModel pair might contain reference to global application data structures without undermining the application stability, since when the pair is released these references die with the pair. Thus, we might use global data structures for the communication of each View-ViewModel pair with their environment. Unluckily, the use  of global variables is an anti-pattern that is known to undermine the modularity of an application. In fact, a change in the global data structures might invalidates all modules! Moreover, modules that depend on global variable are not re-usable in different contexts.

 

The Data Moving Plug-in View Engine faces all above problems by concentrating the contextual dependencies in context rules that are run immediately before rendering a View-ViewModel pair. Context rules are run on the ViewModel being rendered till a matching rule is found. The matching rule collects global information the View-ViewModel pair might depend on both to  build a context object and to fill some ViewModel properties. The information copied by the context rules in the ViewModel may include interface objects that may be used by the ViewModel methods to manipulate the global application state. This way each template ViewModel pair may cooperate with the remainder of the application while keeping its modularity. In case of virtual pages the interface passed to a ViewModel usually depends not only on the module, and local template name, but also on the role assigned to the virtual page by the virtualReference object that required the page to the page store. The virtual page role is available in its role property.

This way all burden of interpreting the external environment falls on the contextual rules and doesn’t undermine the modularity of the View-ViewModel pairs: View-ViewModel pairs may be used in different contexts by simply changing or adding some context rules, without modifying their code.

The purpose of the context object is twofold:

  1. The context object  contains all information needed  for selecting the right version of the template-AMD module pair to instantiate. In fact, the action methods that deploys both the templates and the AMD of each module may accept parameters such as the name of the logged user and the selected culture, to adapt the module to the context. Such parameters MUST  be inserted in the URL because both the AMD and the templates are cached by the browser, thus the insertion of the contextual parameters in the URL is the only way to force the browser to cache different copies for each different set of parameters.
    The context object may contain also a redirect directive that forces the loading of a completely different view (belonging to the same module or to a different module). This capability is used to redirect an user to the login virtual page when he/she tries to access a virtual page that is not available for anonymous users. Moreover, after log-in the insertion of the username among the parameters that are sent to the server allows the personalization of the template/AMD modules to the actual privileges of the user.
  2. The context object contains a synthesis of global information the View-ViewModel pair depends on that is used to decide if the pair needs to be re-rendered, or not. In fact, virtual pages retrieved from the page store normally are not  re-rendered but used as they were left the last time they were used. However, if the context object created when the pair was rendered differs from the current context the pair is re-rendered to adapt it it to the new context. More specifically, re-rendering forces the re-application of the javascript function defined in the AMD module to the ViewModel, that takes advantage of the information about the external environment  written in the ViewModel properties by the context rules to adapt the View-ViewModel pair to the new situation. In case the context rules add different parameters to the URL, or redirect to a different View the ViewModel is completely reset and filled with new content and rendered with a different template.

 

Below a context rule that handles a page that is available only for logged-in users:

  1. mvcct.ko.dynamicTemplates.addContextRule(function (data, cb) {
  2.     var currUser = mvcct.ko.dynamicTemplates.authorizationManager().currentUser();
  3.     if (data.module == "People") {
  4.         if (!currUser) {
  5.             cb.redirect("Login", "Home", "Home");
  6.         }
  7.         else {
  8.             cb.add(currUser)
  9.               .addTicket();
  10.         }
  11.         return true;
  12.     }
  13.     return false;
  14. });

 

A new context rule is added by passing a function to the mvcct.ko.dynamicTemplates.addContextRule method. This function receives the ViewModel and a context builder object as parameters. The function is expected to return true when its pre-concitions are satisfied and false otherwise. Inside the context rule we may build the context object by using the fluent interface of the context builder object.

In our case the preconditions of the rules are satisfied only if the module name is “People”. In more complex applications having several modules that require login, a simple solution is to give them a common name prefix: something like “secure_people”, so that context rules may just check this prefix.

In our case the rule verifies if the user is logged in with a call to the authorizationManager. If the user is not logged, we insert a “redirect to the login virtual page” request into the context object by calling the redirect method of the context builder.

If the user is logged, instead, we add the username and the authentication ticket to the url. In this case we don’t  write anything in the ViewModel, since the partial views that implement the templates and AMD will take care of adapting the content deployed to the logged user.

The addition of the authorization ticket to the URL must be done if and only if the AMD and template contain sensitive information that must be protected from unauthorized  accesses. The insertion of the authorization ticket in the URL DE FACTO prevents the caching of the templates and AMD in the browser cache, since the authorization ticket usually has a short life. Moreover, caching MUST be explicitly prohibited with an adequate OutputCache attribute to protect the sensitive information from malicious users that might manipulate the Browser Cache.

For the above reasons, it is advised to avoid the insertion of the authorization ticket in the URL, and to it is advised to require any sensitive information with a subsequent ajax call to the server. In this case the username  passed in the URL may be used just to adapt the graphics of the virtual page to the logged user by removing links to pages that the user is not authorized to visit, and by customizing the virtual page with some user preference the user might have provided someway.

The add context builder method stacks the strings passed as parameters in the URL one after the other, before the module name. For instance if after the add(currUser) we perform another add for the selected culture we get URLs like: …./en-US/john32/People/ for the templates and …./en-US/john32/People.js for the AMD. In case we require also the addition of the authorization ticket, the ticket is appended just to AMD module URL,: …./en-US/john32/People.js?ticket=<authorization ticket>. The request for the templates doesn’t need to include explicitly the authorization ticket in the URL, since this request uses cookies and consequently it is authenticated with the authorization ticket contained in the Asp.net authorization cookie.

The context builder object contains also other two add methods, namely:

  1. addv(x): it stacks the string passed as parameter before the template local name, instead of before the module name. This means that the module request remain unchanged, but a different template that is contained in the same module is requested. For instance if we stack the “Logged” string with addv, to the view named “List” contained in the “People” module, then  the request for the People module remains unchanged: …./People/ for the templates and …./People.js for the AMD, but the template used to render the ViewModel will be People/Logged/List instead of People/List.
  2. addc(x): it just modifies the context object without modifying neither the module name nor the view name. All strings passed with addc are stacked together to record a dependency from the environment. This way, when the above strings change the difference in the context forces the re-rendering of the View-ViewModel pair. Normally addc is used together with changes in some ViewModel properties. This way, addc forces the re-rendering of the View-ViewModel pair, and the re-rendering of the pair produces a different result because of the ViewModel modifications.

 

Below a set of context rules adequate for handling user authorization:

  1. (function () {
  2.     mvcct.ko.dynamicTemplates.addContextRule(function (data, cb) {
  3.         if ((data.view == "Login" || data.view == "Register" ) && data.module == "Home") {
  4.             if (mvcct.ko.dynamicTemplates.authorizationManager().currentUser()) {
  5.                 cb.vadd("Logged");
  6.             }
  7.             return true;
  8.         }
  9.         return false;
  10.     });
  11.     mvcct.ko.dynamicTemplates.addContextRule(function (data, cb) {
  12.         var currUser = mvcct.ko.dynamicTemplates.authorizationManager().currentUser();
  13.         if (data.module == "People") {
  14.             if (!currUser) {
  15.                 cb.redirect("Login", "Home", "Home");
  16.             }
  17.             else {
  18.                 cb.add(currUser)
  19.                   .addTicket();
  20.             }
  21.             return true;
  22.         }
  23.         return false;
  24.     });
  25.     mvcct.ko.dynamicTemplates.addContextRule(function (data, cb) {
  26.         var currUser = mvcct.ko.dynamicTemplates.authorizationManager().currentUser();
  27.         if (mvcct.ko.dynamicTemplates.isBaseViewModel(data)) {
  28.                           //force re-rendering of all virtual pages
  29.                           //if logged user changes
  30.             cb.cadd(currUser || '');
  31.             return true;
  32.         }
  33.         return false;
  34.     });
  35. })();

 

The first rule changes the view used to render the Login and Register virtual pages in case the user is already logged, by adding a view prefix with vadd. The second rule is the rule we already analyzed for redirecting anonymous users to the login page when they need to access pages that require login.

Finally, the last rules is fired for all virtual pages that doesn’t  satisfy the previous rules. The mvvct.ko.dynamicTemplates.isBaseViewModel verifies that data is a virtual page ViewModel. This rules, requires the re-rendering of a virtual whenever the logged user changes by means of the cadd method. This re-rendering is necessary because of page headers that might depend on the logged use (for instance the login/logout link).

It is worth to point out that re-rendering doesn’t imply necessarily any loss of data that might be contained in the ViewModel. Data are reset just when a virtual page is rendered the first time, or when it has been re-rendered because of a change of the template/javscript selected. Thus, re-rendering caused by a cadd doesn’t cause any loss of data but just a refresh of the contextual information.

The javascript code associated to any template is informed about the need to re-initialize the whole ViewModel through the _initialized boolean property that is added to each ViewModel. When the javascript code initializes a ViewModel it sets this property to true. The property is automatically re-set to false whenever the framework realizes that the ViewModel needs to be re-initialized. Below an example of usage of the _initialized property:

  1.    <script>
  2.     (function () {
  3.         @Html.JavaScriptDefine("viewModel",  new HumanResourcesViewModel())
  4.         function afterRender(nodes, vm) {
  5.             vm.updater.options({
  6.                 onUpdateComplete: function () {
  7.                     $.unblockUI();
  8.                 },
  9.                 htmlStatusMessages: function (statusCode, statusText) {
  10.                     if (statusCode == 401) return "@SiteResources.Unauthorized";
  11.                     else return statusText;
  12.                 },
  13.                 updatingCallback: function () {
  14.                     $.blockUI();
  15.                     return true;
  16.                 }
  17.             });
  18.             vm._utilities.humanResourcesRM.options({
  19.                 htmlStatusMessages: function (statusCode, statusText) {
  20.                     if (statusCode == 401) return "@SiteResources.Unauthorized";
  21.                     else return statusText;
  22.                 }
  23.             });
  24.         }
  25.         mvcct.core.moduleResult(function (vm) {
  26.             if (!vm._initialized) {
  27.                 vm._initialized = true;
  28.                 vm._utilities = {};
  29.                 vm._utilities.clientSubmit = function (id, jTarget, operation) {
  30.                     if (operation == "operation-submit") {
  31.                         var jForm = jTarget.closest('form');
  32.                         if (jForm.validate().form()) vm.updater.update(jForm);
  33.                         return false;
  34.                     }
  35.                     return true;
  36.                 };
  37.                 vm._utilities.selectItem = function (act) { act.addClass("item-selected"); };
  38.                 vm._utilities.unselectItem = function (prev, act) {
  39.                     prev.removeClass("item-selected"); return false;
  40.                 };
  41.                 vm.Content = ko.mapping.fromJS(viewModel);
  42.                 vm._title = "@SiteResources.HumanResourcesListTitle";
  43.             }
  44.             return { afterRender: afterRender };
  45.         });
  46.     })();  
  47. </script>

 

In the example above, if re-rendering is required because of a cadd-only context change the template is re-rendered, and the afterRender function is re-executed on the newly created html, but all ViewModel building is saved because it has been enclosed within an (!vm_initialized) if.

The context object may be accessed also in the javascript code associated to a template by calling the function: mvcct.ko.dynamicTemplates.getContext(vm).  In the example below the context object is used to decide which javascript module to call for initializing the ViewModel:

  1. <script>
  2.     define(function () {
  3.         @Html.IncludeStaticJsModules("Home/MenuJs",
  4.             "Home/RegisterJs", "Home/LoginJs", "Home/LogoutJs", "Home/RegistredJs")
  5.         return mvcct.core.withIncludedModules(
  6.             function (menuF, registerF, loginF, logoutF, registredF) {
  7.             return function (vm, viewName) {
  8.                 var context = mvcct.ko.dynamicTemplates.getContext(vm);
  9.                 vprefix = context ? context.vprefix : '';
  10.                 if (viewName == "Menu") return menuF(vm);
  11.                 else if (viewName == "Register") return vprefix == "Logged" ?
  12.                     registredF(vm) : registerF(vm);
  13.                 else if (viewName == "Login") return vprefix == "Logged" ?
  14.                     logoutF(vm) : loginF(vm);
  15.                 return null;
  16.             }
  17.         });
  18.     });  
  19. </script>

 

In fact in case the user is logged we not only need completely different templates, but also a completely different initialization code, since the underlying data are completely different.

In order to add multi-lingual support to the context rules of our previous example it is enough to add a .add(<language string>)  call to all our context rules. The <language string> may be extracted by the cookie we use to record the user language settings.

 

In order to add support for user authorization we need also to initialize properly the authorizationManager, and to implement  the action methods, to login, logout, register, and to verify the name of the currently logged user. The Register action method is not conceptually different from any standard register method. Below, all other methods:

  1. [OutputCache(NoStore=true, Duration = 0)]
  2. public ActionResult Logout()
  3. {
  4.     if (HttpContext.User.Identity.IsAuthenticated) FormsAuthentication.SignOut();
  5.     return Json(true, JsonRequestBehavior.AllowGet);
  6. }
  7. [OutputCache(NoStore = true, Duration = 0)]
  8. public ActionResult CurrentUser()
  9. {
  10.     if (HttpContext.User != null && HttpContext.User.Identity.IsAuthenticated)
  11.         return Json(HttpContext.User.Identity.Name, JsonRequestBehavior.AllowGet);
  12.     return Json("", JsonRequestBehavior.AllowGet);
  13.     
  14. }
  15. public ActionResult Login(LoginModel model)
  16. {
  17.  
  18.     var rb = ResponseBuilder.NewResponseBuilder(model, ModelState);
  19.     string valueToReturn = "";
  20.     if (ModelState.IsValid)
  21.     {
  22.         if (Membership.ValidateUser(model.Username, model.Password))
  23.         {
  24.             SPASecurity.SetAuthCookie(model.Username, false);
  25.             valueToReturn = model.Username;
  26.         }
  27.         else
  28.         {
  29.             ModelState.AddModelError("",
  30.                 "The user name or password provided is incorrect.");
  31.         }
  32.     }
  33.     return Json(rb.GetResponse(additionalData: valueToReturn));
  34. }

 

The first two methods are self-explanatory, while the login action method differs from a standard Mvc Login method just for the use of the SPASecurity.SetAuthCookie method instead of the FormsAuthentication.SetAuthCookie method.

The login model is sent to the server by a viewModelUpdatesManager, that takes care also of dispatching possible server errors(such as the login failure):

  1. mvcct.core.moduleResult(function (vm) {
  2.     if (!vm._initialized) {
  3.         vm._initialized = true;
  4.         vm.undoRedo = mvcct.core.undoRedo();
  5.         vm.undoRedo.with(function () {
  6.             vm.Content = ko.mapping.fromJS(loginForm);
  7.         });
  8.         var updaterOptions = {
  9.             onUpdateStart: function () {
  10.                 $.blockUI();
  11.             },
  12.             onUpdateComplete: function (e, result, status) {
  13.                 if (e.success) {
  14.                     vm.undoRedo.reset();
  15.                     vm.Content.Password('');
  16.                     var redirect = vm.hasRedirect();
  17.                     mvcct.ko.dynamicTemplates.authorizationManager()
  18.                         .declareUser(result.additionalData, !redirect);
  19.                     if (!redirect)
  20.                         vm.goTo(new mvcct.ko.dynamicTemplates.virtualReference("Home", "Menu"));
  21.                 }
  22.                 $.unblockUI();
  23.             }
  24.         };
  25.         vm.updater = mvcct.viewModelUpdatesManager(
  26.             "@Url.Action("Login", "Account")",
  27.             vm,
  28.             "Content",
  29.             vm.Content,
  30.             "",
  31.             updaterOptions);
  32.         vm._title = "@SiteResources.LoginTitle";
  33.     }
  34.     return {afterRender: afterRender };
  35. });

 

In the onUpdateComplete callback, in case of success,  we reset the undo-redo stack of the Data Moving Plug-in Form, reset the password property of the ViewModel, then we declare the new user (contained in the additionalData property of the response) to the authorizationManager. If the ViewModel rendered the login page because of a redirect context directive,  the ViewModel will be automatically re-initialized to display the original content required as soon as the new user is declared to the authorizationManager. Otherwise, we pass false in the user declaration, which requires a silent user change. As a consequence no re-rendering of the current virtual page is attempted. Then we go manually to the home page by calling the goTo method of the virtual page (see the previous post of the series for more details on the goTo method).

The call to the Logout  and CurrentUser action methods are handled automatically by the by the logout(onError) and updateUserFromserver(onServerResponse, onError) methods of the authorizationManager, that are called from within the click hanlders of the logout and refresh links that are in the header of each virtual page:

HeaderLinks

  1. vm.refresh = function () {
  2.     mvcct.ko.dynamicTemplates.authorizationManager().updateUserFromserver();
  3. };
  4. vm.logout = function () {
  5.     mvcct.ko.dynamicTemplates.authorizationManager().logout();
  6. };

 

The authorization managers is initialized with the call:

  1. mvcct.ko.dynamicTemplates.enableAuthorizationManager(
  2.     "@Url.Action("CurrentUser", "Account")",
  3.     "@Url.Action("Logout", "Account")",
  4.     null,
  5.     applicationModel.CurrentPage
  6.     );

 

The first argument is the link of the CurrentUser action method, the second one the link of the Logout method,  the third argument, if provided, is an onLogout callback while the fourth argument is a single observable or an array of observables that must be refreshed when the user changes. In our case we passed as fourth argument  our unique container of all virtual pages. In a more complex application that hosts several physical pages in the same physical page we might have passed an array containing several observables used as virtual page containers.

In all action methods that deploy templates/AMD only to logged users we must call the method to SPASecurity.ValidatedUser to validated the user received as parameter:

  1. public ActionResult People(string user, bool isJs, string ticket)
  2. {
  3.     string module = "People/Main";
  4.     if (!SPASecurity.ValidateUser(user, ticket)) return new HttpUnauthorizedResult();
  5.     if (isJs) return this.MinifiedJavascriptResult(module + "Js");
  6.     return PartialView(module, user+"/People");
  7. }

 

The SPASecurity.ValidatedUser method attempts validation with both the asp.net authorization cookie and with the ticket passed as argument.

That’s all for now!  A video showing the Data Moving Plug-in SPA View Engine is available here.

Francesco