Saturday, January 26, 2008

Passing complex objects between client browser and web server using ASP.NET AJAX framework

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:

image 

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.

Technorati Tags:

Saturday, January 19, 2008

Loading User Controls Using ASP.NET AJAX Page Methods

In this post I am loading ASP.NET user controls to running page using AJAX page methods.

The sample code makes easy to load UserControls from server to the calling page.

Also it loads the associated css stylesheet file url (if needed by the control) and adds new LINK object to the header of the page, so that page loads the stylesheet file (there is a method to unload LINK object from header back.

Also the sample loads any custom javascript code (if needed by the control), and executes it right after getting it from the server.

Here are screen shots for completed sample.

Loading indicator during page method call:

image

Loaded, rendered custom style, and executed custom script for the control:

image

"Next" button of control in turn calls the server for the second user control. This way we get a wizard :)

image

Here we have two functional buttons again - "Back" button loads from the server previous control, "Next" button - the next one.

image

I think you got the idea, used with Windows Workflow Foundation this approach can do a lot of good things :). The next three pages:

image

image

image

 

You can see live example, or download source code.

 

Please review my previous post, where I am explaining how Page method can be called by method name using reflection. I used that approach in this sample.

Also look at the post, where I am explaining how we can control AjaxControlToolkit ModalPopup control from javascript code.

I use some more interesting tricks in the sample, - code generation project, to generate navigation class using code generation templates engine inside VS 2008. See more about this in my previous post.

I use in sample code some new features of C# 3.0, - collection initializers and lambda expressions.

 

There is a lot of code in the sample, but I will try to list only most important parts of it. You can review full source code by downloading it.

1) How UserControls are organized:

image

Note, none of them are loaded into main page's control collection, they are just rendered during page method requests

2) How client side button requests the UserControl:

<input 
    value="Load user control!" 
    type="button" 
    onclick="mainScreen.LoadServerControlHtml(
                'Welcome',
                {'pageID':1,'data':null}, 
                'methodHandlers.BeginRecieve');" 
    />

mainScreen.LoadServerControlHtml javascript function:

mainScreen.LoadServerControlHtml = function(_title, _obj, _callback) {
    /// <summary>
    /// Loads Server user control to the modal dialog 
    /// </summary>
    /// <param name="_title">Title of modal popup</param>
    /// <param name="_obj">
    /// object that we pass to the server
    /// </param>
    mainScreen.ShowModal(
        _title,
        (mainScreen.activityImgObj)
        ?
        ("<center><img src='" + mainScreen.activityImgObj.src + "' /></center>")
        :
        ""
        );
    mainScreen.ExecuteCommand(
        'GetWizardPage', 
        _callback, 
        _obj
        );
}

This function shows modal dialog with progress image inside it and calls another js function which calls server page method

3) Server-side page method uses reflection to call 'GetWizardPage' method in dedicated command class:

/// <summary>
/// returns rendered control's string representation.
/// object "data" should be passed from javascript method 
/// as array of objects consisting of two objects,
/// first - pageID - integer identificator by which we will
/// lookup real control path; second object may be some data
/// that the control needs.
/// </summary>
public object GetWizardPage(object data)
{
    bool errorLogged = false;
    try
    {
        Dictionary<string, object> param =
            (Dictionary<string, object>)data;
        int pageID = (int)param["pageID"];
        object customData = param["data"];

        string controlPath = 
            m_NavigationData.Find(x => x.Key == pageID).Value;

        if (!String.IsNullOrEmpty(controlPath))
        {
            if(
                controlPath.ToLower()
                .EndsWith(".htm") 
                ||
                controlPath.ToLower()
                .EndsWith(".html") 
                ||
                controlPath.ToLower()
                .EndsWith(".txt"))
            {
                string result = "";
                using (
                        TextReader tr = 
                            new StreamReader(
                                HttpContext.Current.Server.MapPath(controlPath)
                                )
                            )
                {
                    result = tr.ReadToEnd();
                }
                return new ContentsResponse(result, string.Empty, string.Empty);
            }
            else
            {
                return TemplateViewManager.RenderView(controlPath, customData);
            }
        }
    }
    catch (Exception ex)
    {
        // Log error
        errorLogged = true;
    }
    if (!errorLogged)
    {
        // Log custom error saying 
        // we did not find the page
    }
    return ContentsResponse.Empty;
}

