This article describes one possible scenario of passing complex objects between client browser and server code. You can also download source code for the article, or see online sample.
Possible scenarios for using state persistence architecture described in this article
1) We have user controls in web application that have some rich client side functionality. And one of the required features is the ability to work with some complex objects on the server and client.
2) We have a very deep hierarchy of user controls in some pages of web application. The size of ViewState is unacceptable, also the rendered HTML controls have very long ID and Name attributes. We decide to use HTML controls instead of server controls, but for this need to create custom state holder object.
What I want to achieve
1) The server-side user control uses some class instance that describes it's current state.
2) The state is modified before rendering the control in accordance with business logic functionality of the control.
3) In control's OnPreRender event we want to pass the state object to the client side, rendering it to the response as serialized string representing object data.
4) On the client side we are deserializing the string to javascript object that has similar structure as it has on the server.
5) During control working on the browser (before first postback), we can modify the object using javascript.
6) Before postback, the object is serialized into string and sent to the server along with another form variables.
7) Server-side control deserializes the string and gets class instance of an object with all data that contains changes made from javascript.
8) Server-side control uses this state before rendering, changes it's values, analyzes values changed by client script etc..
9) This cycle continues again and again.
What are my requirements to API providing this functionality
1) State object should be customizable and reflect custom logic for user control using it.
2) State object should be inherited from some BaseState class, providing common functionality for all state objects. This is desirable, as in this case we will not have to repeat same code blocks in different classes, and API will be more usable and natural.
3) The mechanism of injecting custom state object support into user control should be as easy as possible. User control should not contain too much code for state persistence, it should contain UI-related code instead.
Solution
To accomplish this goal I will use ASP.NET AJAX library, and it's JavaScriptSerializer class.
The main advantages we get using this class is - AJAX library contains methods that can serialize/deserialize the classes from javascript code or .NET code (c#, VB.NET etc.)
Its very simple to do on the server:
the code that serializes object instance into string:
JavaScriptSerializer serializer = new JavaScriptSerializer(); string serializedString = serializer.Serialize(stateObjectInstance);
the code that deserializes string and creates class instance:
JavaScriptSerializer serializer = new JavaScriptSerializer(); return serializer.Deserialize<T>(serializedString);
T is the type of object we want to create instance of.
Serialization/deserialization is also very simple to do in the javascript code:
the code that serializes the object instance into string:
var serializedString = Sys.Serialization.JavaScriptSerializer.serialize(stateObjectInstance);
the code that deserializes string and creates class instance:
var stateObjectInstance = Sys.Serialization.JavaScriptSerializer.deserialize(serializedString);
Lets view state object I use in this sample - named CheckBoxListState :
using System.Collections.Generic; using System.Web.Script.Serialization; namespace Devarchive_net { public class CheckBoxListState : BaseState { #region Private members private Dictionary<string, bool> m_CheckBoxStates = new Dictionary<string, bool>(); #endregion #region Properties public Dictionary<string, bool> CheckBoxStates { get { return m_CheckBoxStates; } set { m_CheckBoxStates = value; } } #endregion #region Overrides public override string SerializedString() { JavaScriptSerializer serializer = new JavaScriptSerializer(); return serializer.Serialize(this); } #endregion } }
CheckBoxStates - is property of generic Dictionary type. It will hold some key-value pairs describing checkbox states (checked/unchecked) and the associated rowID values.
The class structure is very specific to the User control we use it in.
CheckBoxListState class inherits from BaseState class. The class BaseState has the following structure:
using System; using System.Collections.Generic; using System.Text; using System.Web.Script.Serialization; namespace Devarchive_net
{ public class BaseState { #region Fields private string m_StateID = string.Empty; #endregion #region Properties public virtual string StateID { get { return m_StateID; } set { m_StateID = value; } } #endregion #region Public Methods public virtual string SerializedString() { return ""; } #endregion #region Static members public static T Create<T>(string serializedString) { JavaScriptSerializer serializer = new JavaScriptSerializer(); return serializer.Deserialize<T>(serializedString); } #endregion } }
StateID Property will hold some unique value for the object instance, and will help us use correct instance on the client browser in the case of having several user controls of the same or different types.
SerializedString() is virtual method that will be overridden in the child objects, it will return serialized string of object data.
Create<T>(string serializedString) - is the static method that takes serialized string as a parameter and created the instance of the class of type T. The method is used to deserialize state object from string received from browser.
The object contents before serialization looks in debugger like this:
The following is the string that is generated using JavaScriptSerializer.Serialize method:
{"State":{"1":false,"2":false,"3":false,"7":false,"8":false,"4":false,"5":false,"9":false,"6":false},"StateID":"Sf4b917e573984122b1eb57d386566c65"}
You can read more about serialization using JavaScriptSerializer class on official ASP.NET AJAX online documentation.
Ok, now we know how it works. It's time to show the code that injects this functionality into our user controls with minimal efforts.
First thing I want to mention - I wrote the code for injecting javascript-supported state object functionality in the way it can be easily used for different state classes. Because different user controls can and will have absolutely different state class structures.
Now it's time to show bigger picture.
To better understand the code I strongly recommend to download and play with solution archive(see download link above). The code is not just a sample, I use it in my production projects, and I spent a lot of hours while made it work in the way it works now!
First class here is BaseUserControl :
using System; using System.ComponentModel; using System.Web.UI; using System.Web.UI.WebControls; using System.IO; namespace Devarchive_net { public class BaseUserControl : UserControl { #region Private Fields private HiddenField m_StateField = new HiddenField(); #endregion #region Overrides protected override void OnInit(EventArgs e) { m_StateField.ID = "S"; this.Controls.Add(m_StateField); base.OnInit(e); } #endregion #region Properties /// <summary> /// Hidden field that is injected /// into all user controls inherited from this class, /// used for storing some state visible for javascript /// </summary> [Browsable(false)] public virtual HiddenField StateField { get { return m_StateField; } set { m_StateField = value; } } #endregion #region Methods public string GetTemplate(string fileVirtualPath, params object[] args) { using (StreamReader sr = new StreamReader(MapPath(fileVirtualPath))) { return string.Format(sr.ReadToEnd(), args); } } #endregion } }
this class inherits from UserControl, basically it creates HiddenField control named m_StateField and injects it into UserControl, this HiddenField will contain serialized version of current state object.
there is also method named GetTemplate that just loads text and html files for inclusion into current control, nothing special, just some helper method (I prefer to use simple html and text where possible to improve performance).
I have created also StateInjector<T> class:
using System; using System.Collections.Generic; using System.Text; using System.Reflection; using System.Web.UI; namespace Devarchive_net { public class StateInjector<T> where T : BaseState, new() { #region Constructor public StateInjector(BaseUserControl control) { m_Control = control; m_State = InitState(); } #endregion #region Fields private T m_State = null; private BaseUserControl m_Control; private string m_JsStateVar; #endregion #region Properties public T State { get { return m_State; } set { m_State = value; } } public string StateID { get { return m_State.StateID; } } public string JsStateClass { get { return m_JsStateVar; } } #endregion #region Public methods public void InjectClientScript( string scriptClassName, string scriptPath, string scriptClassArgs ) { ScriptManager Smgr = ScriptManager.GetCurrent(m_Control.Page); if (Smgr == null) throw new Exception("ScriptManager not found."); ScriptReference SRef = new ScriptReference(); SRef.Path = scriptPath; Smgr.Scripts.Add(SRef); m_JsStateVar = String.Concat("S", m_State.StateID); if (!m_Control.Page.ClientScript.IsStartupScriptRegistered(m_Control.GetType(), String.Format("State_{0}", m_Control.ClientID))) { m_Control.Page.ClientScript.RegisterStartupScript( m_Control.GetType(), String.Format("State_{0}", m_Control.ClientID), String.Format( "var {0} = new {1}('{2}'{3});", m_JsStateVar, scriptClassName, m_Control.StateField.ClientID, scriptClassArgs ), true ); } } #endregion #region Private methods private T InitState() { m_Control.PreRender += new EventHandler(control_PreRender); if (m_State == null) { m_State = BaseState.Create<T>((m_Control.Request[m_Control.StateField.UniqueID] != null) ? m_Control.Request[m_Control.StateField.UniqueID] : m_Control.StateField.Value); if (m_State == null) { m_State = new T(); m_State.StateID = String.Concat("S", Guid.NewGuid().ToString().Replace("-", "")); } } return m_State; } #endregion #region Page events void control_PreRender(object sender, EventArgs e) { m_Control.StateField.Value = m_State.SerializedString(); } #endregion } }
The class is responsible for loading State object from string representation (this is done in constructor - so the initialization of the Injector should occur in OnInit event of the UserControl.
Generic type T in this class is giving the ability to provide custom state holder class to be persisted by Injector. The only constraint for generic type is that provided type must inherit BaseState class that was listed above. The initialization occurs in InitState method. It tries to instantiate state object from request using state field UniqueID, and if request does not contain the data yet(data was not posted yet), just creates new instance of state object.
Also it subscribes to OnPreRender event of the control, where it pushes serialized state object into Response (HiddenField's value property).
There is one helper public method named InjectClientScript, that helps UserControls to register custom client script that works with javascript-version of state object.
Now let's see ucCheckBoxListFilter UserControl itself. The control uses Injector class to create state and register custom script.
ucCheckBoxListFilter.ascx file:
<%@ Control EnableViewState="false" Language="C#" AutoEventWireup="true" CodeFile="ucCheckBoxListFilter.ascx.cs" Inherits="controls_Filters_ucCheckBoxListFilter" %> <div style="width: 500px; height: 300px; overflow: auto; border: solid 1px black;"> <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4" DataSourceID="ObjectDataSource1" ForeColor="#333333" GridLines="None" ShowHeader="False" EnableViewState="False" Width="480px" onrowdatabound="GridView1_RowDataBound"> <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" /> <RowStyle BackColor="#EFF3FB" /> <Columns> <asp:TemplateField> <ItemTemplate> <input type="checkbox" rowID="<%#Eval("ProductID")%>" onclick='<%#m_Injector.JsStateClass%>.UpdateState(this)' <%#(m_Injector.State.CheckBoxStates.ContainsKey(Eval("ProductID").ToString()))?(m_Injector.State.CheckBoxStates[Eval("ProductID").ToString()]?"checked":""):"" %> /> </ItemTemplate> </asp:TemplateField> <asp:BoundField DataField="ProductName" /> </Columns> <AlternatingRowStyle BackColor="White" /> </asp:GridView> </div> <br /> <asp:Literal ID="ltButtons" runat="server" EnableViewState="False"> </asp:Literal> <br /> <br /> <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /> <br /> <br /> <div style="width: 500px; height: 300px; overflow: auto; border: solid 1px black;"> <asp:UpdatePanel runat="server" UpdateMode="Conditional"> <Triggers> <asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" /> </Triggers> <ContentTemplate> <asp:Literal ID="ltPostResult" runat="server" EnableViewState="False"> </asp:Literal> </ContentTemplate> </asp:UpdatePanel> </div> <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" OldValuesParameterFormatString="original_{0}" SelectMethod="GetData" TypeName="DsNorthwindTableAdapters.ProductsTableAdapter"> </asp:ObjectDataSource>
The most important part of the file is :
<input type="checkbox" rowID="<%#Eval("ProductID")%>" onclick='<%#m_Injector.JsStateClass%>.UpdateState(this)' <%#(m_Injector.State.CheckBoxStates.ContainsKey(Eval("ProductID").ToString()))?(m_Injector.State.CheckBoxStates[Eval("ProductID").ToString()]?"checked":""):"" %> />
in this part I am accessing current state object and using it's JsStateClass poroperty, that points to the current instance of the javascript state-processing class.
also I use State property of an injector object, which returns me state object itself.
All this is done by using m_Injector variable that is defined in code behind file of control:
using System; using System.Web.UI.WebControls; using Devarchive_net; using System.Data; public partial class controls_Filters_ucCheckBoxListFilter : BaseUserControl { protected StateInjector<CheckBoxListState> m_Injector; protected override void OnInit(EventArgs e) { // execute base OnInit first base.OnInit(e); // Now state field is created so inject state functionality m_Injector = new StateInjector<CheckBoxListState>(this); m_Injector.InjectClientScript( "CheckBoxListFilterState", "~/Controls/Filters/CheckBoxListFilterState.js", String.Format(",'{0}'",GridView1.ClientID) ); } protected void Page_Load(object sender, EventArgs e) { ltPostResult.Text = ""; Button1.OnClientClick = String.Format("{0}.UpdateStateField();", m_Injector.JsStateClass); ltButtons.Text = GetTemplate( "~/Controls/Filters/HtmlTemplates/CheckBoxListFilterButtons.htm", "All", String.Format("{0}.CheckAll();",m_Injector.JsStateClass), "None", String.Format("{0}.UnckeckAll();", m_Injector.JsStateClass), "Invert", String.Format("{0}.InvertAll();", m_Injector.JsStateClass) ); } protected void Button1_Click(object sender, EventArgs e) { foreach (string key in m_Injector.State.CheckBoxStates.Keys) { if (m_Injector.State.CheckBoxStates[key]) { ltPostResult.Text += String.Format("checkbox for rowId={0} is checked!<br />", key); } } } protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e) { if (!Page.IsPostBack && e.Row.RowType == DataControlRowType.DataRow) { DataRowView drv = e.Row.DataItem as DataRowView; if (drv != null) { if (!m_Injector.State.CheckBoxStates.ContainsKey(drv["ProductID"].ToString())) m_Injector.State.CheckBoxStates.Add(drv["ProductID"].ToString(), false); } } } }
I hope code is readable - it's not hard at all to use the API I created.
Initialization of state object is done in OnInit page event, There I create an instance of Injector<T> class (which creates State object in constructor), and call
m_Injector.InjectClientScript( "CheckBoxListFilterState", "~/Controls/Filters/CheckBoxListFilterState.js", String.Format(",'{0}'",GridView1.ClientID) );
method, the method links custom js file to the page, and also creates javascript initialization statement for state-processing js class
Let's see the CheckBoxListFilterState.js file itself, it defines javascript class for working with state object on client side:
function CheckBoxListFilterState() { // this is path to GridView control this.stateField = arguments[0]; this.controlPath = arguments[1]; if(this.stateField) this.state = Sys.Serialization.JavaScriptSerializer.deserialize( $get(this.stateField).value ); // Checks all checkboxes in the grid this.CheckAll = function() { this.__IterateOverRows(true, false); this.UpdateStateField(); }; // Unchecks all checkboxes in the grid this.UnckeckAll = function() { this.__IterateOverRows(false, false); this.UpdateStateField(); }; // Inverts checked state for all checkboxes in the grid this.InvertAll = function() { this.__IterateOverRows(false, true); this.UpdateStateField(); }; // Updates state for single checkbox this.UpdateState = function(chk) { if(this.state) { this.state.CheckBoxStates[chk.attributes.getNamedItem("rowID").value+""] = chk.checked; } }; // Serializes state into hidden field this.UpdateStateField = function() { $get(this.stateField).value = Sys.Serialization.JavaScriptSerializer.serialize( this.state ); } this.__IterateOverRows = function(checked, invert) { var grid = $get(this.controlPath); var row; var cell; var i=0;var j=0; for(i=0; i<grid.rows.length; i++) { row = grid.rows[i]; cell = row.cells[0]; var chkChecked = cell.firstChild; while(chkChecked) { if(chkChecked.tagName=="INPUT") { if (invert) { chkChecked.checked = !chkChecked.checked; } else { chkChecked.checked = checked; } if( this.state && chkChecked.attributes && chkChecked.attributes.getNamedItem("rowID") ) { this.state.CheckBoxStates[chkChecked.attributes.getNamedItem("rowID").value+""] = chkChecked.checked; } break; } chkChecked = chkChecked.nextSibling; } } } }
You can see here how we serialize/deserialize state object and how we access it's properties. The structure is the same as we defined it in c# class!
Of course we could do the functionality shown in the project just by using server-side checkboxes and enumerating through them in c# code to find out which ones are checked. The real world shows that this approach does not work always. For example suppose we have a 10-level hierarchy of user controls, or more. In this case server side checkboxes, and any server side control generates very long ID and Name for html control, this is a big problem, another problem is persisting state in such large hierarchies. Server controls use ViewState. Why store the state that we don't need ? Or why pass 200 byte length form variable names?
With this approach I can store only things I really need, and get an additional "bonus" :) - we can "see" state object from the client, and we can even modify it from client script. In the age of RIA this is really an advantage. The state object like this helps split UI into two tiers that work independently on two machines - server and user, but the same time with the same object!
Don't forget about security. The state is visible in browser, this means we should check all incoming values with care.
Please post what you think.
2 comments:
nice work, thanks.
masoud
It was very interesting for me to read this post. Thanks for it. I like such topics and anything connected to them. I would like to read a bit more soon.
Post a Comment