Sunday, March 30, 2008

StateBag Control - Simplifying Data Exchange in Heavy AJAX Pages - Sample Source Code Part 1

You can download code for this article or see it live.

Also you can download source code for StateBag control only.

This is second part of article that was started here.

I will start with smaller samples.

First lets create simple page with UpdatePanel on it and leverage our state control to save and send to server some client side state.

In the sample I will use google charts API to create charts dynamically, straight from javascript.

Also I will allow a user to choose how many items the graph will show.

And then send the results to the server (this part will be handled automatically by StateBag control).

So the application will look like:

1)

image

Here user can place some integer value from 1 to 100 into the textbox and click "Create new graph".

2)

image

On this stage user specified that he wants to create a chart with 5 data values and clicks "Create new graph" button.

Note this everything is again done using javascript! No postbacks, no service calls. That is why we need complex object to be maintained on the client.

3)

image

User hits "Save" button. On this stage StateBag control understands that form is being submitted and serializes all the state into string and stores it in the hidden field. After that server side part of the control deserializes it and creates in this case simple confirmation message explaining what data was exactly get by the server. In real world scenario you would save these values in the database or validated them and modified on the server. That is good thing on this control. You can modify the state on the client, or if you wish on the server.

 

Ok let's see the code.

Page looks like this:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Sample1_UpdatePanel.aspx.cs" 
    Inherits="Sample1_UpdatePanel" %>
<%@ Register Assembly="Devarchive.Net" Namespace="Devarchive.Net" TagPrefix="dn" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Sample1_UpdatePanel</title>
    <style type="text/css">
        .headerRow 
        {
            background-color:Silver;
            border-width:1px;
            border-color:black;
            border-style:solid;
        }
        .divOutput
        {
            border:solid 1px solid;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="SM">
        <Scripts>
            <asp:ScriptReference Path="~/Sample1_UpdatePanel.js" />
        </Scripts>
    </asp:ScriptManager>
    <dn:StateBag runat="server" ID="State1" StateID="StateBag1" UpdateMode="Always" />
    <div>
        <input type="text" id="txtCount" value="" />
        <input type="button" id="btnCreateGraph" value="Create new graph"
            onclick="$find('sample1').newGraph($get('txtCount').value);" />
    </div>
    <div id="divOutput" class="divOutput"></div>
    <hr />
    <asp:UpdatePanel runat="server" ID="UP">
        <ContentTemplate>
            Click the button to update state on the server and get confirmation message<br />
            <asp:Button runat="server" Text="Save" ID="btnSave" onclick="btnSave_Click" />
            <div runat="server" id="divServerResults" class="divOutput"></div>
        </ContentTemplate>
    </asp:UpdatePanel>
    </form>
    <script type="text/javascript">
        Sys.Application.add_init(function() {    
            $create(
                Devarchive.Net.Sample1, 
                {id:"sample1",stateBagID:"StateBag1",divOutput:"divOutput"}, 
                null, null, null
                );
        });
    </script>
</body>
</html>

 

Here I add StateBag control to the page and set it's StateID property to "StateBag1". In last script on the page I am declaring creation of my custom script class that is specific to this page.

Second thing wee need to create is the state class on the server. This state class will define what we want to be stored in it.

The following is the listing of Sample1State.cs:

using System.Collections.Generic;

namespace Devarchive.Net
{
    public class Sample1State
    {
        public Graph Graph = new Graph();
    }

    public class Graph
    {
        public List<GraphLine> Values = new List<GraphLine>();
    }

    public class GraphLine
    {
        public int Value;
        public string Title;
    }
}

 

Next lets create event handlers in the code behind of the page:

 

using Devarchive.Net;
using System;
using System.Text;

public partial class Sample1_UpdatePanel : System.Web.UI.Page
{

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            // dummy call to GetState - only on initial call - to create the structure of state 
            // for client. This is not required if you create structure on the client manually
            // but it simplifies a lot of things
            State1.GetState<Sample1State>();
        }
    }

    protected void btnSave_Click(object sender, EventArgs e)
    {
        Sample1State state = State1.GetState<Sample1State>();
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("Server recieved and processed the following data from graph state:<hr />");
        foreach (GraphLine line in state.Graph.Values)
        {
            sb.AppendLine(String.Format("Title: {0}; Value: {1}<br />", line.Title, line.Value));
        }
        divServerResults.InnerHtml = sb.ToString();
    }
}