m_NavigationData is static variable, we use collection initializer to assign it a value right in the place of declaration:

private static List<KeyValuePair<int, string>>
            m_NavigationData = new List<KeyValuePair<int, string>>()
            {
                new KeyValuePair<int, string>(1,Pages.Controls.Welcome),
                new KeyValuePair<int, string>(2,Pages.Controls.AcceptLicenceAgreement),
                new KeyValuePair<int, string>(3,Pages.Controls.PleaseClickProceedButton),
                new KeyValuePair<int, string>(4,Pages.Controls.ConfirmationMessage),
                new KeyValuePair<int, string>(5,Pages.Controls.Installing),
                new KeyValuePair<int, string>(6,Pages.Controls.ThankYouMessage)
            };

ContentsResponse object looks like:

namespace Devarchive_net
{
    public class ContentsResponse
    {
        public ContentsResponse(string _html, string _script, string _customStyle)
        {
            html = _html;
            script = _script;
            customStyle = _customStyle;
        }

        public static ContentsResponse Empty
        {
            get
            {
                return new ContentsResponse(string.Empty, string.Empty, string.Empty);
            }
        }

        public string html = "";
        public string script = "";
        public string customStyle = "";
    }
}

this class is returned to the browser.

As you see page method reads the requested file right from disk as text file and returns it - if the extension for the page is htm, html or txt.

If the extension is ascx, it renders the control using TemplateViewManager.RenderView static method :

using System.IO;
using System.Web;

namespace Devarchive_net
{

    public class TemplateViewManager
    {
        public static ContentsResponse RenderView(string path)
        {
            return RenderView(path, null);
        }

        public static ContentsResponse RenderView(string path, object data)
        {
            TemplatePage pageHolder = new TemplatePage();
            TemplateUserControl viewControl = 
                (TemplateUserControl)pageHolder.LoadControl(path);

            if (viewControl == null)
                return ContentsResponse.Empty;

            if (data != null)
            {
                viewControl.Data = data;
            }

            pageHolder.Controls.Add(viewControl);

            string result = "";
            using (StringWriter output = new StringWriter())
            {
                HttpContext.Current.Server.Execute(pageHolder, output, false);
                result = output.ToString();
            }

            return new ContentsResponse(
                    result, 
                    viewControl.StartupScript, 
                    viewControl.CustomStyleSheet
                    );
        }
    }
}

Here is the trick with stylesheets and custom script. All user controls that may be requested should derive from TemplateUserControl class. This class defines the members for custom script and custom stylesheet:

using System.Web.UI;

namespace Devarchive_net
{
    public class TemplateUserControl : UserControl
    {
        private object m_Data = null;

        public object Data
        {
            get { return m_Data; }
            set { m_Data = value; }
        }

        private string m_StartupScript = string.Empty;

        public string StartupScript
        {
            get { return m_StartupScript; }
            set { m_StartupScript = value; }
        }

        private string m_CustomStyleSheet = string.Empty;

        public string CustomStyleSheet
        {
            get { return m_CustomStyleSheet; }
            set { m_CustomStyleSheet = value; }
        }
    }
}

Also there is a TemplatePage class, this class is derived from Page class. The instance of this class is created when rendering UserControl to get rendered HTML.

Here is it:

using System.Web.UI;

namespace Devarchive_net
{
    public class TemplatePage : Page
    {
        public override void VerifyRenderingInServerForm(Control control) { }
    }
}

Next...

4) Browser gets back a response from the server in callback function we specified earlier:

methodHandlers.BeginRecieve = function(_result) {
    /// <summary>
    /// method that shows result from 
    /// page method "GetWizardPage"
    /// </summary>
    var res = false;
    if(_result.customStyle && _result.customStyle!="") {
        mainScreen.LoadStyleSheet(_result.customStyle);
    }
    if(_result.html && _result.html!="") {
        mainScreen.mainModalContentsDiv.innerHTML = _result.html;
        res = true;
    }
    if(_result.script && _result.script!="") {
        eval(_result.script);
    }
    if(!res) {
        mainScreen.CancelModal();
    } else {
        mainScreen.mainModalExtender._layout();
        setTimeout('mainScreen.mainModalExtender._layout()', 3000);
    }
};

It loads CSS files, then contents, and finally executes custom script if any  was returned from the server.

I think you will be also interested with mainScreen.LoadStyleSheet method, here is it:

mainScreen.LoadStyleSheet = function(_path) {
    if(!this.styleSheets[_path]) {
        var styleSheet;
        styleSheet=document.createElement('link');
        styleSheet.type="text/css";
        styleSheet.rel='stylesheet';
        styleSheet.href = _path;
        document.getElementsByTagName("head")[0].appendChild(styleSheet);
        this.styleSheets[_path] = styleSheet;
    }
};

Also there is another method, that is called when modal dialog is closed :

mainScreen.CancelModal = function() {
    /// <summary>
    /// Hides modal dialog 
    /// </summary>
    this.mainModalExtender.hide();
    var _path;
    for(_path in this.styleSheets) {
        document.getElementsByTagName("head")[0].removeChild(this.styleSheets[_path]);
        delete this.styleSheets[_path];
    }
};
This method removes any links to custom stylesheet files to avoid design issues in future calls to server.

 

Lets return to the server code, and see what UserControls itself contain inside, and what the can do.

Lets see Wecome.ascx file:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Welcome.ascx.cs" Inherits="Controls_Welcome" %>
<div class="div">
    <table class="tblStyle">
        <tr>
            <td class="tdLeftUpper">
                <img 
                    alt="Setup" src="Controls/Style/LeftImg.jpg" 
                    style="width: 162px; height: 311px" />
            </td>
            <td class="tdRightUpper">
                <p class="largeText" style="white-space:normal">
                Welcome to the Microsoft ASP.NET AJAX 
                Extentions Source Code Setup Wizard<br />
                </p>
                <br />
                <br />
                <p class="smallText" style="white-space:normal">
                The Setup Wizard allows you to change 
                the way Microsoft ASP.NET AJAX Extensions 
                Source 
                <span 
                    class="customStyleSheetClass">
                    Code
                </span> 
                features are installed on 
                your computer or to remove it from your 
                computer. Click Next to continue or Cancel 
                to exit Setup Wizard.
                </p>
                <div 
                    style="border:solid 1px black;" 
                    class="smallText" 
                    id="custSpan">
                </div>
            </td>
        </tr>
        <tr>
            <td colspan="2" class="tdLower">
                <input
                    type="button"
                    class="wizBtn wizBtnBack"
                    value="Back"
                    onclick=""
                    disabled="disabled"
                    />
                &nbsp;
                <input
                    type="button"
                    class="wizBtnR wizBtnNext"
                    value="Next"
                    onclick="
                       mainScreen.LoadServerControlHtml(
                        'License Agreement',
                        {'pageID':2,'data':null}, 
                        'methodHandlers.BeginRecieve'); 
                    "
                    />
                &nbsp;
                &nbsp;
                &nbsp;
                &nbsp;
                <input
                    type="button"
                    class="wizBtn wizBtnClose"
                    value="Cancel"
                    onclick="mainScreen.CancelModal();"
                    />
            </td>
        </tr>
    </table>
</div>

Note, we use all client side controls, but - you can use any server control you like ! You can use GridView etc.

Code behind for Welcome.ascx:

using System;
using Devarchive_net;

public partial class Controls_Welcome : TemplateUserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.StartupScript =
            "$get('custSpan').innerHTML=" +
            "'Note - you will read this message "+
            "because we passed custom javascript code from "+
            "c# code!" +
            "The word Code is red because of custom stylesheet!'" +
            ";" 
            ;
        this.CustomStyleSheet =
            String.Format(
            "Controls/Style/Welcome.css?{0}"
            ,
            Guid.NewGuid().ToString()
            );
    }
}

here I assign custom javascript that should be executed in browser after call. You can move this into separate file and read it from c# to make separation of different languages clearer.

Also I assign a stylesheet url to the CustomStyleSheet variable.

 

One more thing - I wrote the code in the way we can pass some custom value from browser when requesting the user control.

If you remember we requested control from javascript like this: 

{'pageID':2,'data':true}

here the second parameter is custom data, in this case it is just a boolean value, but we can pass array, dictionary, and more.

In User control we can analyze this value, to return back some specific view. For example open PleaseClickProceedButton.ascx file. There you can find line:

<%=(Data!=null && (bool)Data)?"checked=\"checked\"":"" %>

You can see how we parse the data passed from client browser.

I use this architecture in my current project. It is just great ! speed is much higher comparing to the page with UpdatePanels, Page methods are much faster, they don't postback any form data, as UpdatePanel does. Besides this, page method executes static method - comparing to UpdatePanel that executes whole page controls collection life cycle. Just use this carefully, if you need lot of form data and a small number of dynamically loaded user controls, use UpdatePanel.

Ok, just see it live !

Please post some comments, I need feedback ! :)

Thursday, January 17, 2008

Using AjaxControlToolkit ModalPopup Control to Show Confirmation Dialog Instead of Javascript "confirm" Popup Box

In previous blog post we created simple popup box behaving like javascript "alert" function.

This time I want to create universal confirm dialog. I suppose it will work like this:

1) Dialog should be shown from client script. We should be able to call it with javascript function.

2) Javascript function will take three parameters: button object that invoked the function, title - text that will appear as title of confirmation dialog, and html - text tat will appear inside popup dialog - html parameter will be the question or warning that the user will see inside confirmation box.

3) Confirmation dialog will have two buttons - Yes and No (OK/Cancel). Clicking on "No" will do nothing - confirmation dialog will just hide. Clicking "Yes" will post back the button that invoked confirmation dialog to the server, and the server will process event for that button.

The completed demo looks like:

image

You can see it online or download source code.

The trick with posting back a corresponding button works simple. When invoking javascript function responsible for showing dialog, we pass instance of a button object that invoked the function. This way we know the button.name property, and later we can post back the value to the server if user clicked "Yes".

To accomplish this task I created the following files:

Script.js:

/// --------------------------------------------------
/// mainScreen object
/// --------------------------------------------------
var mainScreen =
{
    mainModalExtender : null,           // modalExtender object on main page
    mainModalTitleSpan : null,          // title span object
    mainModalContentsDiv : null,        // div inside modal dialog
    pageTheme : "Default"
}

mainScreen.Init = function() {
    /// <summary>
    /// Initializes mainScreen variables
    /// </summary>
    this.mainModalExtender = $find('mbMain');
    this.mainModalTitleSpan = $get("spanTitle");
    this.mainModalContentsDiv = $get("mainModalContents");
};
mainScreen.ShowConfirm = function(_button,_title, _html) {
    /// <summary>
    /// Shows modal dialog with contents equal to _html
    /// </summary>
    /// <param name="_button">Button object</param>
    /// <param name="_title">Title of modal popup</param>
    /// <param name="_html">HTML that should be shown inside popup</param>
    this.currentButtonUID = _button.name
    this.mainModalTitleSpan.innerHTML = _title;
    this.mainModalContentsDiv.innerHTML = _html;
    this.mainModalExtender.show();
};
mainScreen.CancelConfirm = function() {
    /// <summary>
    /// Hides modal dialog 
    /// </summary>
    this.mainModalExtender.hide();
    this.currentButtonUID = null;
};
mainScreen.SubmitConfirm = function() {
    /// <summary>
    /// Hides modal dialog 
    /// </summary>
    if(this.currentButtonUID) {
        __doPostBack(this.currentButtonUID, "");
    }
    this.mainModalExtender.hide();
    this.currentButtonUID = null;
};


