Jon Galloway had an interesting post over the weekend regarding Getting JavaScript and ASP.NET talking (outside of AJAX). The post discusses a problem Jon was having in getting server variables to be accessible in client-side JavaScript code and his solution as part of a control using the ASP.NET AJAX IScriptControl interface.

In the comments I remarked that this process seems awfully complex to do an essentially simple thing: Embed script code into a page. However,the problem addressed in the post is a real one - but one that I think can be solved with a more generic and maybe possibly easier solution.

So to reiterate, the problem at hand is: How do you effectively get server variables published into the client page and accessible from JavaScript? The reality is: It's not easy!

The easiest solution might seem to be using <%= %> expressions to embed values into script, but it's really not quite that simple because .NET values - especially strings and dates - need to be properly formatted in order to work correctly as JavaScript values. So writing out a string as:

var Company = '<%= Customer.Company %>';

is actually not good enough. For example if the string contains a single quote the code will fail. You can use double quotes but then a double quote will fail instead. Because the string is embedded into script as a literal the string also needs to be properly escaped. So quotes, slashes, and escape characters have to be marked up as well. Using a plain <%= %> tag is clearly not going to cut it. Some time ago I posted a JavaScript string encoding function that deals with strings, but this still leaves you with the ugly solution of script tags and global variables you have to declare inside script code.

There's also a problem if you need access to the variables in .js files - you can't embed script tags into a .js file so getting a dynamic value into a static file is problematic (although my workaround below would also work with <%= %> tags in the main page).

Anyway, after reading Jon's post I thought that it would be nice to have a generic tool to 'publish' server variables to the client. So I spent a couple hours last night to create a very easy to use class that does the following:

  • Adds key/value pairs to a statebag that gets rendered automatically
  • Dynamically adds any object/control property that gets rendered automatically
  • Keys are rendered as properties of an JavaScript object with the static or dynamic values applied

To use the class inside of an ASP.NET Page this code to use this class looks like this:

protected void Page_Load(object sender, EventArgs e)
{
    wwScriptVariables scriptVars = new wwScriptVariables();
 
    // *** Add any values static or dynamic
    scriptVars.Add("name", "West Wind Technologies");
    scriptVars.Add("entered", DateTime.Now);
    scriptVars.Add("counter",12.22M);
 
    // *** A cleaner way to get ClientIDs into client code?
    scriptVars.Add("txtNameId", txtName.ClientID);
    scriptVars.Add("btnSubmitId", btnSubmit.ClientID);
 
    // *** Add a control's value
    scriptVars.AddDynamicValue("txtName", this.txtName, "Text");
 
    // *** Done
}

This code adds several values to the ScriptVariables object which hold on to these values and then generates a client side object with a name specified in the constructor (or the default which is 'serverVars'). The AddDynamicValue method adds a property that is essentially mapped to a property of a control or object. You can basically assign this value anywhere and still ensure that final rendered value will be embedded into the script object. This is useful so that you can keep all the assignment code in one place putting both static and dynamic values into a single method rather than scattering calls into various page event methods (although you can still do that if you choose).

The class then renders a JavaScript object into the page with each of the key values as property names. For the definition above you'd end up with a Client Script Block in the page:

<script type="text/javascript">
//<![CDATA[
var serverVars = {
    "name": "West Wind Technologies",
    "entered": new Date(1202809550345),
    "counter": 12.22,
    "txtNameId": "txtName",
    "btnSubmitId": "btnSubmit",
    "txtName": ""
}
//]]>
</script>

The script is added a ClientScriptBlock which is rendered at the top of that page, which also means it's visible embedded script files (.js files). Now from anywhere you can access these variables via the static object instance:

var name = serverVars.name;
var entered = serverVars.entered;
var counter = serverVars.counter;

And using a control Id that might from within a master page or other naming container in script:

var txtName = document.getElementById(serverVars.txtNameId);
var name = jQuery("#" + serverVars.txtNameId).css("border","solid 1px red");

which surely beats using <%= %> script tags inside of the script code.

Note that you can specify the name of the defined JavaScript variable by changing ClientObjectName or passing in the name in the constructor, so you can have multiple separate server object variables embedded into the page. This makes this work for control developers as well, as long as you can come up with a unique name for the control.

