6. Make it data-driven

Description

This example takes the previous sample and makes it data driven and adds a way for the user to define new DataSource types. It will also be extended to define a new DataSource and change the user interface to allow the user to switch between the two DataSources

Prerequisites

As this sample builds on the previous one, in order to be able to run it, please esnure you have the latest build of SmartClient (at least version 8.2p). This can be downloaded from here.

Changing the DataSource properties

As the DataSource will work with any type of class, instead of the hard-coded supplyItem class, we need a way to specify which class the DataSource should use.Add a new property to the DataSource definition, called beanClassName which should contain the assembly qualified class name of the class which should be used. For supplyItem, this assembly qualified class name is 'App6.Dao.supplyItem,App6':

isc.RestDataSource.create({
    ID: "supplyItem",
    fields: [
		// field definition here
	],

	dataFormat: "json",
	jsonPrefix: "//'\"]]>>isc_JSONResponseStart>>",
	jsonSuffix: '//isc_JSONResponseEnd',

	beanClassName: 'App6.Dao.supplyItem,App6',

	operationBindings: [
		// operation  binding definition here
	]
});

Also, add this property to the DataSource class where the JSON representation of the DataSource is being deserialized, to:

	public class DataSource
    {
        public string ID { get; set; }
        public string dataFormat { get; set; }

        public string beanClassName { get; set; }

		public ICollection<Dictionary<string, object>> fields { get; set; }
        public ICollection<Dictionary<string, object>> operationBindings { get; set; }

        // rest of the DataSource definition
    }

Assembly qualified class name is required. For instantiating a class which could be in another assembly, both the class name and the assembly file where the class was defined are needed, so this beanClassName property will have the '<namespace>.<class name>,<assembly file>' format.

In the previous articles, in many places the primary key is used (to get the record from the database). However,  to make it generic, this cannot be hard-coded. It is possible to retrieve it from the DataSource definition. To do this, add a method to the DataSource which will search in the loaded fields and return the one which has a primaryKey attribute set to true:

public Dictionary<string, object> getPrimaryKey()
{
    foreach (Dictionary<string, object> field in fields)
    {
        if (field["primaryKey"] != null && field["primaryKey"].Equals("true"))
        {
            return field;
        }
    }

    return null;
}

Changes to the DataSource request processing methods

The code of the previous article up to this point is built in a such way that the entire execution path for request processing makes no use of the supplyItem class except for the methods for processing the requests, defined in the DataSource class itself (there are couple of methods however, defined in RPCManager which should know the type, however they were defined generically, and they're called from DataSource's methods, where class to use is already known). Therefore, a method is required to call these generic methods without hard-coding the generic type parameter. For this a helper class has previously been defined, called ReflectionHelper which provides a number of methods for invoking a generic method or standard method by it's name, getting and setting properties by name and instantiating classes. Below is the specific section of this Helper specifically relating to calling generic methods.

public static object InvokeGeneric(object target, string sMethod, Type type)
{
    return InvokeGeneric(target, sMethod, new Type[] {
        type
    }, null);
}

public static object InvokeGeneric(object target, string sMethod, Type type,
                                   object parameter)
{
    return InvokeGeneric(target, sMethod, new Type[] { type }, new object[] { parameter });
}

public static object InvokeGeneric(object target, string sMethod, Type type,
                                   object[] parameters)
{
    return InvokeGeneric(target, sMethod, new Type[] { type }, parameters);
}

public static object InvokeGeneric(object target, string sMethod, Type[] type)
{
    return InvokeGeneric(target, sMethod, type, null);
}

public static object InvokeGeneric(object target, string sMethod, Type[] type,
                                   object parameter)
{
    return InvokeGeneric(target, sMethod, type, new object[] { parameter });
}

public static object InvokeGeneric(object target, string sMethod, Type[] type,
                                   object[] parameters)
{
    MethodInfo method = target.GetType().GetMethod(sMethod);

    if (method == null)
    {
        System.Diagnostics.Debug.WriteLine("Method not found {0} in class {1}",
                           sMethod, target.GetType().Name);

        return null;
    }

    MethodInfo methodGeneric = method.MakeGenericMethod(
        type);

    if (methodGeneric == null)
    {
        System.Diagnostics.Debug.WriteLine("Cannot convert method {0} in class {1} to required generic method",
                           sMethod, target.GetType().Name);

        return null;
    }

    return methodGeneric.Invoke(target, parameters);
}

As can be seen,  the types for which the generic method needs to be called and the method parameters (if required) are provided as a parameter (either single or in an array, if the generic method has more than one of these).

Using this helper class, refactor the methods in the DataSource for removing the hard-coded supplyItem.

The Add operation