/// --------------------------------------------------
/// Page events processing
/// --------------------------------------------------

Sys.Application.add_load(
    applicationLoadHandler
    );
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(
    endRequestHandler
    );
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(
    beginRequestHandler
    );

function applicationLoadHandler() {
    /// <summary>
    /// Raised after all scripts have been loaded and 
    /// the objects in the application have been created 
    /// and initialized.
    /// </summary>
    mainScreen.Init()
}

function endRequestHandler() {
    /// <summary>
    /// Raised before processing of an asynchronous 
    /// postback starts and the postback request is 
    /// sent to the server.
    /// </summary>
    
    // TODO: Add your custom processing for event
}

function beginRequestHandler() {
    /// <summary>
    /// Raised after an asynchronous postback is 
    /// finished and control has been returned 
    /// to the browser.
    /// </summary>

    $get("resultDiv").innerHTML = 
        "<img src='App_Themes/"+
        mainScreen.pageTheme+
        "/Img/activity_small.gif'/>";
}

Default.aspx:

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

<%@ 
    Register 
    assembly="AjaxControlToolkit" 
    namespace="AjaxControlToolkit" 
    tagprefix="AjaxToolkit" %>

<!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>
        Using AjaxControlToolkit ModalPopup Control 
        to Show Confirmation Dialog Instead 
        of Javascript "confirm" Popup Box
    </title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager 
        EnablePageMethods="true" 
        ID="MainSM" 
        runat="server" 
        ScriptMode="Release"
        LoadScriptsBeforeUI="true">
        <Scripts>
            <asp:ScriptReference 
                Path="~/Scripts/Main.js" />
        </Scripts>
    </asp:ScriptManager>
    <div>
        This sample shows confirm dialog 
        when you click on any button on the page.
        If you confirm the query, 
        original button you clicked 
        will be posted back to the server
        using __doPostBack('button.UniqueID','') method.
        So you can process event as normally in the code behind code.
        <br />
        <br />
        <asp:UpdatePanel 
            runat="server" 
            ID="up"
            UpdateMode="Conditional"
            >
            <ContentTemplate>
                <div>
                    <asp:Button
                        runat="server"
                        ID="btn1"
                        Text="btn1" 
                        onclick="btn1_Click"
                        OnClientClick="
                        mainScreen.ShowConfirm(
                            this,
                            'Please confirm',
                            'Are you sure to submit &lt;b&gt;btn1&lt;/b&gt;?'
                        );
                        return false;"
                        />
                    &nbsp;
                    <asp:Button
                        runat="server"
                        ID="btn2"
                        Text="btn2" 
                        onclick="btn2_Click"
                        OnClientClick="
                        mainScreen.ShowConfirm(
                            this,
                            'Please confirm',
                            'Are you sure to submit &lt;b&gt;btn2&lt;/b&gt;?'
                        );
                        return false;"
                        />
                    &nbsp;
                    <asp:Button
                        runat="server"
                        ID="btn3"
                        Text="btn3" 
                        onclick="btn3_Click"
                        OnClientClick="
                        mainScreen.ShowConfirm(
                            this,
                            'Please confirm',
                            'Are you sure to submit &lt;b&gt;btn3&lt;/b&gt;?'
                        );
                        return false;"
                        />
                    &nbsp;
                    <asp:Button
                        runat="server"
                        ID="btn4"
                        Text="btn4" 
                        onclick="btn4_Click"
                        OnClientClick="
                        mainScreen.ShowConfirm(
                            this,
                            'Please confirm',
                            'Are you sure to submit &lt;b&gt;btn4&lt;/b&gt;?'
                        );
                        return false;"
                        />
                    &nbsp;
                    <!-- 
                        We set UseSubmitBehavior="false"
                        to have __doPostBack function present
                        in the scope of the page
                     -->
                    <asp:Button
                        UseSubmitBehavior="false"
                        runat="server"
                        ID="btn5"
                        Text="btn5" 
                        onclick="btn5_Click"
                        OnClientClick="
                        mainScreen.ShowConfirm(
                            this,
                            'Please confirm',
                            'Are you sure to submit &lt;b&gt;btn5&lt;/b&gt;?'
                        );
                        return false;"
                        />
                    <br /><br />
                </div>
                <div 
                    style="border: dashed 1px black; 
                        font-weight:bold;
                        height:20px;" 
                    id="resultDiv"
                    runat="server"
                    >
                    &nbsp;
                </div>
            </ContentTemplate>
        </asp:UpdatePanel>
        <br /><br />
        <br />
        <br />
        <asp:Panel 
            ID="MPanel" 
            runat="server" 
            Style="display: none">
            <table 
                class="mainModalTable" 
                cellpadding="0" 
                cellspacing="0">
                <tr>
                    <td 
                        class="mainModaTableTD">
                        <table 
                            class="mainModalInnerTable" 
                            cellspacing="0" 
                            cellpadding="0">
                            <tr>
                                <td 
                                    class="mainModalInnerTableTD">
                                    <table 
                                        border="0" 
                                        width="100%" 
                                        cellspacing="0" 
                                        cellpadding="4">
                                        <tr>
                                            <td 
                                                class="mainModalDraggablePanelTD">
                                                <asp:Panel 
                                                    CssClass="mainModalDraggablePanel" 
                                                    ID="MPD" runat="server">
                                                    <span 
                                                        class="mainModalTitle" 
                                                        id="spanTitle" 
                                                        Text="">
                                                    </span>
                                                </asp:Panel>
                                            </td>
                                            <td 
                                                align="right" 
                                                class="mainModalDraggablePanelCloseTD">
                                                    <asp:ImageButton 
                                                        SkinID="CloseModal" 
                                                        runat="server" 
                                                        ID="clB" />
                                            </td>
                                        </tr>
                                        <tr>
                                            <td 
                                            style="text-align:center"
                                                class="mainModalContentsTD" 
                                                colspan="2">
                                                <div 
                                                    id="mainModalContents">
                                                </div>
                                            </td>
                                        </tr>
                                        <tr>
                                            <td 
                                                class="mainModalContentsTD" 
                                                align="center"
                                                colspan="2">
                                                <input
                                                    type="button"
                                                    value="Yes"
                                                    onclick="mainScreen.SubmitConfirm();"
                                                    class="btn btnYes"
                                                    />
                                                &nbsp;
                                                <input
                                                    type="button"
                                                    value="No"
                                                    onclick="mainScreen.CancelConfirm();"
                                                    class="btn btnCancel"
                                                    />
                                            </td>
                                        </tr>
                                    </table>
                                    </td>
                            </tr>
                        </table>
                    </td>
                </tr>
            </table>
        </asp:Panel>
        <AjaxToolkit:ModalPopupExtender 
            ID="mpeModal" 
            runat="server" 
            PopupControlID="MPanel"
            TargetControlID="btnHid"
            BehaviorID="mbMain"
            BackgroundCssClass="modalBackground" 
            CancelControlID="clB"
            OnCancelScript="mainScreen.CancelConfirm();" />
        <asp:Button 
            runat="server" 
            ID="btnHid" 
            style="display:none;" />    
    </div>
    </form>
</body>
</html>

and CSS files.

Hope this helps.