This approach can also be useful for localization. Because the code is generated on the server, you can assign values with HttpContext.GetLocalResourceObject() or HttpContext.GetGlobalResourceObject() to assign values or - if you're binding a control value - get the localized control value at render time.

Values that are assigned are encoded using JSON serialization to ensure they are 'literal' values that evaluate. One very cool side effect of this is that you can actually embed fairly complex objects in addition to simple types. For example, you can directly serialize an object, a DataSet/DataTable/DataRow, an array, list or enumerable type.

I'm using the JSONSerializer from the West Wind Ajax Toolkit in the control (because it supports a couple of date formatting options) but you can also use the System.Web.Extensions.JavaScriptSerializer along the same lines.

Here's my first cut at this class:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web;
using System.Reflection;
 
namespace Westwind.Web.Controls.Controls
{
    /// <summary>
    /// Provides an easy way for server code to publish strings into client script code.
    /// This object basically provides a mechanism for adding key value pairs and embedding
    /// those values into an object that is hosted on the client.        
    /// </summary>
    public class wwScriptVariables
    {
 
        /// <summary>
        /// Internally holds all script variables declared
        /// </summary>
        Dictionary<string, object> ScriptVariables = new Dictionary<string,object>();
 
 
        /// <summary>
        /// Internally tracked reference to the Page object
        /// </summary>
        Page Page = null;
 
 
        /// <summary>
        /// The name of the object generated in client script code
        /// </summary>
        public string ClientObjectName
        {
            get { return _ClientObjectName; }
            set { _ClientObjectName = value; }
        }
        private string _ClientObjectName = "serverVars";
 
        /// <summary>
        /// Determines whether the output object script is rendered
        /// automatically as part of Page PreRenderComplete. If false
        /// you can manually call the RenderClientScript() method to
        /// retrieve the script and embed it yourself.
        /// </summary>        
        public bool AutoRenderClientScript
        {
            get { return _AutoRenderClientScript; }
            set { _AutoRenderClientScript = value; }
        }
        private bool _AutoRenderClientScript = true;
 
 
        /// <summary>
        /// Full constructor that receives an instance of any control object
        /// and the client name of the generated script object that contains
        /// the specified properties.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="clientObjectName"></param>
        public wwScriptVariables(Control control, string clientObjectName) 
        {
            if (control == null)
                // Note: this will fail if called from Page Contstructor
                //       ie. wwScriptVariables scripVars = new wwScriptVariables();
                this.Page = HttpContext.Current.Handler as Page;
            else
                this.Page = control.Page;
 
            if (this.Page == null)
                throw new ArgumentException("Could not retrieve a Page instance in wwScriptVariables.\r\n Either provide a Control or Page reference to the wwScriptVariables constructor or intialize the class in OnInit() of the page or later.");
 
            if (!string.IsNullOrEmpty(clientObjectName))
                this.ClientObjectName = clientObjectName;
 
            // *** Force RenderClientScript to be called before the page renders
            this.Page.PreRenderComplete +=new EventHandler(Page_PreRenderComplete);
        }
        /// <summary>
        /// This constructor only takes an instance of a Control. The name of the
        /// client object will be serverVars.
        /// </summary>
        /// <param name="control"></param>
        public wwScriptVariables(Control control) : this(control,"serverVars")
        {            
        }
        /// <summary>
        /// This constructor can only be called AFTER a page instance has been created.
        /// This means OnInit() or later, but not in the constructor of the page.
        /// 
        /// The name of the client object will be serverVars.
        /// </summary>
        public wwScriptVariables() : this(null, "serverVars")
        { 
        }            
 
        private void Page_PreRenderComplete(object sender, EventArgs e)
        {
            this.RenderClientScript();
        }
 
        public void Add(string variableName, object value)
        {
            this.ScriptVariables.Add(variableName, value);
        }
 