And last lets write some custom javascript class that will implement rich client logic and will use StateBag on the client to persist the state:

/// <reference name="MicrosoftAjax.debug.js" />
/// <reference name="MicrosoftAjaxWebForms.debug.js" />

// define Devarchive.Net namespace
Type.registerNamespace("Devarchive.Net");

// --------------------------------------------------------------
// Sample1 class, derived from Sys.Component
// --------------------------------------------------------------
Devarchive.Net.Sample1 = function() {
    // init base constructor
    Devarchive.Net.Sample1.initializeBase(this);
    
    this._stateBagID = null;
    this._divOutput = null;
    this._pageLoadHandlerDelegate = null;
    this._tblGraph = null;
    this._imgGraph = null;
};
Devarchive.Net.Sample1.prototype = {
    
    // --------------------------------
    // Properties
    // --------------------------------
    
    get_stateBagID : function() {
        return this._stateBagID;
    },
    set_stateBagID : function(value) {
        this._stateBagID = value;
    },
    get_divOutput : function() {
        return this._divOutput;
    },
    set_divOutput : function(value) {
        this._divOutput = value;
    },
    
    // --------------------------------
    // Overrides
    // --------------------------------
    
    initialize: function() {
        /// <summary> 
        /// Initializes Sys.Component instance
        /// <summary>
        Devarchive.Net.Sample1.callBaseMethod(this, 'initialize');
        this._pageLoadHandlerDelegate = 
            Function.createDelegate(this,this._pageLoadHandler);
        this.createGraphsTable();            
        Sys.Application.add_load(this._pageLoadHandlerDelegate);
    },
    
    dispose: function() {
        /// <summary>
        /// Dispozes Sys.Component instance
        /// </summary>
        Sys.Application.remove_load(this._pageLoadHandlerDelegate);
        Devarchive.Net.Sample1.callBaseMethod(this, 'dispose');
    },
    
    // --------------------------------
    // Event handlers
    // --------------------------------
    
    _pageLoadHandler : function(sender, args) {
        /// <summary>
        /// Fires after each async postback
        /// </summary>
        
    },
    
    createGraphsTable : function() {
        var div = $get(this._divOutput);
        div.innerHTML = "";
        this._tblGraph = document.createElement("table");
        this._tblGraph.style.width = "100%";
        div.appendChild(this._tblGraph);
    },
    
    clearGraphsTable : function() {
        while(this._tblGraph.rows.length>0) {
            $clearHandlers(this._tblGraph.rows[0]);
            this._tblGraph.deleteRow(this._tblGraph.rows[0]);
        }
    },
    
    updateGraphsTable : function() {
        var graph = $find(this._stateBagID).get_state().Graph;
        this.clearGraphsTable();
        var tbl = this._tblGraph;
        // create title for table
        var newRowt = tbl.insertRow(tbl.rows.length);
        newRowt.className = "headerRow";
        var cellt2 = newRowt.insertCell(newRowt, 0);
        cellt2.innerHTML = "Value";
        var cellt1 = newRowt.insertCell(newRowt, 0);
        cellt1.innerHTML = "Title";
        for(var i=0; i< graph.Values.length; i++) {
            var title = graph.Values[i].Title;
            if(!title) title = "";
            var value = graph.Values[i].Value;
            if(!value) value = "";
            var newRow = tbl.insertRow(tbl.rows.length);
            var cell2 = newRow.insertCell(newRow, 0);
            var tbValue = document.createElement("input");
            tbValue.type = "text";
            tbValue.value = value;
            cell2.appendChild(tbValue);
            var cell1 = newRow.insertCell(newRow, 0);
            var tbTitle = document.createElement("input");
            tbTitle.type = "text";
            tbTitle.value = title;
            cell1.appendChild(tbTitle);
            var delegateKeyUpV = Function.createDelegate(
                {
                    "uc" : this, 
                    "type" : "Value",
                    "index" : i
                }, 
                this.tbValueChanged
            );
            var delegateKeyUpT = Function.createDelegate(
                {
                    "uc" : this, 
                    "type" : "Title",
                    "index" : i
                }, 
                this.tbValueChanged
            );
            $addHandler(tbValue, "keyup", delegateKeyUpV);
            $addHandler(tbTitle, "keyup", delegateKeyUpT);
        }
        var newRow = tbl.insertRow(tbl.rows.length);
        var cell = newRow.insertCell(newRow, 0);
        cell.collspan = 2;
        var graphImage = document.createElement("img");
        cell.appendChild(graphImage);
        this._imgGraph = graphImage;
        this.showChartData(graph);
    },
    
    showChartData : function() {
        var graph = $find(this._stateBagID).get_state().Graph;
        var img = this._imgGraph;
        if(graph && img) {
            var sb = new Sys.StringBuilder();
            var sbT = new Sys.StringBuilder();
            var sbV = new Sys.StringBuilder();
            sb.append("http://chart.apis.google.com/chart?");
            sb.append("chs=450x200");
            sb.append("&cht=p3");
            var lastValueSep = "";
            var lastTitleSep = "";
            for(var i=0; i< graph.Values.length; i++) {
                if(
                    graph.Values[i].Title && 
                    graph.Values[i].Title != "" &&
                    graph.Values[i].Value &&
                    graph.Values[i].Value != ""
                   ) {
                    sbT.append(lastTitleSep);
                    lastTitleSep = "|";
                    sbV.append(lastValueSep);
                    lastValueSep = ",";
                    sbT.append(graph.Values[i].Title);
                    sbV.append(graph.Values[i].Value);
                }
            }
            sb.append("&chd=t:");
            sb.append(sbV.toString());
            sb.append("&chl=");
            sb.append(sbT.toString());
            img.src = sb.toString();
        }
    },
    
    tbValueChanged : function(args) {
        var graph = $find(this.uc._stateBagID).get_state().Graph;
        var tb = args.target;
        if(graph && graph.Values[this.index] && tb) {
            if(this.type == "Title") {
                graph.Values[this.index].Title = tb.value;
            } else if(this.type == "Value") {
                graph.Values[this.index].Value = tb.value;
            }
            this.uc.showChartData();
        }
    },
    
    newGraphLine : function(title, value) {
        return {
            "Title" : title,
            "Value" : value
        };
    },
    
    newGraph : function(value){
        var graph = $find(this._stateBagID).get_state().Graph;
        var length = parseInt(value);
        if(length && length<100) {
            graph.Values = new Array();
            for(var i = 0; i<length; i++) {
                graph.Values[i] = this.newGraphLine("", "");
            }
            this.updateGraphsTable();
        } else {
            alert("length can not be parsed, or it is too large value");
        }
    }
};
Devarchive.Net.Sample1.registerClass('Devarchive.Net.Sample1', Sys.Component);

 

Here the most important line of code almost in all functions in the class is:

var graph = $find(this._stateBagID).get_state().Graph;

this is how we access the state in the StateBag. By using $find shortcut of AJAX library, specifying the id of the control we have access to it's properties and functions.

We don's set the state itself by calling set_state() function explicitly, because state property itself is used by reference, so if we change something in graph variable, it will be changed in our StateBag.

 

The series of articles will continue and in next posts I will show more complex examples(see first figure in the previous article).

Stay tuned !

See next post continuing with another more complex sample here: StateBag Control - Simplifying Data Exchange in Heavy AJAX Pages - Sample Source Code Part 2

 

 

Technorati Tags:
kick it on DotNetKicks.com

No comments: