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

Saturday, March 29, 2008

StateBag Control - Simplifying Data Exchange in Heavy AJAX Pages

Download source code

View samples live:

 

Sample 1

Sample 2

Using standard ASP.NET postback model, browser sends data to the server using QueryString and Form objects.

If we look farther, ASP.NET has state persistence mechanism known also as ViewState, which uses simple hidden input to store the state data. Every time we make postback - the value of that hidden input is send with all other form data in Form object.

But the problem is - the state inside ViewState is encoded and is not accessible from the client side scripts. I already discussed this problem in my previous post(Passing complex objects between client browser and web server using ASP.NET AJAX framework). You can find in that post also some reasons why I need to view or modify some UI related state from client script.

The approach I used - is again the simple hidden input element containing some JSON formatted state representation, which is accessible from the javascript, which can be modified from javascript and sent back to the server, where it can be again modified and sent back.

In this post I want to write down my farther thoughts about this model, and how it can be used in more complex scenarios.

For example suppose the following page model:

image

Here we have a page that is loaded and constructed from different sources on the server.

I want my page to be very fast to render different dynamic areas in the browser, and the same time not to use postback to achieve this.

 

Suppose I use three dynamic areas in the page:

First area is being rendered by original page and contains main contents of the page.

Second is some iframe that is controlled from javascript. I want to load some another pages to it, and this way split functionality into separate pages in the system. There are many reasons to do this. For example you could load there page performing file upload functionality. Or you want to implement tabbed browsing in your website, and in each tab you want to place new iframe with different page from the server.

Third area is loaded using web service. Web service calls in AJAX are very fast comparing to simple postback, we can send them only the data we need in it, and not the whole form data as in the case of postback. Besides this on the server only the web service method is invoked - no page lifecycle, nothing unnecessary, but only the small and fast piece of code.

JSON state can help us to exchange data between these parts of the page. But I have to explain why JSON.

1) JSON is very useful when using rich client scripting in the browser. We can easily access and modify state object and it's parts.

2) JSON comparing to XML or some other formats occupies less space and is shorter. If you use XML to describe some data object it will need many extra attributes to be defined as node element names for example, and the resulting string will be longer. In web we have to think about size because we send and receive data to remote machines, and therefore network traffic should be as small as possible.

3) We can save state on the server - this is preferred of course when we don't need it on the client because in this case we don't have to pass the state over the wire at all, but as I said this approach is needed when we want to be aware of some state in the javascript. With javascript we can do many UI-related tasks without requesting the server, making page much faster and more useful for users.

I am going to use javascript to sync these parts of the page and pass some centralized state between them. Suppose that web services need to know something about part of the page in UpdatePanel. for example they return some data depending on what was selected in main dynamic area (UpdatePanel). Also after web services return some data and user changes something in that part (for example selects something or enters some text) I want to make other parts know about it(for example UpdatePanel part).

I created very simple but useful control that can help us with this.

The simple figure explains what control does:

image

The control has APIs on server and client.

server-side API:

image

GetStateID<T>() - method that is used to retrieve the state. In T generic parameter you need to specify the type into which you want to deserialize the state. The object is deserialized only after first call, on every subsequent call the cached copy is returned. This way if you modify it in any way before EndRequest handler - the first instance will be modified. This is important to know that in page lifecycle if you access the object using this method several times, the T generic parameter should be the same all the times.

StateID is optional property. Set it to some descriptive name and you will be able to call corresponding StateBag object on the client using this syntax: $find("StateID"). If property is not set - ClientID of the control will be used

UpdateMode property, can be set to one of two values: Always - the state is always sent to the server.  OnDemand - Developer needs to call Update() method manually to send the updated state to the browser (makes sense only during partial postback, when normal postback occurs - the state is always rendered to the value attribute of hidden input).

The following is diagram for enumeration of UpdateMode property type:

image

Update() method:  Call this method only when UpdateMode is set to OnDemand. Marks current state object to be sent to the client.

 

Client side API:

get_state() - returns state object.

set_state(value) - sets state object.

getSerializedString() - Returns current state object - serialized into string. We will need this function when calling web services. In postback model synchronization with the server is handled automatically.

setSerializedString(value) - sets current state object from string passed to this function. This function can be used when web service returns some result containing modified state.

