Problem: Allowing a Model class to use AJAX.

Setup

You will need copies of the following files:

Motivation

Your copy of the file Model.ts should contain the following code:

enum ModelUrlType { Create = 0, Read = 1, Update = 2, Delete = 3 };
enum ModelStatus { Created, Read, Updated, Deleted, Dropped, AjaxError };

abstract class Model
{
    // Attributes
    private _data: Object;
    private _primaryKeyName: string = "id";
    private _primaryKeySet: boolean = false;
    private _urls: string[] = [];

    private static _ajaxSetup: boolean = true;

    // Constructor
    constructor(dataFields: any, primaryKeyName: string, urls: string[])
    {
        this._data = dataFields;
        this._primaryKeyName = primaryKeyName;
        this._urls = urls;
        Model.AjaxSetup();
    }

    // Properties
    get PrimaryKeyName(): string
    {
        return this._primaryKeyName;
    }

    get PrimaryKey(): number
    {
        return this.GetData(this._primaryKeyName);
    }

    set PrimaryKey(value: number)
    {
        this.SetData(this._primaryKeyName, value);
    }

    protected get PrimaryKeyNameValue(): Object
    {
        var priKey = {};
        priKey[this.PrimaryKeyName] = this.PrimaryKey;
        return priKey;
    }

    // Methods
    static AjaxSetup(): void
    {
        if (this._ajaxSetup)
        {
            $.ajaxSetup({ cache: false });  // May need to turn off browser page cache
            this._ajaxSetup = false;
        }
    }

    protected GetData(propName: string): any
    {
        if (this._data[propName] !== undefined) // Does field exist?
        {
            // If reading the primary key value and it hasn't been set, then return undefined.
            return (this._primaryKeySet || propName != this._primaryKeyName) ? this._data[propName] : undefined;
        }
        return undefined;
    }

    protected SetData(propName: string, value: any): boolean
    {
        if (this._data[propName] !== undefined || propName == this._primaryKeyName)
        {
            if (value !== undefined)
            {
                this._data[propName] = value;
                if (!this._primaryKeySet && propName == this._primaryKeyName) this._primaryKeySet = true;   // Note the primary key has been provided
                return true;
            }
        }
        return false;
    }

    protected Url(urlType: ModelUrlType, args?: Object): string
    {
        if (this._urls[urlType] !== undefined)
        {
            return this.UrlAppendPostfix(this._urls[urlType], args);
        }
        else
        {
            throw new Error("No postfix supplied for url type " + urlType);
        }
    }

    protected UrlAppendPostfix(postfix: string, args?: Object): string
    {
        var url: IUrl = Url.Create();
        url.Append(postfix);
        if (args)
        {
            url.AppendArgs(args);
        }
        return url.Url;
    }

    ToHTMLPairs(skipList?: string[]): string
    {
        return HTMLPairsConvertor.ConvertObjToPairsa(this._data, skipList);
    }

    abstract Create(): boolean;
    abstract Read(): boolean;
    abstract Update(): boolean;
    abstract Delete(): boolean;
} 
There are a few additions to this class. Firstly, two enumerator types are provided to indicate the type of operation being performed by the class. Line 1 is used to indicate the URL that should be used by an operation. Lines 24-44 are properties that simplify the use of the primary key, including a get/set property that maps to the GetData() method. Before we can use AJAX, we need to setup it up and an AjaxSetup() method does this for us (a static variable at line 12 ensures this only ever happens once). Two methods Url() and UrlAppendPostfix() are provided to manage the URL used by the AJAX calls.The class itself has become abstract (line 4) and a number of abstract methods are provided for the CRUD operations (lines 108-111). The implementaion of these operations will be provided by a child class AjaxifiedModel which you will be completing.

Usage

To allow us to extend the Model class to provide AJAX support, the class has been made abstract. From now on we will be required to inherit from the class before we can use it. We will create an AjaxifiedModel class that will write and read data from the server into the model. Again we will make this an abstract class because we want to specify that we are loading a specific type of model e.g. a Student model. For example, to create a Student model I might write:


interface IAjaxifiedCallback
{
    OnAjaxSuccess: (modelStatus: ModelStatus) => void;
}

class AjaxifiedStudentModel extends AjaxifiedModel
{
    // Attributes
    private _page: IAjaxifiedCallback;

    // Constructor
    constructor(page: IAjaxifiedCallback)
    {
        var urls: string[] = [];

		// Remember the page the Model is connect to
        // so that we can tell it that changes have
        // been made.
        this._page = page;

		// Provide urls for each operation
        // Use the ModelUrlType to provide a URL
        // for each action!!
        urls[ModelUrlType.Create] = "./AjaxServerSide/ModelCreateStudent";
        urls[ModelUrlType.Read] = "./AjaxServerSide/ModelReadStudent";
        urls[ModelUrlType.Update] = "./AjaxServerSide/ModelUpdateStudent";
        urls[ModelUrlType.Delete] = "./AjaxServerSide/ModelDeleteStudent";

        // Specify what the model fields are and their
        // types in an object passed to the parent
        // constructor
        super({
                "id": 0,        // Data fields of model
                "name": "",
                "age": 0
            },
            "id",               // Primary key
            urls);              // URLs for create, read, etc.
    }

    // Properties
    get Name(): string { return this.GetData("name"); }
    set Name(name: string) { this.SetData("name", name); }
    get Age(): number { return this.GetData("age"); }
    set Age(age: number) { this.SetData("age", age); }

    // Methods
    protected OnSuccess(modelStatus: ModelStatus): void
    {
        this._page.OnAjaxSuccess(modelStatus);
    }
}
This class inherits from AjaxifiedModel a class that you will be creating. The constructor is passed the Page object that needs to be notified when the models values change following an AJAX call. An array of URL strings is created in the constructor and the URLs for each action are also specified. The parent constructor (super()) is passed the data fields, the name of the primary key field and the URLs we just specified. A set of properties are also provided to map onto the GetData() and SetData() methods. Finally, the provided OnSuccess() method overrides the same method in the parent AjaxifiedModel class and is called when a successful AJAX response is received from the server. Currently, this just calls OnAjaxSucc() on the Page class passing it the model.

Assume we have a web page with approprite text boxes and buttons like this:


    <form id="student-form">
        <span id="student-id">
        <br /><input id="student-name" type="text" />
        <br /><input id="student-age" type="text" />
        <br /><input id="createbutton" type="button" value="Create" />
        <br /><input id="updatebutton" type="button" value="Update" />
        <br /><input id="deletebutton" type="button" value="Delete" />
        <br /><input id="readbutton" type="button" value="Read" />
    </form>

    <span id="student-status">

    <script src="Scripts/jquery-2.2.0.js">
    <script src="HTMLPairsConvertor.js">
    <script src="URL.js">
    <script src="Model.js">
    <script src="AjaxifiedModel.js">
    <script src="AjaxifiedStudentModel.js">
    <script src="ExampleModelPage.js">
We can add a page class with a PageLoad() method to add event handlers for each button. Each handler, reads the appropriate text boxes, sets the data the models data fields using SetData() and then call the appropriate model method e.g. model.Create() to create new database entry.
class ExampleModelPage implements IAjaxifiedCallback
{
    // Attributes
    private _model: AjaxifiedStudentModel;

    // Constructor

    // Properties

    // Methods
    PageLoad(): void
    {
        this._model = new AjaxifiedStudentModel(this);

        $("#student-id").hide();

        // Add event handlers to buttons
        // Use $.proxy() to keep the context i.e. 'this'
        // as this page when in click event handlers
        $("#createbutton").click($.proxy(this.CreateStudent, this));
        $("#updatebutton").click($.proxy(this.UpdateStudent, this));
        $("#deletebutton").click($.proxy(this.DeleteStudent, this));
        $("#readbutton").click($.proxy(this.ReadStudent, this));
    }

    // Button event handler
    private CreateStudent(e: JQueryEventObject): boolean
    {
        this._model.Name = $("#student-name").val();
        this._model.Age = $("#student-age").val();
        this._model.Create();
        e.preventDefault();
        return false;
    }

    private ReadStudent(e: JQueryEventObject): boolean
    {
        this._model.PrimaryKey = $("#student-search-id").val();
        this._model.Read();
        e.preventDefault();
        return false;
    }

    private UpdateStudent(e: JQueryEventObject): boolean
    {
        this._model.Name = $("#student-name").val();
        this._model.Age = $("#student-age").val();
        this._model.Update();
        e.preventDefault();
        return false;
    }

    private DeleteStudent(e: JQueryEventObject): boolean
    {
        this._model.Delete();
        e.preventDefault();
        return false;
    }

    // AJAX Callback
    // Maintain page as the current context for 'this'
    OnAjaxSuccess(modelStatus: ModelStatus): void 
    {
        switch (modelStatus)
        {
            case ModelStatus.Created:
                $("#student-id").show();
                $("#student-id").html(this._model.PrimaryKey + "");
                $("#student-status").html("created");
                break;
            case ModelStatus.Read:
                $("#student-id").show();
                $("#student-id").html(this._model.PrimaryKey+"");
                $("#student-name").val(this._model.Name);
                $("#student-age").val(this._model.Age);
                $("#student-status").html("read");
                break;
            case ModelStatus.Updated:
                $("#student-status").html("updated");
                break;
            case ModelStatus.Deleted:
                $("#student-id").hide();
                $("#student-id").html("");
                $("#student-name").val("");
                $("#student-age").val("");
                $("#student-status").html("deleted");
                break;
        }        
    }
}

// Before you can style a web page you have to wait until all the web tags
// have been loaded. In JavaScript you would add a function to the
// body page load event, in jQuery do it this way.
// jQuery page ready
$(function ()
{
    var page: ExampleModelPage = new ExampleModelPage();

    page.PageLoad();
});

Details

You must build the abstract AjaxifiedModel class that takes the provided URLs and performs the Read, Update and Delete actions. To help you, the code for the Create action has been provided. It must inherit from the Model class and will take the following form:

// The jQuery AJAX methods expect a callback that looks like this.
interface IModelAjaxSuccessCallback
{
    (data: any, status: string, request: JQueryXHR): void;
}

abstract class AjaxifiedModel extends Model
{
    // Attributes

    // Constructor
    constructor(dataFields: any, primaryKeyName: string, urls: string[])
    {
        super(dataFields, primaryKeyName, urls);
    }

    // Properties

    // Methods
    Create(): boolean
    {
        if (this.PrimaryKey === undefined)  // Can only create once!!
        {
        	// Get the URL we should be using for the Create action
        	var url: string = this.Url(ModelUrlType.Create);
            
            // Ignore primary key when creating pairs because we
            // don't have a primary key value when we create a
            // new database record for the new Model.
            var pairs: string = this.ToHTMLPairs([this.PrimaryKeyName]);	
            
            // Get the callback method to be used when AJAX returns
            // the result.
            var callback: IModelAjaxSuccessCallback = this.OnSuccessCallback(ModelStatus.Created);
            
            // Use HTTP POST Request to send string to server
            // Assumes the result returned contains the new primary key value.
            $.post(url, pairs, callback, "html");
            
            return true;
        }
        return false;
    }

    Read(): boolean
    {
        if (this.PrimaryKey !== undefined)      // Can only read if primary key is set
        {
            // Use Http GET Request and send no data.
            // Your code goes here
            return true;
        }
        return false;
    }

    Update(): boolean
    {
        if (this.PrimaryKey !== undefined)      // Can only update if primary key is set
        {
            // Use HTTP POST Request to send string to server
            // Assumes primary key in model is correct!!
            // Your code goes here
            return true;
        }
        return false;
    }

    Delete(): boolean
    {
        if (this.PrimaryKey !== undefined)      // Can only delete if primary key is set
        {
            // Use HTTP POST Request to send string to server
            // Your code goes here
            return true;
        }
        return false;
    }

	// The data we get back from the AJAX calls will be in HTML Pairs
    // format. This method recovers the name/value pairs. The names
    // must match those expected by the Model.
    protected ReadData(data: any, status: string, request: JQueryXHR, setPrimaryKey: boolean): void
    {
        // Assume data is returned in name/value pairs
        
        // Split the pairs up
        var pairs: string[] = data.split('&');
        var name: string;
        var value: string;

		// Split each pair into the name and value
        for (var p of pairs)
        {
            var pair: string[] = p.split('=');
            name = pair[0];
            value = pair[1];
            if (name != this.PrimaryKeyName || setPrimaryKey)
            {
                if (this.SetData(name, value) == false) throw new Error("Incorrect name [" + name + "] for data field returned by AJAX");
            }
        }
    }

    protected CheckForError(data: string): any
    {
        // Assumes errors are returned as a string as follows
        // "error:message"
        var error: string = data.trim();
        var n: number = error.search("error");  // Does the HTML returned contain the word "error"?
        if (n > -1)
        {
            var msg: string[] = error.split(':');
            return msg[1].trim();       // Return the error message
        }
        return false; // No error
    }

    protected OnSuccessCallback(modelStatus: ModelStatus): IModelAjaxSuccessCallback
    {
        var model: AjaxifiedModel = this;

        // Using closure to wrap AJAX success() callback
        // so we can keep the callback can use the modelStatus value
        return function (data: any, status: string, request: JQueryXHR): void
        {
        	// We only need to read the data received back from
            // AJAX if we are doing a Create or Read.
            if (modelStatus == ModelStatus.Created
            || modelStatus == ModelStatus.Read)
            {
                var error: any = model.CheckForError(data);
                if (error === false) model.ReadData(data, status, request, true);
            }
            model.OnSuccess(modelStatus);
        }
    }

	// Method to be provided by child class
    protected abstract OnSuccess(modelStatus: BasicModelStatus): void;
}   

Hints

  1. Create the appropriate URL, any pairs data required, provide a callback and call the appropriate jQuery AJAX function (POST or GET) for each action where "Your code goes here" is shown.
  2. If you are sending data to the server you should use a POST.