Firstly, get the type specified in the beanClassName property and use this type and the ReflectionHelper class, to invoke the convertRequest method which converts DSRequest<Dictionary<string, object>> to DSRequest<beanClassName>:

virtual public DSResponse executeAdd<T>(DSRequest<T> request)
{
    Type beanType = Type.GetType(beanClassName);

    if (beanType == null)
    {
        return null;
    }

    var req = ReflectionHelper.InvokeGeneric(request.RpcManager, "convertRequest", beanType, request);
    var data = ReflectionHelper.GetProperty(req, "data");

    request.RpcManager.Session.Save(ReflectionHelper.GetProperty(req, "data"));
    request.RpcManager.Session.Flush();

    DSResponse dsresponse = new DSResponse();

    dsresponse.data = data;
    dsresponse.status = 0;

    return dsresponse;
}

As the the type of the bean class is unknown,  ReflectionHelper is used to get the data stored in the request, converted to the bean class for later when it will be set in the response that is sent back. As historically, the RPCManager was called to convert the DSRequest to a DSRequest with it's data property typed to the bean class, it also makes sense to save it directly as an NHibernate entity.

The Remove operation

The executeRemove is almost identical to the executeAdd method:

virtual public DSResponse executeRemove<T>(DSRequest<T> request)
{
    Type beanType = Type.GetType(beanClassName);

    if (beanType == null)
    {
        return null;
    }

    var req = ReflectionHelper.InvokeGeneric(request.RpcManager, "convertRequest", beanType, request);
    var data = ReflectionHelper.GetProperty(req, "data");

    var itm = request.RpcManager.Session.Get(beanType,ReflectionHelper.GetProperty(data, GetPrimaryKey()));

    request.RpcManager.Session.Delete(itm);
    request.RpcManager.Session.Flush();

    DSResponse dsresponse = new DSResponse();

    dsresponse.data = data;
    dsresponse.status = 0;

    return dsresponse;
}

You will notice that instead of the hard-coded primary key value it is now using the getPrimaryKey() method introduced earlier.

The Update operation

The executeUpdate method is the same as the one in the previous article, adapted to use the ReflectionHelper class and beanClassName property to remove it's dependency to supplyItem:

virtual public DSResponse executeUpdate<T>(DSRequest<T> request)
{
    DSRequest<Dictionary<string, object>> reqmap = request as DSRequest<Dictionary<string, object>>;
    Type beanType = Type.GetType(beanClassName);

    if (beanType == null)
    {
        return null;
    }

    var req = ReflectionHelper.InvokeGeneric(request.RpcManager, "convertRequest", beanType, request);
    var reqdata = ReflectionHelper.GetProperty(req, "data");

    var itm = request.RpcManager.Session.Get(beanType, ReflectionHelper.GetProperty(reqdata, GetPrimaryKey()));

    // update all fields which have changed. they are defined in the data property
    // build the criteria
    if (reqmap.data.Keys.Count != 0)
    {
        foreach (string key in reqmap.data.Keys)
        {
            if (getField(key) != null)
            {
                object val = ReflectionHelper.GetProperty(reqdata, key);
                ReflectionHelper.SetProperty(itm, key, val);
            }
        }
    }

    request.RpcManager.Session.Update(itm);
    request.RpcManager.Session.Flush();

    // create a result object to be returned
    // and copy all properties of the updated object into this one
    var data = ReflectionHelper.CreateInstance(beanClassName);

    if (reqmap.oldValues.Keys.Count != 0)
    {
        foreach (string key in reqmap.oldValues.Keys)
        {
            if (getField(key) != null)
            {
                object val = ReflectionHelper.GetProperty(itm, key);
                ReflectionHelper.SetProperty(data, key, val);
            }
        }
    }

    // create the DSResponse object
    DSResponse dsresponse = new DSResponse();
    dsresponse.data = data;
    dsresponse.status = 0;

    return dsresponse;
}

This approach gets the type defined in the beanClassName property and uses it to convert the request appropriately (as in the previous methods). For setting and getting properties it uses the ReflectionHelper's methods. Another difference from the previous sample's code is that instead of creating a supplyItem, setting it's properties and returning it back, this is creating an instance of the bean class using ReflectionHelper, to remove the hard-coded supplyItem reference.

The Fetch operation

Then, the fetch method, contains mostly the same changes in the previous methods, but focused towards removing the supplyItem dependency:

