1. Handling a fetch request from RestDataSource using .NET+NHibernate

Description

This sample shows how to integrate a SmartClient front-end with an ASP.Net backend web application. 

SmartClient applications are single page applications most of the time. All the javascript and HTML is loaded or generated dynamically, and most of the time users don't need to leave the initially loaded page. This means the usual ASP.Net application architecture is not really usefull for developing rich applications, as most components and  helpers offered by the platform are mostly not needed or relevant.

The content discussed here will explore a way to integrate ASP.Net with SmartClient, by leveraging SmartClient javascript API's on the front-end and MVC architecture in ASP.Net MVC web application on the back-end to generate the result sets. In this sample, a DataSource wil be created which inherits RestDataSource, which will then be connected to an ASP.net MVC controller on the backend. This will get the data from the database using NHibernate and will serialize it back to te DataSource using JSON.Net. The results will be displayed in a ListGrid.

JSON.Net is being used as it is faster and offers more flexibility than the default service provided in ASP.net.

Prerequisites

  • Visual Web Developer 2010 Express, part of the Visual Studio Express suite nas been used as the development environment. It can be downloaded for free from here .
  • Microsoft Sql Server has been used as the Database. Any version is acceptable, including Express.
  • NHibernate has been used for the DAO layer. NHibernate can be downloaded from here
  • JSON.Net has been used for the JSON serialization, which can be downloaded from here

Setting up the application

Firstly, access Microsoft Web Developer and create an empty ASP.net MVC web application (for this example, choose C# as the development language).

Download the Evaluation version of SmartClient from here

After the download has completed, extract the archive and copy the complete isomorphic folder from *SmartClient_82_Evaluation\smartclientSDK* to the Scripts folder in your application's folder.

Now create a master page for the application. Here we will include the SmartClient scripts, so there will be no additional work to do in in the views themselves. To do this,, right click on the folder 'Views/Shared' (create it if does not exist) in your solution explorer and select from the displayed menu 'Add new Item'. In the popup which appears, select 'MVC 2 View Master Page' and enter 'SmartClient.master' for the name of the master page. Then, click 'Add' and a new master page should appear.

Find this section:

<title></title>

Immediately after this, insert the following code sequence, (note this is copied directly from the 'helloworld.html' template file found in the downloaded SmartClient archive, (in the SmartClient_82_Evaluation\smartclientSDK\templates\ folder)):

<SCRIPT>    var isomorphicDir = "/Scripts/isomorphic/";</SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_Core.js"></SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_Foundation.js"></SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_Containers.js"></SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_Grids.js"></SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_Forms.js"></SCRIPT>
    <SCRIPT SRC="/Scripts/isomorphic/system/modules/ISC_DataBinding.js"></SCRIPT>
	<SCRIPT SRC="/Scripts/isomorphic/skins/TreeFrog/load_skin.js"></SCRIPT>

Adding these script tags will now ensure the master page will manage the loading of SmartClient, providing that this page is always defined as the master page for the view.

Creating a view which uses SmartClient

Now, create a first view in which SmartClient is used. Views are displayed using controllers, therefore a new controller needs to be created for this view. Click on the 'Controllers' folder in the solution explorer and in the popup menu select 'Add' then select 'Controller'. For the name of the Controller enter 'SampleController' then click 'Add'. A new controller will be added, with a content similar to the below structure:

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

namespace App1.Controllers
{
    public class SampleController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
}

This simply means when the user accesses the Sample/index URL, this view will be returned. The views for this controller will be stored in the 'Views\Sample' folder. Now, right click on the 'Views\Sample' folder (create the 'Sample' folder if it doesn't exist) and select 'Add', then 'View'. In the popup that is now displayed, enter 'Index' for the view name. Leave 'select master page' checked, click on the  '...' button and select 'SmartClient.master' (this is the master page created earlier in the displayed popup). Leave 'ContentPlaceHolder ID' field with the current default value. and click Add.

Edit the newly created Index.aspx page and change it's content to the details specified below,.(this is the "hello world" application and just confirms everything works as expected):

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/SmartClient.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="head" runat="server">
</asp:Content>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

    <SCRIPT>

        isc.IButton.create({
            title: "Hello",
            click: "isc.say('Hello World')"
        })

</SCRIPT>

</asp:Content>

Now, to set this to this examples required definitions , the controller and view need to be set to the default values specified above. Locate in the solution explorer the 'Global.asax.cs' file and open it for editing  At line:

	new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults

Change these values to match the required example generated values:

	new { controller = "Sample", action = "Index", id = UrlParameter.Optional } // Parameter defaults

At this point there is now an ASP.Net application which is roughly equivalent of the Hello World sample. To test, start the application by pressing F5 or Ctrl+F5. The ASP.NET development server should start on a random port and a new browser will open. In the page will be the button that was added. By clicking this the button, will verify this all works as expected.

Creating the database

Create a database to use with this application. For this, open a command prompt and type:

	sqlcmd -S[your server instance} -E

(Note: add additional parameters if required in order to be able to connect to your sql server - this depends on how it is configured).

Once the has application started, type the following:

	create database app1;
	go

You should now have a database created for your application called app1. Add a connection to it in Visual Studio. To do this, select the  Database Explorer tab in Visual Studio and add a new connection by right clicking on the 'Data Connections' item. In the popup menu select 'Add connection, which will open an new window. In this window browse for your server name/instance and select it. Setup your credentials for connecting to the database. For the database name enter 'app1',  then click 'Ok'.

Select and right click 'Tables' in the newly created connection and 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

itemID

int

No

Identity and Primary Key

itemName

varchar(128)

No

SKU

varchar(10)

No

description

varchar(2000)

Yes

category

varchar(128)

Yes

units

varchar(5)

No

unitCost

float

Yes

inStock

int

Yes

nextShipment

datetime

Yes

Save the table with the name "supplyItem", then open the table data and add one or two of sample rows (so that the ListGrid we ultimately load this data into has some valid rows to display).

Integrating NHibernate

Download the binary for NHibernate and extract it to a folder on your disk. In Visual Studio right click in the Solution Explorer to 'References' and select 'Add reference' then select 'Browse' tab in the popup which appears. Browse to your NHibernate folder and add 'NHibernate.dll' and 'Iesi.Collections.dll' to the project.

Now create a configuration file for NHibernate. This is to specify how to connect to the database. In the downloaded archive there is a 'Configuration_Templates' folder which contains configuration template files for various database back-ends. Copy the MSSQL.cfg.xml to the project folder as 'hibernate.cfg.xml'. Edit it and change the connection string to the one used to connect to the database.

(Note: you can get the connection string by going to the Database Explorer tag and displaying the properties of the connection you created)

Change the build properties of this file to 'Copy always', as NHibernate will require it to be in the deployed folder, when running the application.

Creating the mapping file and the business object file

Go to the Solution Explorer and create a DAO folder in your project.

Right click on this folder and select 'Add new item', then add an xml file with the name 'supplyItem.hbm.xml' (which will be the hibernate mapping for the table in the database). It's content should be defined as:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="App1.Dao" assembly="App1">
    <class name="supplyItem" table="supplyItem">
        <id name="itemID" column="itemID" type="Int32">
            <generator class="native" />
        </id>
        <property name="itemName"/>
        <property name="SKU"/>
        <property name="description"/>
        <property name="category"/>
        <property name="units"/>
        <property name="unitCost" type="double"/>
        <property name="inStock" type="boolean"/>
        <property name="nextShipment" type="date"/>
    </class>
</hibernate-mapping>

In the properties section of the Solution Explorer for this file, select 'Embedded Resource' for the build action.

Right click on the DAO folder in Solution Explorer and add a new class, 'supplyItem.cs' (this is the object that NHibernate will use to map to the table in the database). This class simply defines the getters and setters for the table columns:

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

namespace App1.Dao
{
    public class supplyItem
    {
        public int itemID
        {
            get;
            set;
        }

        public string itemName
        {
            get;
            set;
        }

        public string SKU
        {
            get;
            set;
        }

        public string description
        {
            get;
            set;
        }

        public string category
        {
            get;
            set;
        }

        public string units
        {
            get;
            set;
        }

        public float unitCost
        {
            get;
            set;
        }

        public bool inStock
        {
            get;
            set;
        }

        public DateTime? nextShipment
        {
            get;
            set;
        }
    }
}

(Note: DateTime has a trailing "?" as, by default, DateTime cannot be null. If  the ? is not present null values will not be retrievable from the database for the nextShipment field)

NHibernate needs to be configured when the application starts. For this, open the 'Global.asax.cs' file, locate the 'protected void Application_Start()' function and add to the beginning the following code, which will configure NHiberbate and also make it load all the mapping files from the application's assembly:

Configuration cfg = new Configuration();
            cfg.Configure();

            cfg.AddAssembly(typeof(Dao.supplyItem).Assembly);

Utility classes

In order to make the implementation cleaner and easier, some utility classes are required. Firstly, create a Utils folder in the Solution Explorer by right clicking on the solution and then select 'Add', then 'New Folder' in the popup menus. Enter 'Utils' as the name and click OK.

One required utility is the NHibernateHelper class, which helps with creating and handling NHibernate sessions. Right click on the previously created 'Utils' folder and in the popup menu select 'Add' then 'Class', and enter NHibernateHelper as the class name. It should now look like this:

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

using NHibernate;
using NHibernate.Cfg;
using App1.Dao;

namespace App1.Utils
{
    public class NHibernateHelper
    {
        private static ISessionFactory _sessionFactory;

        private static ISessionFactory SessionFactory
        {
            get
            {
                if (_sessionFactory == null)
                {
                    var configuration = new Configuration();
                    configuration.Configure();
                    configuration.AddAssembly(typeof(supplyItem).Assembly);
                    _sessionFactory = configuration.BuildSessionFactory();
                }
                return _sessionFactory;
            }
        }

        public static ISession OpenSession()
        {
            return SessionFactory.OpenSession();
        }
    }
}

Now navigate to http://www.smartclient.com/docs/8.2/a/b/c/go.html#class..RestDataSource which describes, in detail, how the request and response should be defined for RestDataSource use, From this you will note that the response JSON sent back from the controller will look like this:

{
    response:{
       status:0,
       startRow:0,
       endRow:76,
       totalRows:546,
       data:[
           {field1:"value", field2:"value"},
           {field1:"value", field2:"value"},
           ... 76 total records ...
       ]
    }
 }

In order to able to do this, a helper object is required to mimic this structure. This object will be serialized with JSON.Net in the controller. This will be the DSResponse object.

Firstly, add a reference to JSON.Net in the project. In the Solution Explorer, right click on References and select 'Add reference' in the popup menu. Then in the popup window select the 'Browse' tab and navigate to the folder where JSON.Net was extracted, and select 'Newtonsoft.Json.dll', then click Ok.

Now right click on the  'Utils' folder in the Solution Explorer and select 'Add' then 'Class' in the popup menus. For class name enter 'DSResponse' and change the generated class content to this:

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

using Newtonsoft.Json;

namespace App1.Utils
{
    public class DSResponse
    {
        public class Response
        {
            public int status { get; set; }
            public int startRow { get; set; }
            public int endRow { get; set; }
            public int totalRows { get; set; }
            public object data { get; set; }
        }

        Response _response;

        public Response response
        {
            get
            {
                if (_response == null)
                {
                    _response = new Response();
                }

                return _response;
            }
            set
            {
                _response = value;
            }
        }

        [JsonIgnore]
        public int status
        {
            get { return response.status; }
            set { response.status = value; }
        }

        [JsonIgnore]
        public int startRow
        {
            get { return response.startRow; }
            set { response.startRow = value; }
        }

        [JsonIgnore]
        public int endRow
        {
            get { return response.endRow; }
            set { response.endRow = value; }
        }

        [JsonIgnore]
        public int totalRows
        {
            get { return response.totalRows; }
            set { response.totalRows = value; }
        }

        [JsonIgnore]
        public object data
        {
            get { return response.data; }
            set { response.data = value; }
        }
    }
}

Also, for easier JSON handling, define a JsonNetResult class, as defined here

Place this in the 'Utils' folder. with the following content:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Text;
using Newtonsoft.Json;

namespace App1.Utils
{
    public class JsonNetResult : ActionResult
    {
        public Encoding ContentEncoding { get; set; }
        public string ContentType { get; set; }
        public object Data { get; set; }

        public JsonSerializerSettings SerializerSettings { get; set; }
        public Formatting Formatting { get; set; }

        public JsonNetResult()
        {
            SerializerSettings = new JsonSerializerSettings();
        }

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            HttpResponseBase response = context.HttpContext.Response;

            response.ContentType = !string.IsNullOrEmpty(ContentType)
              ? ContentType
              : "application/json";

            if (ContentEncoding != null)
                response.ContentEncoding = ContentEncoding;

            if (Data != null)
            {
                JsonTextWriter writer = new JsonTextWriter(response.Output) { Formatting = Formatting };

                JsonSerializer serializer = JsonSerializer.Create(SerializerSettings);
                serializer.Serialize(writer, Data);

                writer.Flush();
            }
        }
    }
}

Creating the DataSource and the controller to handle the requests

Open the created view for the application (in the Solution Explorer, in the Views/Sample folder, open Index.aspx) and change it's content to:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/SmartClient.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="head" runat="server">

</asp:Content>

<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<SCRIPT SRC="/Scripts/ui.js">
</script>
</asp:Content>

This is now loading a javascript file from '/Scripts/ui.js'. We will place all required code into this javascript file, so it is easier to change, plus it is also good practice to have this separate from the view itself.

Create a datasource which inherits RestDataSource and define the data URL to a controller (which will be create later). This controller will handle the requests from the DataSource. The fields in the newly created DataSource will match the columns in the database table created earlier:

isc.RestDataSource.create({
	ID:"suppyItem",
	fields:[
		{name:"itemID", type:"sequence", hidden:"true", primaryKey:"true"},
        {name:"itemName", type:"text", title:"Item", length:"128", required:"true"},
        {name:"SKU", type:"text", title:"SKU", length:"10", required:"true"},
        {name:"description", type:"text", title:"Description", length:"2000"},
        {name:"category", type:"text", title:"Category", length:"128", required:"true", foreignKey:"supplyCategory.categoryName"},
        {name:"units", type:"enum", title:"Units", length:"5",
            valueMap:["Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"]
        },
        {name:"unitCost", type:"float", title:"Unit Cost", required:"true",
            validators:[
                {type:"floatRange", min:"0", errorMessage:"Please enter a valid (positive) cost"},
                {type:"floatPrecision", precision:"2", errorMessage:"The maximum allowed precision is 2"}
            ]
        },

        {name:"inStock", type:"boolean", title:"In Stock"},
        {name:"nextShipment", type:"date", title:"Next Shipment"}
	],

	dataFormat:"json",
	dataURL:"/RequestHandler/fetch"

});

Note, The required format  is 'json' and the data url is set to 'RequestHandler/fetch', which means the controller name will need to be set to 'RequestHandlerController' and the method name will be 'fetch'. As everything else is unchanged, the DataSource will send the request parameters in the URL of the request.

Create a ListGrid to display the results returned from the server:

isc.ListGrid.create({
    ID: "suppyItem",
    width: 700, height: 224, alternateRecordStyles: true,
    dataSource: suppyItem,
    autoFetchData: true
});

Now, create the controller to send back the response. For this, right click on the Controllers folder in the Solution Explorer and in the popup menu select 'Add' then 'Controller'. For the controller name enter 'RequestHandlerController'. Then enter the following content for it:

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

using NHibernate;
using App1.Utils;
using App1.Dao;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace App1.Controllers
{
    public class RequestHandlerController : Controller
    {
        public ActionResult fetch()
        {
            using (ISession session = NHibernateHelper.OpenSession())
            {
                var products = session
                    .CreateCriteria(typeof(supplyItem))
                    .List<supplyItem>();

                DSResponse dsresponse = new DSResponse();
                dsresponse.data = products;
                dsresponse.startRow = 0;
                dsresponse.endRow = products.Count - 1;
                dsresponse.totalRows = products.Count;
                dsresponse.status = 0;

                JsonNetResult jsonNetResult = new JsonNetResult();
                jsonNetResult.Formatting = Formatting.Indented;
                jsonNetResult.SerializerSettings.Converters.Add(new IsoDateTimeConverter());
                jsonNetResult.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;

                jsonNetResult.Data = dsresponse;

                return jsonNetResult;
            }
        }

    }
}

Note: The NHibernateHelper object defined earlier to create an NHibernate Session is used here. Once the collection of records is received,  the DSResponse helper object which was defined earlier set it's attributes. Finally the results need to be serialized with JSON.Net. Create a JsonNetResult helper object and define the serialization properties (ie. ignoring null fields, how to convert datetime fields, how to format responses etc), Set it's data to the DSResponse object created earlier, then return it as the response.

By hitting F5 or Ctrl+F5, the application will start and  a grid fetching and displaying the rows found in the table will be shown.

Date fields are correctly displayed, and if null, are missing from the grid.

The complete code for this sample project can be downloaded from here .