        /// <summary>
        /// Adds the dynamic value of a control or any object's property
        /// that is picked up just before rendering. 
        /// 
        /// This allows you to specify a given control or object's value to 
        /// added to the client object with the specified property value 
        /// set on the JavaScript object and have that value be picked
        /// up just before rendering. This is useful so you can do all
        /// client object declarations up front in one place regardless
        /// of where the values are actually set.
        /// 
        /// Dynamic values are retrieved with Reflection so this method
        /// is necessarily slower than direct assignment.
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="control"></param>
        /// <param name="property"></param>
        public void AddDynamicValue(string variableName, object control, string property)
        {
            // *** Special key syntax: .varName.Property syntax to be picked up by parser
            this.ScriptVariables.Add("." + variableName + "." + property, control);            
        }
 
        /// <summary>
        /// Explicitly forces the client script to be rendered into the page.
        /// This code is called automatically by the configured event handler that
        /// is hooked to Page_PreRenderComplete
        /// </summary>
        private void RenderClientScript()
        {            
            if (!this.AutoRenderClientScript || this.ScriptVariables.Count == 0)
                return;
 
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("var " + this.ClientObjectName + " = {");
 
            // *** We'll serialize single values into the client
            JSONSerializer ser = new JSONSerializer();
            ser.SerializeDateAsString = false// use new Date() output
 
            foreach(KeyValuePair<string,object> entry in this.ScriptVariables)
            {
                if (entry.Key.StartsWith("."))
                {
                    // *** It's a dynamic key
                    string[] tokens = entry.Key.Split(new char[1] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                    string varName = tokens[0];
                    string property = tokens[1];
 
 
                    object propertyValue = null;
                    if (entry.Value != null)
                        propertyValue = entry.Value.GetType().
                                    GetProperty(property, BindingFlags.Instance | BindingFlags.Public).
                                    GetValue(entry.Value, null);  
 
                    sb.AppendLine("\t\"" + varName + "\": " + ser.Serialize(propertyValue) + ",");                
                }
                else
                    sb.AppendLine("\t\"" + entry.Key + "\": " + ser.Serialize(entry.Value) + ",");
            }
 
            // *** Strip off last comma plus CRLF
            if (sb.Length > 0)
                sb.Length -= 3;                
 
            sb.AppendLine("\r\n}");
 
            if (this.Page == null)
                this.Page = HttpContext.Current.Handler as Page;
 
            // *** Use ClientScriptProxy from West Wind Ajax Toolkit to be MS Ajax compatible - otherwise use ClientScript
            ClientScriptProxy.Current.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
 
            //this.Page.ClientScript.RegisterClientScriptBlock(typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
        }
    }
}

The code has a couple of dependencies that are part of the West Wind Ajax Toolkit. ClientScriptProxy is used to manage client script as a front end to ScriptManager or the Page.ClientScript object depending on availability of the ScriptManager. The JSONSerializer is used here rather than JavaScriptSerializer from System.Web.Extensions because of the obvious System.Web.Extensions dependency as well as a problem with Date formatting by JavaScriptSerializer which uses encode date strings (ie."\/Date(1198874600025)\/") which doesn't actually turn into a date value. JSONSerializer can optionally output the required new Date() sytnax to produce a live date (this may be possible with JavaScriptSerializer but offhand I don't know how).

You can pick up the source code for this class along with the JSONSerializer dependency by downloading the West Wind Ajax Toolkit.

What now?

There might be a few improvements to this concept. Maybe it'd be useful to allow creating pre and post rendering script code so you can do things like scope the object and add namespaces potentially.

Another thing that keeps flicking around in my head is that damn problem with ClientIDs. It might be nice to have an optional method that can add properties for each control on the page automatically. Something like:

.AddClientIds(control)

which would then add each control and its clientID with properties generated by ID name. I'm not quite sure just how far this could go though and whether this would just be too expensive. Maybe you could specify a container and it would only retrieve control Ids at that container level rather than digging into the container hierarchy. So if you specified Page it would only pick up top level controls.

I don't know - need to mull that one over, but the ClientID mess has been one of the uglier things to do with AJAX code and this class at least provides an option for a cleaner way to address ControlIds even if it requires an extra line of code on the server to expose it. <shrug>

[Update: I've posted an update with a few of the enhancements above as well as some from the comments below]