virtual public DSResponse executeFetch<T>(DSRequest<T> request)
{

    DSRequest<Dictionary<string, object>> req = request as DSRequest<Dictionary<string, object>>;

    Type beanType = Type.GetType(beanClassName);

    if (beanType == null)
    {
        return null;
    }

    var itmreq = ReflectionHelper.InvokeGeneric(request.RpcManager, "convertRequest", beanType, req);

    var query = request.RpcManager.Session.CreateCriteria(beanType);

    if (req.advancedCriteria == null)
    {
        buildStandardCriteria(itmreq, req, ref query);
    }
    else
    {
        buildAdvancedCriteria(req, ref query);
    }

    // build the criteria
    if (req.data.Keys.Count != 0)
    {
        foreach (string key in req.data.Keys)
        {
            Dictionary<string, object> field = getField(key);

            // make sure the field is in the DataSource
            if (field != null)
            {
                object val = ReflectionHelper.GetProperty(itmreq, key);

                string type = field["type"] as string;

                if (type.Equals("text") || type.Equals("link") || type.Equals("enum") ||
                    type.Equals("image") || type.Equals("ntext"))
                {
                    query.Add(Restrictions.Like(key, "%" + val + "%"));
                    break;
                }
                else
                {
                    query.Add(Restrictions.Eq(key, val));
                    break;
                }
            }
        }
    }

    // add sorting
    if (req.sortBy != null)
    {
        // add the sorting
        foreach (string column in req.sortBy)
        {
            // if column name is with -, then ordering is descending, otherwise ascending
            if (column.StartsWith("-"))
            {
                // if sort is descending, then we have to remove the '-' from the beginning to get te correct
                // column name
                query.AddOrder(Order.Desc(column.Substring(1)));
            }
            else
            {
                query.AddOrder(Order.Asc(column));
            }
        }
    }

    // create a response object
    DSResponse dsresponse = new DSResponse();

    // set start row and number of rows on the query itself
    if (req.endRow != 0)
    {
        query.SetMaxResults(req.endRow - req.startRow);
        query.SetFirstResult(req.startRow);
    }

    // get the requested number of objects
    var products = query.List();

    // change projection to get the total number of rows
    // we need to clear the result range and the ordering for this to work
    query.SetProjection(Projections.RowCount());
    query.SetFirstResult(0);
    query.SetMaxResults(int.MaxValue);
    query.ClearOrders();

    // set total rows using the projection
    dsresponse.totalRows = (int)query.UniqueResult<int>();

    // set the response data
    dsresponse.data = products;
    dsresponse.startRow = req.startRow;
    dsresponse.endRow = dsresponse.startRow + (ReflectionHelper.GetProperty(products, "Count") as int?).Value ;

    // sanity check, if no rows, return 0
    if (dsresponse.endRow < 0)
    {
        dsresponse.endRow = 0;
    }

    dsresponse.status = 0;

    return dsresponse;
}

and finally,  a small change in the buildStandardCriteria method, where the property handling is replaced with the use of ReflectionHelper.

void buildStandardCriteria(object req, DSRequest<Dictionary<string, object>> reqmap, ref ICriteria query)
{
    // build the criteria
    if (reqmap.data.Keys.Count != 0)
    {
        foreach (string key in reqmap.data.Keys)
        {
            Dictionary<string, object> field = getField(key);

            // make sure the field is in the DataSource
            if (field != null)
            {
                // get the property value
                object data = ReflectionHelper.GetProperty(req, "data");
                object val = ReflectionHelper.GetProperty(data, key);

                string type = field["type"] as string;

                if (type.Equals("text") || type.Equals("link") || type.Equals("enum") ||
                    type.Equals("image") || type.Equals("ntext"))
                {
                    query.Add(Restrictions.Like(key, "%" + val + "%"));
                    break;
                }
                else
                {
                    query.Add(Restrictions.Eq(key, val));
                    break;
                }
            }
        }
    }
}

Adding a new DataSource

Tto test this new code,  define a new DataSource instance for employee and later allow the user to switch between them. Firslyt, the definition of the DataSource itself:

isc.RestDataSource.create({
    ID: "employees",
    fields: [
        {name:"Name", title:"Name", type:"text", length:"128"},
        {name:"EmployeeId", title:"Employee ID", type:"integer", primaryKey:"true", required:"true"},
        {name:"ReportsTo", title:"Manager", type:"integer", required:"true", foreignKey:"employees.EmployeeId", rootValue:"1", detail:"true"},
        {name:"Job", title:"Title", type:"text", length:"128"},
        {name:"Email", title:"Email", type:"text", length:"128"},
        {name:"EmployeeType", title:"Employee Type",type:"text", length:"40"},
        {name:"EmployeeStatus", title:"Status", type:"text", length:"40"},
        {name:"Salary", title:"Salary", type:"float"},
        {name:"OrgUnit", title:"Org Unit", type:"text", length:"128"},
        {name:"Gender", title:"Gender", type:"text", length:"7",
            valueMap: [ "male", "female" ]
        },
        {name:"MaritalStatus", title:"Marital Status", type:"text", length:"10",
            valueMap: [ "married", "single" ]
        },
    ],

    dataFormat: "json",
    jsonPrefix: "//'\"]]>>isc_JSONResponseStart>>",
    jsonSuffix: '//isc_JSONResponseEnd',

    beanClassName: 'App6.Dao.Employee,App6',

    operationBindings: [
            { operationType: "fetch", dataProtocol: "postMessage", dataURL: "/RequestHandler/process" },
            { operationType: "add", dataProtocol: "postMessage", dataURL: "/RequestHandler/process" },
            { operationType: "update", dataProtocol: "postMessage", dataURL: "/RequestHandler/process" },
            { operationType: "remove", dataProtocol: "postMessage", dataURL: "/RequestHandler/process" },
        ]
});

Note that the beanClassName property has been defined as App6.Dao.Employee,App6, Below is the Employee class also used by NHibernate as an entity class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace App6.Dao
{
    public class Employee
    {
        public virtual string Name
        {
            get;
            set;
        }

        public virtual int EmployeeId
        {
            get;
            set;
        }

        public virtual int ReportsTo
        {
            get;
            set;
        }

        public virtual string Job
        {
            get;
            set;
        }

        public virtual string Email
        {
            get;
            set;
        }

        public virtual string EmployeeType
        {
            get;
            set;
        }

        public virtual string EmployeeStatus
        {
            get;
            set;
        }

        public virtual float Salary
        {
            get;
            set;
        }

        public virtual string OrgUnit
        {
            get;
            set;
        }

        public virtual string Gender
        {
            get;
            set;
        }
        public virtual string MaritalStatus
        {
            get;
            set;
        }

    }
}

This class is used by NHibernate as an entity mapped with the following mapping file:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="App6.Dao" assembly="App6">
    <class name="Employee" table="employee">
        <id name="EmployeeId" column="employeeId" type="Int32">
            <generator class="native" />
        </id>
        <property name="Name"/>
        <property name="ReportsTo"/>
        <property name="Job"/>
        <property name="Email"/>
        <property name="EmployeeType"/>
        <property name="EmployeeStatus"/>
        <property name="Salary" type="float"/>
        <property name="OrgUnit"/>
        <property name="Gender"/>
        <property name="MaritalStatus"/>
    </class>
</hibernate-mapping>

Create the database table for this DataSource. For this, open the Database Explorer, select the connection to the database and right click 'Tables'. In the popup menu select 'Add new table'. Using the table editor, enter the fields for the table as follows:

Column name

Data Type

Allow Nulls

Additional

EmployeeId

int

No

Identity and Primary Key

Name

varchar(128)

No

 

ReportsTo

int

No

 

Job

varchar(128)

Yes

 

Email

varchar(128)

Yes

 

EmployeeType

varchar(40)

Yes

 

EmployeeStatus

varchar(40)

Yes

 

Salary

float

Yes

 

OrgUnit

varchar(128)

Yes

 

Gender

varchar(7)

Yes

 

MaritalStatus

varchar(10)

Yes

 

Save the table with the name "employee", then open the table data and add a couple of sample rows, so the table has data.

Finally,  load this newly defined DataSource into the browser. For this edit the Index.aspx file located in the Views/Sample folder and add the following code:

<script SRC="/ds/employees.js">
</script>

just below where the supplyItem.js is currently loaded

UI changes

On the user interface, a change is required to allow users to switch the current DataSource. Add a form with a drop-down with the DataSources to switch and place it in front of the grids below the ListGrid. This requires putting the form in the HStack layout used for the buttons:

isc.HStack.create({
    membersMargin: 10,
    ID: "gridButtons",
    members: [
        isc.DynamicForm.create({
            values: { dataSource: "Change DataSource" },
            items: [
                { name: "dataSource", showTitle: false, editorType: "select",
                    valueMap: ["supplyItem", "employees"],
                    change: function (form, item, value, oldValue) {
                        if (!this.valueMap.contains(value)) return false;
                        else {
                            supplyItemGrid.setDataSource(value);
                            advancedFilter.setDataSource(value);
                            supplyItemGrid.filterData(advancedFilter.getCriteria());
                        }
                    }
                }
            ]
        }),
        isc.IButton.create({
            top: 250,
            title: "Edit New",
            click: "supplyItemGrid.startEditingNew()"
        }),

        isc.IButton.create({
            top: 250,
            left: 100,
            title: "Save all",
            click: "supplyItemGrid.saveAllEdits()"
        }),
    ]
});

Notice that when the user changes the DataSource,  the selected DataSource is set, both for the supplyItemGrid and for the advancedFilter FilterBuilder. Then filterData() is called on the grid to refresh the content.

This example now shows a data-driven DataSource that allows users to add/remove/update two DataSources with two different entity classes, and also apply various filter criterias built with the Filter Builder.

A visual studio solution with the complete source code can be downloaded from here