update() - Marks current state object for sync operation. on first postback the state will be sent to the server. Can be called only when updateMode property is set to OnDemand value.

 

Actually server side API will be used only on initial page render or on subsequent postback, in the case of communicating with web services of another pages in iframe area we will use client side API to pass data back and forth.

 

I created server side control that is derived from Control type and is implementing IScriptControl interface. On the client the control will represent javascript control derived from Sys.UI.Control class.

If you want to know more about IScriptControl and Sys.UI.Control see the following links:

 

1) Adding Client Capabilities to a Web Server Control by Using ASP.NET AJAX Extensions.

2) Creating Custom ASP.NET AJAX Client Controls

3) IScriptControl Interface (System.Web.UI)

4) Sys.UI.Control Class

 

StateBag.cs file listing:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.UI;

namespace Devarchive.Net
{
    /// <summary>
    /// StateBag control simplifies data exchange between JavaScript and C# code
    /// </summary>
    [
        AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal),
        AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal),
        DefaultProperty("StateID")
    ]
    public class StateBag : Control, IScriptControl
    {
        #region Class level variables
        private object _state = null;
        private bool _stateDirty = false;
        private Page _page;
        private ScriptManager _scriptManager;
        #endregion

        #region Overrides
        protected override void OnInit(EventArgs e)
        {
            // disable the viewstate - we don't need it
            this.EnableViewState = false;
            base.OnInit(e);
        }

        protected override void OnPreRender(EventArgs e)
        {
            // Register control as ScriptControl
            this.ScriptManager.RegisterScriptControl(this);

            // Send state to the browser using DataItems collection
            // during async postback
            if (this.ScriptManager.IsInAsyncPostBack)
            {
                string state = GetJsonState();
                if (_stateDirty)
                {
                    this.ScriptManager.RegisterDataItem(this, state, false);
                }
            }
            else
            {
                _stateDirty = true;
            }

            // register onSubmit statement - it will serialize the js object and send back to the server
            string onSubmitHandler = GetOnSubmitStatement();
            if (
                !ContainingPage.ClientScript.IsOnSubmitStatementRegistered(
                    this.GetType(),
                    String.Format("StateBag_{0}", StateID)
                    )
                )
            {
                ContainingPage.ClientScript.RegisterOnSubmitStatement(
                    this.GetType(),
                    String.Format("StateBag_{0}", StateID),
                    onSubmitHandler
                    );
            }

            base.OnPreRender(e);
        }

        protected override void Render(HtmlTextWriter writer)
        {
            // Render control as hidden field with id set to StateID
            // and original UniqueID.
            // If the request was not async, or the page is being requested first time -
            // assign serialized graph to the value property
            if (!DesignMode)
            {
                ContainingPage.VerifyRenderingInServerForm(this);
                writer.AddAttribute(HtmlTextWriterAttribute.Id, StateID);
                writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);
                writer.AddAttribute(HtmlTextWriterAttribute.Value, GetJsonState());

                writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
                writer.RenderBeginTag(HtmlTextWriterTag.Input);
                writer.RenderEndTag();

                // Register descriptors
                ScriptManager.RegisterScriptDescriptors(this);
            }
            else
            {
                writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "black");
                writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "2px");
                writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "dashed");
                writer.AddStyleAttribute(HtmlTextWriterStyle.Padding, "5px");
                writer.RenderBeginTag(HtmlTextWriterTag.Div);
                writer.WriteEncodedText(
                        String.Format(
                            "StateBag <{0}> | UpdateMode <{1}>",
                            StateID,
                            UpdateMode
                            )
                        );
                writer.RenderEndTag();
            }
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// StateID is optional property.
        /// Set it to some descriptive name and you will
        /// be able to call corresponding StateBag object
        /// on the client using this syntax: $find("StateID")
        /// If not set - ClientID will be used
        /// </summary>
        [
            DefaultValue(""),
            Browsable(true)
        ]
        public string StateID
        {
            get
            {
                string id = GetPropertyValue("StateID", "");
                return (string.IsNullOrEmpty(id) ? ClientID : id);
            }
            set
            {
                SetPropertyValue("StateID", value);
            }
        }

        /// <summary>
        /// UpdateMode property, can be set to one of two values:
        /// Always - The state is always sent to the server.
        /// OnDemand - Developer needs to call Update() method manually
        /// to send the updated state to the browser (makes sense only during
        /// Partial postback, when normal postback occurs - the state is always rendered)
        /// </summary>
        [
            DefaultValue(StateBagUpdateMode.Always),
            Browsable(true)
        ]
        public StateBagUpdateMode UpdateMode
        {
            get
            {
                return GetPropertyValue("UpdateMode", StateBagUpdateMode.Always);
            }
            set
            {
                SetPropertyValue("UpdateMode", value);
            }
        }

        // Hide this derived property from designer - it is not needed
        [
            Browsable(false),
            DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
            EditorBrowsable(EditorBrowsableState.Never)
        ]
        public override bool Visible
        {
            get
            {
                return base.Visible;
            }
            set
            {
                throw new NotImplementedException();
            }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Returns state object. Type of expected object should be set in T.
        /// Object is cached after first call during page's lifecycle, so if second time 
        /// the object will be requested with different type, unhandled exception will occur.
        /// </summary>
        /// <typeparam name="T">
        /// Type of object to exchange with client
        /// </typeparam>
        /// <returns>
        /// Instanse on state object of type T
        /// </returns>
        public T GetState<T>() where T : class, new()
        {
            if (_state == null)
            {
                string stateString = GetRequestParamString(UniqueID, "");
                if (String.IsNullOrEmpty(stateString))
                {
                    _state = new T();
                }
                else
                {
                    _state = new JavaScriptSerializer().Deserialize<T>(stateString);
                }
            }
            return _state as T;
        }

        /// <summary>
        /// Call this method only when UpdateMode is set to OnDemand.
        /// Marks current state object to be sent to the client
        /// </summary>
        public void Update()
        {
            if (UpdateMode == StateBagUpdateMode.Always)
            {
                throw new InvalidOperationException("Update can not be called for StateBag control with UpdateMode set to Always");
            }
            UpdateInternal(false);
        }
        #endregion

        #region Internal helper members
        internal string GetOnSubmitStatement()
        {
            return
                String.Format(
                    "$find('{0}')._onSubmitHandler();",
                    StateID
                    );
        }

        /// <summary>
        /// Internal method - serializes current object to string
        /// </summary>
        /// <returns></returns>
        internal string GetJsonState()
        {
            string result = "";
            UpdateInternal(true);
            if (_stateDirty)
            {
                if (null == _state)
                {
                    GetState<object>();
                }
                string serializedObject = "";
                if (null != _state)
                {
                    serializedObject = new JavaScriptSerializer().Serialize(_state);
                }
                result = serializedObject;
            }

            return result;
        }

        internal void UpdateInternal(bool internalCall)
        {
            if ((internalCall && UpdateMode == StateBagUpdateMode.Always) || !internalCall)
            {
                _stateDirty = true;
            }
        }

        internal V GetPropertyValue<V>(string propertyName, V nullValue)
        {
            if (ViewState[propertyName] == null)
            {
                return nullValue;
            }
            return (V)ViewState[propertyName];
        }

        internal void SetPropertyValue<V>(string propertyName, V value)
        {
            ViewState[propertyName] = value;
        }

        /// <summary>
        /// Method is used internally to get JSON string from request
        /// </summary>
        /// <param name="parameterName"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        internal string GetRequestParamString(string parameterName, string defaultValue)
        {
            HttpContext context = HttpContext.Current;
            string result;
            if (context.Request[parameterName] != null)
            {
                result = context.Request[parameterName];
            }
            else
            {
                result = defaultValue;
            }
            return result;
        }

        internal ScriptManager ScriptManager
        {
            get
            {
                if (_scriptManager == null)
                {
                    Page page = ContainingPage;
                    _scriptManager = ScriptManager.GetCurrent(page);
                    if (_scriptManager == null)
                    {
                        throw new InvalidOperationException(
                            "Script Manager Required On The Page For StateBag Control"
                            );
                    }
                }
                return _scriptManager;
            }
        }

        internal Page ContainingPage
        {
            get
            {
                if (null == _page)
                {
                    Page page = Page;
                    if (null == page)
                    {
                        throw new InvalidOperationException("Page Cannot Be Null");
                    }
                    _page = page;
                }
                return _page;
            }
        }
        #endregion


        #region IScriptControl Members
        IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
        {
            ScriptComponentDescriptor s =
                new ScriptControlDescriptor("Devarchive.Net.StateBag", StateID);
            s.AddProperty("updateMode", (int)UpdateMode);
            s.AddProperty("clientID", ClientID);
            yield return s;
        }

        IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
        {
            yield return new ScriptReference("Devarchive.Net.StateBag.js", this.GetType().Assembly.FullName);
        }
        #endregion
    }
}

 

 

StateBag.js file listing:

 

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

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

// --------------------------------------------------------------
// StateBag class, derived from Sys.Component
// --------------------------------------------------------------
Devarchive.Net.StateBag = function(element) {
    // init base constructor
    Devarchive.Net.StateBag.initializeBase(this, [element]);
    
    // declare class level variables
    this._state = null;
    this._stateDirty = false;
    this._updateMode = null;
    this._clientID = null;
    this._pageRequestManager = null;
    this._pageLoadingHandlerDelegate = null;
};
Devarchive.Net.StateBag.prototype = {
    
    // --------------------------------
    // Overrides
    // --------------------------------
    
    initialize: function() {
        /// <summary> 
        /// Initializes Sys.Component instance
        /// <summary>
        
        // call base method
        Devarchive.Net.StateBag.callBaseMethod(this, 'initialize');
        
        // deserialize state first time
        this._deserializeState();
        
        // subscribe to pageLoading event to deserialize state every time after async postback
        
        // define delegate for event handler
        this._pageLoadingHandlerDelegate = 
            Function.createDelegate(this,this._pageLoadingHandler);
        
        // get the instance of PageRequestManager object
        if (Sys.WebForms && Sys.WebForms.PageRequestManager){
           this._pageRequestManager = Sys.WebForms.PageRequestManager.getInstance();  
        }
        
        // Subscribe to pageLoading event
        if (this._pageRequestManager !== null ){
            this._pageRequestManager.add_pageLoading(this._pageLoadingHandlerDelegate);
        }

    },
    
    dispose: function() {
        /// <summary>
        /// Dispozes Sys.Component instance
        /// </summary>
        
        // release resources
        this._state = null;
        
        // detach events
        if(this._pageRequestManager !== null){
            this._pageRequestManager.remove_pageLoading(this._pageLoadingHandlerDelegate);
        }
        
        // call base method
        Devarchive.Net.StateBag.callBaseMethod(this, 'dispose');
    },
    
    // --------------------------------
    // Properties
    // --------------------------------

    // state object itself
    get_state : function() {
        if (arguments.length !== 0) throw Error.parameterCount();
        return this._state;
    },
    
    set_state : function(value) {
        var e = Function._validateParams(arguments, [{name: "value", type: Object}]);
        if (e) throw e;
        if (this._state !== value) {
            this._state = value;
            this.raisePropertyChanged('state');
        }
    },
    
    // ClientID of a hiddenField - is needed to get the correct dataItem object (see below)
    get_clientID : function() {
        if (arguments.length !== 0) throw Error.parameterCount();
        return this._clientID;
    },
    
    set_clientID : function(value) {
        var e = Function._validateParams(arguments, [{name: "value", type: String}]);
        if (e) throw e;
        if (this._clientID !== value) {
            this._clientID = value;
            this.raisePropertyChanged('clientID');
        }
    },
    
    // updateMode property - May be set to Always or OnDemand values
    get_updateMode : function() {
        if (arguments.length !== 0) throw Error.parameterCount();
        return this._updateMode;
    },
    
    set_updateMode : function(value) {
        var e = Function._validateParams(arguments, [{name: "value", type: Number}]);
        if (e) throw e;
        if (this._updateMode !== value) {
            this._updateMode = value;
            this.raisePropertyChanged('updateMode');
        }
    },

    // --------------------------------
    // Public methods 
    // --------------------------------
    
    getSerializedString : function() {
        /// <summary>
        /// Returns current state object - serialized into string
        /// </summary>
        /// <returns type="String"></returns>
        return Sys.Serialization.JavaScriptSerializer.serialize(this.get_state());
    },
    
    setSerializedString : function(value) {
        /// <summary>
        /// sets current state object from string passed to this function
        /// </summary>
        this.set_state(Sys.Serialization.JavaScriptSerializer.deserialize(
            value)
            );
    },
    
    update : function() {
        /// <summary>
        /// Marks current state object for sync operation.
        /// on first postback the state will be sent to the server
        /// can be called only when updateMode property is set to OnDemand value (2)
        /// </summary>
        if(this.get_updateMode() == Devarchive.Net.StateBagUpdateMode.Always) {
            throw Error.invalidOperation(
                "Update can not be called for StateBag control when it's"+
                " updateMode property is set to Always");
        }
        this._stateDirty = true;
    },
    
    // --------------------------------
    // Event handlers
    // --------------------------------
    
    _pageLoadingHandler : function(sender, args) {
        /// <summary>
        /// Raised after the response from the server to an asynchronous 
        /// postback is received but before any content on the page is updated.
        /// At this stage we deserialize string if any was sent by server.
        /// </summary>
        var dataItem = args.get_dataItems()[this.get_clientID()];
        if (dataItem){
            this.get_element().value = dataItem;
            this._deserializeState();
          }
    },
    
    _onSubmitHandler : function(sender, args) {
        /// <summary>
        /// Raised before form is submitted.
        /// If needed serialized version of state object is created and
        /// stored in the hidden field
        /// </summary>
        if(
            // check if object was marked to sent to the server an updated version
            this._stateDirty || 
            // or if updateMode is set to Always - send it's updated version everytime
            this.get_updateMode() == Devarchive.Net.StateBagUpdateMode.Always
          ) {
            this._serializeState();
            this._stateDirty = false;
        } else {
            this.get_element().value = "";
        }
    },
    
    _serializeState : function() {
        /// <summary>
        /// Converts a JavaScript object graph into a JSON string
        /// and saves it in the HiddenField element
        /// </summary>
        this.get_element().value = this.getSerializedString();
    },
    
    _deserializeState : function() {
        /// <summary>
        /// Converts a JSON string into a JavaScript object graph
        /// and sets this object to state property
        /// </summary>
        this.setSerializedString(this.get_element().value);
    }
};
Devarchive.Net.StateBag.registerClass('Devarchive.Net.StateBag', Sys.UI.Control);

// --------------------------------------------------------------
// StateBagUpdateMode enumeration
// --------------------------------------------------------------
Devarchive.Net.StateBagUpdateMode = function(){};
Devarchive.Net.StateBagUpdateMode.prototype = 
{
        Always      :   1, 
        OnDemand    :   2
}
Devarchive.Net.StateBagUpdateMode.registerEnum("Devarchive.Net.StateBagUpdateMode", false);

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

 

Here I have to stop for a while. I will use this post as a reference to StateBag control in my next posts, where I will show sample code showing how to use this control. Also in next postings I am going to show how to exchange the data between three parts of the page as was shown on the first figure.

Stay tuned !

 

Read Part 2 of the article here: StateBag Control - Simplifying Data Exchange in Heavy AJAX Pages - Sample Source Code Part 1

Read Part 3 of the article here: StateBag Control - Simplifying Data Exchange in Heavy AJAX Pages - Sample Source Code Part 2

 

Technorati Tags:

kick it on DotNetKicks.com

Creating Custom Auto Complete Content Loading Functionality Using ASP.NET AJAX

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

As more developers move to AJAX and start using it's controls, I noticed - the more they are bound to AjaxControlToolkit controls. Often I can see they use AjaxControlToolkit controls in places where it is not needed at all, sometimes, in places where the problem could be solved more efficiently, using very simple script.

I want to remind that AjaxControlToolkit and in fact any toolkit is designed in the way it can be used in some scenarios, and to make control that answers more requirements it could often contain more code than it is needed for some custom little task. The more universal control is, the more code it uses (true but not always). The more code it contains the less performance it provides (true but not always), the more complex control is - the more complex is to use it (again it may be not true for everything - but mainly it is true).

I am participant on ASP.NET forums, and often see when people try to use controls in places for which the control was not designed.

I am not an angel also. I caught myself several times trying to solve problem quickly with some control that is already done, but paid for this with performance degradation and in the end with large amount of spent time. I am really happy when I see that I do something wrong. I am by nature the person who can change mind in the last second, and after rethinking and healthy criticizing my actions can rewrite a good bunch of code to make it right.

 

Sorry for long preface,

Lets create a simple auto complete - like functionality.

On the page I have one input control and one div element. I want to show list of links in the div when typing something in text input control. The list should be loaded from the server.

Here is the markup of a page:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="Default.aspx.cs" Inherits="Loading_Contents_While_Typing_Ajax._Default" %>

<!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>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
            <Services>
                <asp:ServiceReference Path="~/AutoComplete.asmx" />
            </Services>
        </asp:ScriptManager>
        <script type="text/javascript">
        // time in milliseconds to wait after last character typed by user
        var _WAIT_TIME = 300;
        var _timeoutFunction;
        var _executor = null;
        Sys.Net.WebRequestManager.add_invokingRequest(onInvokingRequest); 
        function onTxtKeyUp() {
            _timeoutFunction = setTimeout(performSearch , 300);
        }
        function onTxtKeyDown() {
            if(_timeoutFunction)clearTimeout(_timeoutFunction);
        }
        function performSearch() {
            if(_executor) {
                _executor.abort();
                _executor = null;
            }
            AutoComplete.GetHypherLinks($get("txtText").value, onComplete, onError);
        }
        function onInvokingRequest(sender, args) {
            _executor = args.get_webRequest().get_executor();
        }
        function onComplete(result) {
            // do whatever you want with result from web server
            $get("divOutput").innerHTML = result
            _executor = null;
        }
        function onError() {
            // place error handling code here
            _executor = null;
        }
        </script>
        <input onkeydown="onTxtKeyDown()" onkeyup="onTxtKeyUp()" 
            type="text" id="txtText" value="" />
        <div id="divOutput"></div>
    </form>
