Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / IIS

Classic ASP MVC for dynamic JavaScript pages

4.78/5 (8 votes)
20 Mar 2014CPOL17 min read 44.8K   970  
Classic ASP reloaded.

Introduction

This topic is a "how to" create a fully dynamic JavaScript application intended for the Classic ASP programmers.

Background

There was an MVC pattern in the Classic ASP VBScript applications discussed in the previous topic. [^]

Now we are going apply MVC pattern to Classic ASP application and use JavaScript as a language for the Server-side and Client-side code.
The value of this topic is an experience of server-side MVC JavaScript Classic ASP and client-side MVC JavaScript applications for those who work with classic ASP IIS applications.

Using the code

  • Download and unzip the archive into the IIS virtual directory.
  • Set up the default page for the virtual directory (default.asp)
  • Set up the 404 error handler (router.asp)
  • Create a data model and generate your own Classic ASP JavaScript MVC application with Classic ASP code generator online.

Instructions for 404 handler and default page in pictures.

Server-side code

There is a router as an entry point for the MVC web application.

Router

  • parses the URL, calculates the Controller/Action
  • passes the execution to the Controller/Action with parameters

The image that shows the logic of custom Classic ASP router.

There will be some other functions in the router.asp (as it is a single entry point of an application) :

  • load required libraries
  • load Controllers
  • load Model
  • authenticate/authorize user
  • forms sanitation, etc.

You will need to capture 404 IIS error to enable nice URLs.
You can set up IIS error handler manually or using the script. Here is an instruction on how to set up 404 error handler to route nice formatted URLs through the custom Classic ASP router.(select "Architect" from the menu.)
Let's store the server-side router in the /Router.asp file.

Controller/Action

Usually there is a number of different Controllers in the application. Controllers are classes with business logic of application. Actions are the functions of these classes.

{an example of business logic of Controller/Actions}

<%
var SomethingController = {

    About: function(){
        var model = someCalculation();
        //render model in the view
        %>   <!--#include file="../Views/Something/About.asp" --> <%
    },
    //read operation
    Read: function(callback){
        var model = {data : {items: SomethingHelper.SelectAll()} };
        //send model as JSON data
        callback(JSON.stringify(model),"application/json") ;
    },
    //create operation
    Create: function(callback){
        var model = {data : new Something()};
        callback(JSON.stringify(model),"application/json") ;
    },
    //create operation - put the model into DB
    CreatePost: function(callback){
        var obj = getArguments();
        var result = createModelSomething(obj);
        var model = {result : result, data: obj };
        callback(JSON.stringify(model),"application/json") ;
    },
    //update operation - get the model from DB
    Edit: function (callback,vars){
        var model = {data: readModelSomething(vars)};
        callback(JSON.stringify(model),"application/json") ;
    },
    //update operation - put the model back to DB
    EditPost: function (callback){
        var obj = getArguments();
        var result = updateModelSomething(obj);
        var model = {result : result, data: obj };
        callback(JSON.stringify(model),"application/json") ;
    },
    //basic delete operation - get the model from DB
    Delete: function(callback,vars){
        var model = {data: updateModelSomething(vars)};
        callback(JSON.stringify(model),"application/json") ;
    },
    //basic delete operation - delete the object from DB
    DeletePost: function(callback){
        var obj = getArguments();
        var result = deleteModelSomething(obj);
        var model = {result : result, data: obj };
        callback(JSON.stringify(model),"application/json") ;
    }
};
//list of allowed controller/action
controllers['Something']=SomethingController;
 %>

In the MVC pattern Controller handles the User input, and manipulates the Model, and updates the View.
Early web MVC pattern took a thin client approach that placed the entire model, view and controller logic on the server.
In this approach, the client sends either "get" or "post" requests through the Router to the Controller and then receives a complete and updated web page.
This thin client approach will be used to display the whole page. For example, a default server-side HomeController.Index() will display the main page of the sample application.

Logic when server-side Controller,Action and View produce the whole page:
an image of codeflow when Controller,Action and View produce the whole page.

Once you may want to add more liveliness to your web-pages. The client-side JavaScript code will help. The client-side code would send AJAX requests to the server and update the DOM(document object model).

Then after several updates/features the client-side code becomes tangled, poor structured, hard-to-maintain. To resolve the problem, again, there was an effort to structure the client-side code.

As client-side technologies have matured , a number of client-side MVC frameworks have been created that allow the MVC components to execute partly on the client.
You may have heard about Backbone.js, Knockout, AngularJs and other.

For the purpose of this topic none of them will be used, but the bare MVC pattern will be applied to the Client-side JavaScript code.
The extensive client-side code affects the server-side code too: most of the server-side Controllers/Actions will return the data rather than the compiled HTML.
For the JavaScript code it is natural to use JSON to transfer data, so the most of the server-side controllers in the sample application will return JSON data to the client side. Server will not be merging Model with Views to produce HTML.

The following example shows the server-side Controller and Action produce the JSON string:
an image of codeflow when Controller,Action produce the JSON string.

Server-side Model

There is wide and narrow meaning of the term "Model" in the MVC pattern

The wide is the set of classes for CRUD(Create, Read, Update, Delete) operations over the objects in the database and the database itself.

The narrow meaning of the term "Model" is the data we send to the client at once. It can be an object serialized to JSON as in the example above, or false/true as a result of operation, or any other data that the server may display with the HTML template, or send to the client.

I will use the set of classes as the Data Model. These classes will have methods with SQL clauses to update/retrieve data from/to relational databases. Files of Data Model will be stored in the /Server/Model/(Name).asp files.

Server-side View

Although there are options now when selecting the template engine, I'm going to use Classic ASP templating for the purpose of this topic.
Classic ASP engine extracts and executes code between <% %> tags while parsing .ASP files.
I will use the term Server-side View for .asp pages with less logic and more content. The code-generator that I'm using here creates a single server-side view /Server/View/Home/Index.asp


Client-side code

When user requests the page from server browser sends the Request to the server with URL to the requested resource. It forms the protocol of interaction between user and server-side code.

With the client-side JavaScript code such interaction is usually built on events bound to the HTML controls, such as onclick Event [^]. Frameworks like JQuery may help set your events to appropriate client-side event handler.

However, for an applying of the MVC pattern there should be some common way used to capture the user interactions to transform them into the calls to the Router. For the purpose of this topic the client-side URLs will be used to signal about the user requests.

Client-side part of URLs.

The URLs contains the part that identify the server-side resource and the client-side resource. Client-side resource are often called "links with the same page" or "named anchors". For example the url

HTML
http://localhost/Home/Index/#User/List  

consist of server-side part:

HTML
http://localhost/Home/Index/ 

and client-side part:

HTML
#User/List. 

When user clicks a link with such URL the browser will request a new page if the server-side part is different than the current page location.
If the server-side part has not been changed browser will not request a page, but instead will try to find an anchor on the page and set the focus on it.

For the purpose of this topic we will modify this behavior. Jquery-hashchange-plugin will capture the change of the client-side part of URL and send them to the client-side Router.

{code to hook the changes to URL}

$(document).ready(function() {
        // Bind the event.
        $(window).hashchange(function() {
            route();
        })
    });

Router

The client-side Router will parse client-side part of URLs and pass the execution to the client-side Controller/Action.

{code of client-side Router}

    var controllers = {};

function route() {
    var defaultController = ""
    var defaultAction = ""
    var controller = null;
    var action = null;
    var vars = null;

    var globalpath = location.hash.split('#');
    if (globalpath.length < 2) return;
    //the path should be like:  /Controller/Action/var1/var2/var3
    var path = globalpath[1];

    //normalyze path
    if (path.charAt(0) != "/") path = "/" + path;

    var pathchuncks = path.split('/');
    if (pathchuncks.length) {
        if (pathchuncks.length >= 3) {
            controller = pathchuncks[1]; //suffix 'Controller' is not used on client
            action = pathchuncks[2];
        }
        if (pathchuncks.length > 3) {
            vars = pathchuncks.slice(3).join('/');
        }
        else  {vars='';}

    };

    if (!controller) {
        controller = defaultController;
    };
    if (!action) {
        action = defaultAction;
    };

    //alert(controller);
    //alert(action);
    if (controllers[controller]){
        if (controllers[controller][action]){
            var ControllerAction = controllers[controller][action];
            ControllerAction(null,vars);
        }
    }

}

When a controller is initialized it is added to the list of allowed controllers.

   var ProjectController = {
        List:       function(callback, args){ ... },
        Create:     function(callback, args){ ... },
        CreatePost: function(callback, args){ ... },
...
        Details:    function(callback, args){ ... }
   }

   //add to list of allowed controllers/actions
   controllers['Project']=ProjectController;

So in the line :

var ControllerAction = controllers['Project']['List'];

the variable ControllerAction takes the pointer to the ProjectController.List() function, which is called in the next row.

ControllerAction(null,vars);
The javascript object square bracket notation is used.

The client-side router is located in the /Client/router.js file

Client side Controllers/Actions

When user hit the link and the URL has changed, the "hashchange" event is fired and the client-side Router is called. Router parses URL. It calls the client-side Controller/Action.

Client side Controller/Action has an access to the DOM(document object model) and it can update the interface, but it should get the Model and View first to produce the HTML.

Thus there will be 2 calls to the server to get the Model and View. While Model is a JSON sent by the server-side Controller/Action, the View is just a file in the server's file system.

Client side Controller/Action does the following:

  • prepares the callback function to update the DOM when the Model and the View are ready.
  • calls the server-side controller/action and receive JSON data as Model via ModelProxy.
  • calls the server-side controller/action to retrieve the View via ViewProxy.
  • when callback is called, and both Model in View are ready, the client-side Controller/Action renders the Model in the View to produce the HTML - a part of of the page.
  • updates the part of the page with the new content.

An example of client-side controller ProjectController /action List

  var ProjectController = {
        List: function(callback, args){
            var view=null, model=null;

            //3. this is called via callback when the Model or View is ready.
            // thus it is called twice. When both Model and View are ready - then the final part:
            //locally render the model in the view when they are ready, and update the DOM
            var UpdateDOM = function(Model,View) {
                if (Model != null){ model = Model; }
                if (View != null){ view = View; }
                if ((model != null) && (view != null)) {
                    //render the Model into the Template
                    var templateOutput = Mustache.render(view, model);
                    //update the DOM:
                    $("#dialog1-body").empty();
                    $("#dialog1-body").append(templateOutput);
                    $("#dialog1-label").empty().append("list of Project");
                    $("#dialog1").modal("show");
                    NormalizeHash();
                }
            }

            //1. this is called first
            //call for the model, async
            ModelProxy(UpdateDOM, 'Project','List', args, 'get');

            //2. this is called second, simultaneously with the first call
            //get the view, async
            ViewProxy(UpdateDOM, 'Project','List');

        },

        //get the view and model and update the DOM
        Create: function(callback, args){
            var view=null, model=null;

            //locally render the model in the view when they are ready, and update the DOM
            var UpdateDOM = function(Model,View) {
                if (Model != null){ model = Model; }
                if (View != null){ view = View; }
                if ((model != null) && (view != null)) {
                    var templateOutput = Mustache.render(view, model);
                    $("#dialog2-body").empty();
                    $("#dialog2-body").append(templateOutput);
                    $("#dialog2-label").empty().append("");
                    $("#dialog2").modal("show");
                    NormalizeHash();
                }
            }

            //call for the model, async
            ModelProxy(UpdateDOM, 'Project','Create', args, 'get');

            //get the view, async
            ViewProxy(UpdateDOM, 'Project','Create');

        },

        //this is called from dynamic script from the /Client/Views/Project/Create.html
        CreatePost: function(callback, args){
            var view=null, model=null;

            //locally render the model in the view when they are ready, and update the DOM
            var UpdateDOM = function(Model,View) {
                if (Model != null){ model = Model; }
                if (View != null){ view = View; }

                //may show the form/components on the client here
                if ((model != null) && (view != null)) {
                    if (model.result == true) {
                        $("#dialog2-body").empty();
                        $("#dialog2").modal("hide");
                        NormalizeHash();
                        var templateOutput = Mustache.render(view, model);
                        //find the table Projectdatatable
                        //find the last row  and inset the row with attribute rowid='id'

                        if ($("#Projectdatatable")[0]!=undefined){
                           $("#Projectdatatable tr:last").after(templateOutput);
                           bindEvents();
                        }

                    }
                    if (model.result == false) {
                        var templateOutput = Mustache.render(view, { error: model.result });
                        $("#dialog2-label").empty().append(templateOutput);
                    }
                }
            }

            //call for the model, async
            ModelProxy(UpdateDOM, 'Project','CreatePost', args, 'post');

            //get the view, async
            ViewProxy(UpdateDOM, 'Project','CreatePost');

        },
...
}


      //add to list of allowed controllers/actions
      controllers['Project']=ProjectController;

Architecture overview

an image with overview of the architecture.

Client-side View (mustache templates)

Client-side controller receives the JSON string with data via ModelProxy . This data should be transformed into the HTML.

It is possible to parse JSON data to the HTML in the JavaScript. However, the better way is to use Templates and special library for templates.

There is a number of Template engines for JavaScript: dustjs, underscore.js, jade.js, mustache, etc. I'm going to use mustache for the purpose of this topic.

It is convenient to store the client-side Views in the /Client/Views/(Controller)/(Action).html

Views are stored in the server but they are transformed in the HTML in the Client code. That is why they are called Client-side Views.

After the Client-side Controller/Action produces the HTML it updates the DOM. For example it could be: display data received from server , display the status when post form to the server, add/remove rows in the tables, show/hide dialogues, menus, etc.

A set of client-side controllers will be in the files /Client/Controllers/(nameController).js

The code-generator I'm going to use creates one file per controller.

An example of client-side view /Views/Project/List.html

{{#data}}<div id='ProjectListContainer'>
<ul class="pagination pagination-sm  pull-right"><li><a class="btn" href='#/Project/About'>?</a></li></ul>
<ul class="pagination pagination-sm">
    <li><a id="create_button" href="#/Project/Create" class="btn"> </a>

    </li><li class="button_container" style="display: none;" > <a id="edit_button" class="btn"> </a>
    </li><li class="button_container" style="display: none;" ><a id="delete_button" class="btn"> </a>
    </li><li class="button_container" style="display: none;" ><a id="details_button" class="btn"> </a>
</li></ul>
{{#paginator}}
<div  >
 <ul class="pagination pagination-sm">
    <li><a href="#/Project/List/{{first}}"></a>
    {{#previous}}</li><li><a href="#/Project/List/{{previous}}"></a>{{/previous}}
       {{#links}}
          </li><li><a href="#/Project/List/{{n}}">{{n}}</a>
        {{/links}}
    {{#next}}</li><li><a href="#/Project/List/{{next}}"></a>{{/next}}
    </li><li><a href="#/Project/List/{{last}}"></a>
 </li></ul>
</div>
 {{/paginator}}



<table id="Projectdatatable" class="table table-striped"><thead><tr><td>ProjectName</td><td>POP3Address</td><td>Active</td><td></td></tr></thead><tbody>{{#items}}
  <tr rowid='{{id}}'><td>{{ProjectName}}</td><td>{{POP3Address}}</td><td>{{Active}}</td><td></td></tr>{{/items}}
  </tbody></table>
{{^items}}
<div>No records</div>
{{/items}}
<script type="text/javascript">
    ProjectListModule = (function() {
        // private
        var onSelectorClick = function(_selector) {
            if ($(_selector).hasClass('active')) {
                $(_selector).removeClass('active');
                disableButtons();
            }
            else {
                $(_selector).addClass('active').siblings().removeClass('active');
                $(_selector).find("tr").addClass('active');
                var rID = $(_selector).attr('rowid');
                enableButtons(rID);
            }
        };

        var enableButtons = function(id) {
            $("div#ProjectListContainer li.button_container").fadeIn(100);
            $("div#ProjectListContainer #edit_button")[0].href = '#/Project/Edit/' + String(id);
            $("div#ProjectListContainer #delete_button")[0].href = '#/Project/Delete/' + String(id);
            $("div#ProjectListContainer #details_button")[0].href = '#/Project/Details/' + String(id);
        };

        var disableButtons = function() {
            $("div#ProjectListContainer li.button_container").fadeOut(100);
            $("div#ProjectListContainer #edit_button")[0].href = '#';
            $("div#ProjectListContainer #delete_button")[0].href = '#';
            $("div#ProjectListContainer #details_button")[0].href = '#';
        };

        var bindEvents = function() {
            $("div#ProjectListContainer  #Projectdatatable > tbody tr").each(function() {
                $(this).unbind();
                $(this).bind('click', function() {
                    onSelectorClick(this);
                });
            });
        };
        //public
        return {bindEvents : bindEvents ,
                disableButtons : disableButtons
        }//end public members
    })();  //end ProjectListModule
$(document).ready(function() {
    ProjectListModule.bindEvents();
});

</script>
</div>{{/data}}

These templates work similar to web applets. They may contain HTML as well as supporting client-side JavaScript code. Both are loaded dynamically when the generated HTML is added to DOM. The templates work even better with caching as it discussed later.

As you can see the part of client-side logic is stored out of the Controllers/Actions. The architecture has suffered, but the coding is easier.

Other considerations

A typical server-based MVC application has very simple interface between user and server code: there is just a single get or post HTTP request/response.

Browser requests, Server sends, and then browser loads the whole page, then the user clicks the link or submit the form.

However there is no such simplicity in an application with massive client-side code . One page may get and post data to/from a number of server-side URLs, without refreshing the whole page.

An application with massive client-side code and built with the MVC pattern is often called a "single-page application". It does not mean that the application should have exactly one page. It means that the point of view (or your focus of attention) should be shifted to the page that you are working on.

You should be looking for resources for your page. Development becomes very page-centric. When on the page the focus is on the questions like:

  • where should the page call to get a form for user input
  • or where should the page get a data for a list
  • or where should a form send an input from the user, etc.

The page becomes very much like Microsoft's WebForms with it's call-backs. However the big difference is that here the events are driven to multiple server-side controllers/actions, that the requests are sent from the client-side code (without reloading the page), that the updates to the interface happened in the client-side code.

Let's look how this system may handle standard tasks like: getting, displaying and sending Forms, loading and displaying lists and grids, loading heavy bootstrap carousel sliders, etc.

Forms

Forms are shown as bootstrap modal dialogs on page. The code loads Model and Template from the server, produce the HTML form and update DOM: insert the HTML fragment with the form into the dialog.

A sample of code to display the Edit form (/Client/Controllers/Project.js)

//get the view and model and update the DOM
Edit: function(callback, args){
    var view=null, model=null;

    var UpdateDOM = function(Model,View) {
        if (Model != null){ model = Model; }
        if (View != null) { view = View; }
        if ((model != null) && (view != null)) {
            var templateOutput = Mustache.render(view, model);
            $("#dialog2-body").empty();
            $("#dialog2-body").append(templateOutput);
            $("#dialog2-label").empty().append("Update Project");
            $("#dialog2").modal("show");

            NormalizeHash();
        }
    }

    //call for the model, async
    ModelProxy(UpdateDOM, 'Project','Edit', args, 'get');

    //get the view, async
    ViewProxy(UpdateDOM, 'Project','Edit');
}

The Client-side code validates a form and send it to the server with POST request. Posting the forms happened from the separate client-side POST Controller/Action.

A sample of code that handles the "Edit" form values (/Client/Views/Project/Edit.html)

var SubmittingProjectEditForm = function() {
    //alert("The form has been validated. Now send the form...");
    //make an AJAX call
    var args = {id: $('#ProjectEditContainer #Projectid').val(),
        ProjectName: $('#ProjectEditContainer #ProjectName').val() ,
        POP3Address: $('#ProjectEditContainer #POP3Address').val() ,
        Active:($('#ProjectEditContainer #Active').is(':checked')) ? "on" : ""
    };
    //call Controller/Action eaither via controllers or directly
    //controllers['Project']['EditPost'](null, args);
    ProjectController.EditPost(null, args);
};

Server-side controller sends result of POST form processing. The result is available in the client-side code with data or status from server-side.

A sample of code that posts the "Edit" form values to the server (/Client/Controllers/Project.js)

var ProjectController = {
    EditPost: function(callback, args){
        //locally render the model in the view when they are ready, and update the DOM
        var UpdateDOM = function(Model,View) {
            show results of post request here
        };
        //call for the model, async
        ModelProxy(UpdateDOM, 'Project','EditPost', args, 'post');
    }
}
Lists(Grids)

When you need to display list, call the appropriate Controller/Action, for example /ProjectController/List

Client-side controller (ProjectController.List()) will call the server-side controller/action and get the data for List(Model). In the same time it will load the appropriate view - a mustache template (/Client/Views/Project/List.html).

Then the client-side Controller/Action will update the DOM: display the rendered List in the selected element of the Page.

Heavy bootstrap carousel sliders, menu, language resources

There is a great benefit from dynamic applications - the ability to load the lightweight page in the beginning, and then later load and update heavy components. Bootstrap slider may have a lot of images inside. It may take a while to load the page if you embed heavy images into the page. You may load the carousel slider with one or two items at the beginning, and then load the rest of them in the following way:

On the "page load" event call the client-side Controller/Action. It will call the server-side controller/action and get the data for Carousel(Model).

In the same time it will load the appropriate View: a mustache template to render the bootstrap carousel.

Then the client-side Controller/Action will update the DOM: display the rendered carousel items in the selected element of the Page.

I'm using this background page update class to display menu and model in the code-generator.

Client-side caching

You can implement caching for Models and Templates on the client to enable the offline application use. There are Web SQL , IndexedDB and localStorage available in the modern browsers. ModelProxy and ViewProxy are places to fix for Models and Views caching

Client-side View caching

It is an obvious idea to cache Views on the client if cache is supported. The benefits are: decreasing the network traffic, lower latency when call local controller/action.

{the ViewProxy without caching}

function ViewProxy (callback, controller, action) {

    //Load the view
    $.get('Client/Views/' + controller + '/' + action + '.html', {}, function(view) {
         callback(null,view);
        //may put the template into the client's local cache;
    }).fail(function() {
        //alert("error");
        //failed to load the view, try render with empty one
        var view = "";
        callback(null,view);
    });
};

On the screenshot when using ViewProxy without caching you could see that 2 queries are made every time when user clicks the (Project) link. Note that some of them are cached by browser.

{the ViewProxy with caching}

function ViewProxy(callback, controller, action) {
    //may load template from/to cache: either Web SQL , IndexedDB or localStorage

    var cache = new ViewCache(controller, action);
    if (!cache.read(callback)) {
        //Load the view
        $.get(baseurl + 'Client/Views/' + controller + '/' + action + '.html', {}, function(view) {
            //Template = Mustache.compile(view);
            callback(null, view);
            //may put the template into the client's local cache;
            cache.write(view);

        }).fail(function() {
            //alert("error");
            //failed to load the view, try render with empty one
            var view = "";
            callback(null, view);
        });
    }
};


//
// This defines the cache
//
ViewCache = function(controller, action, data) {
    var enabled = true;
    var Controller = String(controller);
    var Action = String(action);
    var Data = null;
    var appName = "test";
    if (data)
        Data = String(data);
    var storagename = appName+ "/Views/" + Controller + "/" + Action;
    return {
        write: function(data) {
            //if (!enabled) return false;  //write anyway
            if (data)
                Data = String(data);
            if (typeof (Storage) !== "undefined") {
                // Yes! localStorage and sessionStorage support!
                localStorage[storagename] = Data;
                return true;
            }
            else {
                // Sorry! No web storage support..
                return false;
            }
        },
        read: function(callback) {
            if (!enabled) return false;
            if (typeof (Storage) !== "undefined") {
                // Yes! localStorage and sessionStorage support!
                Data = localStorage[storagename];
                if (Data) {
                    if (Data.length > 1) {
                        callback(null, Data);
                        return true;
                    }
                }
                return false; //read was not successful
            }
            else {
                // Sorry! No web storage support..
                return false;
            }
        },
        clear: function(callback) {
            if (!enabled) return false;
            if (typeof (Storage) !== "undefined") {
                // Yes! localStorage and sessionStorage support!
                //localStorage.clear();
                //clear removes all cache values for domain. for subdomains remove values via iteration.
                for (var key in localStorage) {
                    if (key.substring(0, appName.length) === appName)
                        localStorage.removeItem(key);
                }
                return true;
            }
            else {
                // Sorry! No web storage support..
                return false;
            }
        }
    }//end public
}//end ViewCache

On the screenshot when ViewProxy use caching you could see that 1 query is made every time when user clicks the (Project) link.

Model (data) caching

While the reasons for View caching are obvious, they are not so evident for data caching.

  • why you may need it

    You may need data caching to store often-used-but-rarely-updated data. It could be a menu or dropdown values or some data rendered to the interface, e.g. language resources, etc. For the purpose of this topic the caching will be applied to the dropdowns.

  • why you may not need it

    Although there are some benefits from data caching, there are reasons to do not use it:

    • some actions requires alive (unconditionally non-cached data). This includes Create, Update,Delete operations.
    • the actions mentioned above change the data, thus the cache invalidation is required.
    • in multi-user systems cache invalidation will not help: someone else may change the database while local cache still remain marked as valid.
  • where it could lead you

    If you implement the local data caching it could lead you later to the following things:

    • Data caching for the CRUD operation may give you the power of offline/online applications. Your client-side code could read, update, delete data in the cache while offline and sync changes with server when get back online.
    • You could replace the data caching with client-side storage. This could give you a simple way to build hybrid mobile applications.

{code of ModelProxy without caching}

function ModelProxy(callback, controller, action, vars, method) {
    //var Callback = callback;
    //could load the model from the local storage: either Web SQL , IndexedDB or localStorage

    //Load model
    if (!method) method = 'get';
    method = method.toLowerCase();
    if (method == 'post') {
        $.post( baseurl + controller + '/' + action + '/', vars, function(model, status) {
            callback(model,null);
        });
    }
    else if (method == 'get') {
        //this string is for MVC sites
        $.get( baseurl + controller + '/' + action + '/' + vars, {}, function(model, status) {
            callback(model,null);
        });
    }
};

{code of ModelProxy with caching (only DropDowns are cached)}

//ModelProxy extracts the data from cache or from server(with the call to server-side Controller and Action).
function ModelProxy(callback, controller, action, vars, method) {
    //could load the model from the local storage: either Web SQL , IndexedDB or localStorage
    var cache = new ModelCache(controller, action);

    if (cache.cacherequired) {
        if (cache.read(callback)) {
            //successful cache read, value is passed in the callback function
            return;
        }
    }

    //Load model
    if (!method) method = 'get';
    method = method.toLowerCase();
    if (method == 'post') {
        $.post(baseurl + controller + '/' + action + '/', vars, function(model, status) {
            callback(model, null);
            if (cache.cacherequired) { cache.write(model); }
        });
    }
    else if (method == 'get') {
        //this string is for MVC server-side code
        $.get(baseurl + controller + '/' + action + '/' + vars, {}, function(model, status) {
            callback(model, null);
            if (cache.cacherequired) { cache.write(model); }
        });
    }
};

//
// This defines the cache
// it reads/writes the data to localStorage
// can invalidate cache either internally or via call
ModelCache = function(controller, action, data) {
    //private
    var enabled = true;
    var Controller = String(controller);
    var Action = String(action);
    var Data = null;
    var appName = "test";
    if (data) { Data = JSON.stringify(data); }
    var storagename = appName + "/Model/" + Controller + "/" + Action;

    //this defines which Actions to cache. usually this is a heavy dropdowns
    var cachedActions = new Array("DropDown");

    //this defines which Actions to cause the cache invalidation. Usually this is a data change actions
    var dataChangeActions = new Array("CreatePost", "EditPost", "DeletePost");

    var invalidate = function() {
        if (typeof (Storage) !== "undefined") {
            // Yes! localStorage and sessionStorage support!
            var iAction;
            for (iAction in cachedActions) {
                localStorage.removeItem(appName + "/Model/" + Controller + "/" + cachedActions[iAction]);
            }
        }
    };

    var invalidationrequired = (dataChangeActions.indexOf(action) >= 0);
    if (invalidationrequired) {
        invalidate();
    }
    var cacherequired = (cachedActions.indexOf(action) >= 0);

    //public
    return {
        write: function(data) {
            //if (!enabled) return false;  //write anyway
            if (data)
                Data = JSON.stringify(data);
            if (typeof (Storage) !== "undefined") {
                // Yes! localStorage and sessionStorage support!
                localStorage[storagename] = Data;
                return true;
            }
            else {
                // Sorry! No web storage support..
                return false;
            }
        },
        read: function(callback) {
            if (!enabled) return false;
            if (typeof (Storage) !== "undefined") {
                // Yes! localStorage and sessionStorage support!
                Data = localStorage[storagename];
                if (Data) {
                    if (Data.length > 1) {
                        Data = JSON.parse(Data);
                        callback(Data, null);
                        return true;
                    }
                }
                return false; //read was not successful
            }
            else {
                // Sorry! No web storage support..
                return false;
            }
        },
        // a reference to internal function
        invalidate: invalidate,
        cacherequired: cacherequired,
        invalidationrequired: invalidationrequired
    }//end public
}//end ModelCache
code assembly and placement. general

JavaScript is an interpreted language. We need to store the code in separate files. There is a number of ways to keep the code modules together as a system.

  • When doing a spaghetti code there are lot of global variables reused. You just can't join files together; they won't work. With MVC pattern you could simply join all script code files together starting from router in the order of inclusion. If it still works then you could compress/minify the code using one of the free available services(e.g. http://jscompress.com/). As a bonus the minification provides the natural obfuscation.
    Both Server-side and client-side code could be compressed.
  • Keep the code together with built-in code-linkers
server-side code linking

Classic ASP uses #include directives to link in modules. The file Router.asp contains directives to link Controllers, Model files, Lib files. These 3 work as modules and include corresponding custom files.

The Classic ASP Server.Execute() command could be used to include code modules.

There is another less known option for including sever-side modules: a standard <script> tag with an option runat="server".


<script language="JScript"  src="Controller/controllers.inc" runat="server"></script>
<script language="JScript" src="Model/lib.inc"  runat="server"></script>
<script language="JScript" src="lib/utils.inc"  runat="server"></script>

This last option comes from MS Scripting Engine. Including files in this way allows the code in the different scripting language, e.g. VBScript:

<script language="VBScript" src="lib/VBModule.inc" runat="server"></script>

However you should carefully verify how your IIS processes file extensions of your modules. If you leave modules in the ".vbs" or ".js" files, and if someone directly requests these files from server, they will be sent to the client as is. This will expose the server-side logic and constants you may have in the code. The extension ".inc" should be safe in IIS default settings.

If you are in doubts regarding accessing the server-side code then you can relocate an entire server-side code out of IIS folder structure. Since the pattern we have used here assumes the single entry-point of the application, there can be a single #include directive in the single file in the IIS application folder.


client-side code linking

There are 2 general ways to link JavaScript on the client:

  • Link scripts with a standard <script> tag
  • Link scripts dynamically loading them into the code: load scripts with an Ajax and then call them with use of eval().

The first option is OK when JavaScript code files are not too large, otherwise it may take a time to load them all. If scripts are large unfortunately there is another problem with standard <script> tag: browsers do not load scripts and other resources in parallel, so it takes even longer lo display the page for the first time.

The second option is easy with use of jQuery.getScript(), RequireJS, labjs, etc. libraries. However there is a couple of problems you may encounter using them:

  • There may be an interface glitch when the DOM is loaded but supporting JavaScript libraries are still not loaded.
  • There may be a broken sequence of events: Suppose you have the module, which is loaded from Require.js. jQuery document.ready() may not call a function defined in the module, as it is still loading when the event is fired

Debugging

You can debug the server-side application using MS Visual Studio. You will need:

Advantages and disadvantages

  • The similar dialects of the same JavaScript are used both on the server and client. Less context switching due to the similar versions of the language for both client and server.
  • The similar implementation of the same MVC pattern is used on the server and client. Less context switching due to the similar versions of the language for both client and server .
  • The code is portable and relocatable between client and server with the minimal modifications.
  • Availability of JavaScrip libraries from the most unexpected sources when doing the server-side JavaScript programming.
  • It is IIS. You can bring your VBScript code and mix it with server side. Don't forget about the ActiveX extensions on Windows through Server.CreateObject() and ASP.NET pages
  • Scripts are loaded and unloaded for every server call, thus it is truly stateless, but should be slower than all-in-memory systems
  • Classic ASP does not support asynchronous code execution.
  • Classic ASP is available on a number of MS operating systems, starting from XP.

Points of Interest

  • The MVC pattern applied to Classic ASP Server-side JavaScript code
  • The MVC pattern applied to Client-side JavaScript code
  • The code-generator is created
  • The client-side caching and templating has been applied
Classic ASP reloaded.

History

  • Mobile application for Intel XDK and classic ASP/IIS.
  • Using the code generator to create the node.js based application.
  • Application for Chrome and Classic ASP/IIS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)