</body>
</html>

And here is a sample webService:

using System.Web.Services;
using System.Web.Script.Services;
using System.Collections.Generic;
using System.Text;
using System;
[WebService]
[ScriptService]
public class AutoComplete : System.Web.Services.WebService
{

    [WebMethod]
    public string GetHypherLinks(string input)
    {
        return GetFormattedList(GetHypherLinksInternal(input));
    }

    private Dictionary<string, string> GetHypherLinksInternal(string input)
    {
        Dictionary<string, string> dic = new Dictionary<string, string>();

        // test results - add real implementation later
        for (int i = 0; i < 5; i++)
        {
            dic.Add(String.Format("{0}_{1}", input, i), String.Format("{0}_{1}", input, i));
        }

        return dic;
    }

    private string GetFormattedList(Dictionary<string, string> links)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("<ul>");
        foreach (string text in links.Keys)
        {
            sb.AppendLine(String.Format("<li><a href='{0}'>{1}</a></li>", links[text], text));
        }
        sb.AppendLine("</ul>");
        return sb.ToString();
    }
}

 

 

And one more note in the end - In some places and some projects using single control may be preferred, Suppose for example you have used this control in a 1000 places in the project, and you want to change one behavior aspect in all 1000 places, by changing something in one place. - ok here you have to use the control. But anyway I would write my own for such a simple tasks.

Any control has it's "application" area, and range where you can use it and where using it does not make sense is important to understand.

 

Hope this helps.

Technorati Tags:

Sunday, March 9, 2008

JavaScript Enumerations in ASP.NET AJAX Framework

With AJAX "revolution" last years we write more and more complicated javascript code. Using enumerations can significantly improve the quality of client-side code and make it more readable. In this post I will show some code samples describing how to create and use enumerations in ASP.NET AJAX framework.

So how can we create and use enumerations in ASP.NET AJAX framework.

The simplest enumeration looks like this:

// Register new Namespace, we will create enumeration inside it
Type.registerNamespace('MyNamespace');

// Define an enumeration type.
MyNamespace.MyEnum = function(){};
MyNamespace.MyEnum.prototype = 
{
        Value1  : 1, 
        Value2  : 2,
        Value3  : 4,
        Value4  : 8
}
// Register type as enumeration
// Syntax
//     ANamespace.AnEnum.registerEnum(name, flags);
// Arguments
//     name 
//     A string that represents the fully qualified name of the enumeration.
//     flags 
//     true to indicate that the enumeration is a bit field; otherwise, false.
MyNamespace.MyEnum.registerEnum("MyNamespace.MyEnum", false);

 

Now lets assign enumeration value to some variable:

// assign some value from enumeration to variable
var value = MyNamespace.MyEnum.Value1;

Now it's interesting to see that in javascript the variable "value" will have different type then we expect, it's simply "Long" value. This is how it differs from using enums in C#:

image

Also note that Value1, Value2 and another members of enum are also of type "Long".

Ok, next let's use switch statement for our enumeration to find out which value is assigned to it:

// we can use switch statement to find which value is assigned to variable
switch(value) {
    case MyNamespace.MyEnum.Value1:
        alert("MyNamespace.MyEnum.Value1");
    break;
    case MyNamespace.MyEnum.Value2:
        alert("MyNamespace.MyEnum.Value2");
    break;
    case MyNamespace.MyEnum.Value3:
        alert("MyNamespace.MyEnum.Value3");
    break;
    case MyNamespace.MyEnum.Value4:
        alert("MyNamespace.MyEnum.Value4");
    break;
}

And here is the result:

image

As you can see the code is clean - we can see definitely what values are standing for and what is the processing logic for each of them. Of course it's better than using cryptic numbers like 1,2,3,4 etc. (In real application you will assign more descriptive names to enumeration members then they are in the sample).

Let's move next. 

Enumerations, when registered with line of code in the first block above are automatically supplemented with two useful functions. These are "parse" and "toString" functions.

For example the following code at this point:

// we can get string representation of variable containing some value
// that enumeration contains
var valueStr = MyNamespace.MyEnum.toString(value);
alert(valueStr);

And the result of executing the code is:

image

By using parse function of enumeration we can get Long value by specifying the string value:

// we can get Long type value by specifying string for enumeration member:
var valueFromStr = MyNamespace.MyEnum.parse(valueStr);
alert(valueFromStr);

And the result:

image

Ok, now what if we want to use enumeration as a set of flags? Can we do this with AJAX, - answer - yes. But first look at the first block of code above. We must specify this when registering the enumeration:

// Arguments
//     name 
//     A string that represents the fully qualified name of the enumeration.
//     flags 
//     true to indicate that the enumeration is a bit field; otherwise, false.
MyNamespace.MyEnum.registerEnum("MyNamespace.MyEnum", false);

In this code the second parameter is telling the AJAX framework, that the enumeration we are registering can not be used as flags. So If for example I will try to run the following code:

// use enumeration as a set of flags and try to show string representation
var values = MyNamespace.MyEnum.Value1 | MyNamespace.MyEnum.Value1;
alert(MyNamespace.MyEnum.toString(values));

Immediately the following exception is shown:

image 

So why is this happening? And what "3" stands for?

When we assign several values to some variable using "|" operator - this performs simple bitwise "OR" operation, and therefore for values 1 and 2 - the result of the operation will be 3, so this line of code goes without exception, but when we try to return string representation of that value - the framework throws an error.

Let's try to change the line of code where we will register enumeration as flags:

MyNamespace.MyEnum.registerEnum("MyNamespace.MyEnum", true);

After running the code everything works now:

image

Let's try to do reverse thing - turn the string "Value1, Value2" into Long value and see what will it be:

// Assign several flags to Long value by using String representation
var valuesFromStr = MyNamespace.MyEnum.parse("Value1, Value2");
alert(valuesFromStr);

Here is the result:

image

Ok, now how we check if a long value with flags contains some specific flag:

// check if values var containing flags includes separate flag 
if(values & MyNamespace.MyEnum.Value1) {
    alert("values flags includes Value1 flag");
}

if(values & MyNamespace.MyEnum.Value2) {
    alert("values flags includes Value2 flag");
}

if(values & MyNamespace.MyEnum.Value3) {
    alert("values flags includes Value3 flag");
}

if(values & MyNamespace.MyEnum.Value4) {
    alert("values flags includes Value4 flag");
}

Result:

image image

 

Hope this helps.

If you find this post helpful - please "kick" it.

kick it on DotNetKicks.com

Technorati Tags: ,