Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Embedding ASP.NET Server Variables in Client JavaScript, Part 2


:P
On this page:

Last week I wrote about a small wwScriptVariables class I created that can be dropped into an ASP.NET page to handle passing server data into a client application by essentially providing a state bag to add key value pairs to, and then rendering those key value pairs as a client side object that has each key as a property. It's a dirt simple way to pass data from the server side code into client side script code and is a reasonable way to create global data - including ClientID references you can pass - that is accessible globally.

As a quick review the original behavior works like this (you can do this anywhere in Page code typically off of OnLoad):

wwScriptVariables scriptVars = new wwScriptVariables(this,"serverVars");
  
// *** Add any values static or dynamic
scriptVars.Add("name", "Rick Strahl");
scriptVars.Add("company", "Rick's \"East\\West\" Trading");
scriptVars.Add("entered", DateTime.Now.AddYears(-10));
scriptVars.Add("counter",12.22M);
 
// *** Add a control's value dynamically at pre render time
scriptVars.AddDynamicValue("txtNameValue", this.txtName, "Text");

This dynamically creates a global client side class that's accessible via script code rendered into startup script code:

var serverVars = {
    "name": "Rick Strahl",
    "company": "Rick's \"East\\West\" Trading",
    "entered": new Date(888053450808),
    "counter": 12.22,
    "txtNameValue": "Rick"
};

The key aspect here is that class manages proper formatting for JavaScript values. Strings are properly encoded (somehting the <%= this.MyString %> does not easily provide) and non-string values are turned into proper .NET types. You can even pass complex objects this way if you choose since a full JSON serializer is used to serialize the property values.

You can specify the name of the object, so it's possible for creating multiple objects which effectively allows scoping of the generated data, which can be useful if you're building a control and you need more than one instance. If you want to read more on how the basics work you can look at the original post.

Improvements a Go Go

At the end of the last post I mused that there were a few more interesting things that could be done and a few suggestions and improvements were - as usual - suggested in the comments. I took a little time last night to add a few enhancements to the class that includes some of the ideas I'd been kicking around.

Client ID Generation

One of the most annoying things to deal with in client side ASP.NET applications is working with a Master Page and then having to figure out the proper client id for a control. Usually you'll end up with a funky name like Ctl00_Content_txtName or something longer if more naming containers are involved.

I offered one solution to this problem with just the base behavior above which was to simply add any control names you care about to the above script variables bag and expose it as part of the object. On the server you'd simply do:

scriptVars.Add("txtNameId", this.txtName.ClientID);

and that works fine, but gets tedious if you have a large number of controls you need to access. As you add controls you'd need to add more keys which gets messy quickly.

So there are now two new methods that allow adding client IDs by container and its immediate children or by container and recursive children. You can do the following:

// *** Add all Client Ids - note this may cause naming conflicts on duplicate names
// *** in separate naming containers. First one wins!
scriptVars.AddClientIds(this,true);

and add all ClientIDs of all controls to the above object. The properties names add  an 'Id' postfix to minimize naming conflicts with other properties in the control:

 
var serverVars = {
    "name": "Rick Strahl",
    "company": "Rick's \"East\\West\" Trading",
    "entered": new Date(888054678218),
    "counter": 12.22,
    "txtNameValue": "",
    "headId": "ctl00_head",
    "form1Id": "aspnetForm",
    "ContentId": "ctl00_Content",
    "txtNameId": "ctl00_Content_txtName",
    "btnSubmitId": "ctl00_Content_btnSubmit",
    "panel1Id": "ctl00_Content_panel1",
    "txtPanelTextBoxId": "ctl00_Content_txtPanelTextBox",
    "repListId": "ctl00_Content_repList",
    "gdvTestId": "ctl00_Content_gdvTest"
};

This includes recursive digging into containers so this can produce a ton of controls and you'd definitely want to be careful with this. You can also call this non-recursively by leaving out the second parameter (or passing false) which only embeds client IDs for that particular Container level.

This feature is what I call a 'good enough' feature - it's not going to solve all naming problems since naming containers in ASP.NET exist for an obvious reason that containers can have multiple controls with the same name, so it's possible to end up with controls that have the same name and these controls will cause property naming conflicts. If you have duplicate names last one wins and the value is simply overwritten so only the last one found is honored.

I think it's still tremendously useful. Most of the application where I use AJAX controls that get accessed tend to at a very specific level in the page hierarchy. If there are potential naming conflicts those can be handled manually if necessary by explicitly adding the duplicate named controls as plain values with .Add().

Pre and Post Object Script Code

Jon Galloway (who sparked my original code) brought up the point of scoping for the generated output - a class in this case. The first avenue - addressed in the original code - is that you can customize the name of the control (or serverVars which is the default if you leave it out of the constructor).

But it might also be to create an instance on an existing object or to create an object hierarchy of which the serverVars object becomes a part of. For example, you can set up namespacing (even if you don't use MS AJAX).There are now AddScriptBefore() and AddScriptAfter() methods that let you add script before and after the rendered object.

wwScriptVariables scriptVars = new wwScriptVariables(this,"MyNameSpace.Custom.serverVars");
 
// *** Create a 'namespace'
scriptVars.AddScriptBefore("var MyNameSpace = {};\r\nMyNameSpace.Custom = {};");

scriptVars.AddScriptAfter("var serverVars = MyNameSpace.Custom.serverVars;");

This effectively adds the 'variable' servervars onto MyNameSpace.Custom rather than creating a new var variable.

var MyNameSpace = {}
MyNameSpace.Custom = {};
MyNameSpace.Custom.serverVars = {
    "name": "Rick Strahl",
    "company": "Rick's \"East\\West\" Trading",
    "entered": new Date(888056001940),
    "counter": 12.22,
    "txtNameValue": ""
};
var serverVars = MyNameSpace.Custom.serverVars;

The after script here is frivolous, but it demonstrates how alias the variable in multiple ways.

Updating Values from the Client

There was a comment from Frederik that shows of a way using two way sharing of variables via a dictionary which is a cool idea. His approach is using MS Ajax however, which I want to avoid. So I came up with another way of doing this which is essentially allowing two types of updates:

  • Update the object itself simply posting it back on a PostBack
  • Add a generic dictionary to add values arbitrarily from the client

What I ended up with a is an UpdateMode property that takes values of None, Properties, Items, All. There are also two methods Get<TType> and GetItem<TType> that allows retrieving property bag items that have been deserialized from the server.

// *** pass page reference and name of the client side objects (both optional)
   wwScriptVariables scriptVars = new wwScriptVariables(this,"serverVars");
   scriptVars.UpdateMode =  AllowUpdateTypes.All;
 
   // *** Add any values static or dynamic
   scriptVars.Add("name", "Rick Strahl");
   scriptVars.Add("company", "Rick's \"East\\West\" Trading");
   scriptVars.Add("entered", DateTime.Now.AddYears(-10));
   scriptVars.Add("counter",12.22M);
 
   // *** Read back values assigned to the client object
   if (this.IsPostBack)
   {
       Response.Write(scriptVars.GetValue<string>("name"));
       Response.Write(scriptVars.GetItemValue<string>("Custom"));
       Response.Write(scriptVars.GetItemValue<string>("CustomValue"));
       Response.Write(scriptVars.GetItemValue<DateTime>("curDate"));
       Response.Write(scriptVars.GetValue<DateTime>("entered"));
 
   }

This code adds several properties to be rendered in the client object and then also allows reading the values back on PostBack. Get<TType> (key) retrieves that key and casts it to the specified type. The type is required in order for the JSON deserialization to work.

The Items collection is generated on the client as and _Items property and an .add() method on the client object. The generated code looks like this:

var serverVars = {
    "name": "Rick Strahl",
    "company": "Rick's \"East\\West\" Trading",
    "entered": new Date(888059771295),
    "counter": 12.22,
    "txtNameValue": "",
    "_Items":{},
    "add": function(key,value) { this._Items[key] = value; }
};
function __submitServerVars(inst,hiddenId)
{
    var output = '';
    for(var prop in inst)
    {        
        if (prop == '_Items') 
        {
            var out = '';
            for(var p in inst._Items)            
                out += p + '=' + encodeURIComponent(JSON.serialize(inst._Items[p]) ) + '&';
            output += '_Items=' + encodeURIComponent(out) + '&';
        } else
        output += prop + '=' + encodeURIComponent(JSON.serialize(inst[prop])) + '&';
    }  
    $w(hiddenId).value = output;
};
wwEvent.addEventListener(document.forms['aspnetForm'],'submit',function() { __submitServerVars(serverVars,'__serverVars'); },true);//]]>

This code basically adds a generic handler for submitting the form variables and it hooks the OnSubmit handler of the page calling it and serializing the object data into POST variables. The process is pretty ugly - it encodes each key value pair as UrlEncoded values with the value JSON encoded. On the server the code then reads the whole string extracts the key value and deserializes the JSON into a proper typed value.

Now that this is in place you can now use the client collection:

// *** Update Properties to be sent back to server
serverVars.entered = new Date();
serverVars.name = "Jimmy Johnston Jr.";
 
// *** Update _Items 'collection'
serverVars.add("Custom","Rick");
serverVars.add("CustomValue","West Wind");
serverVars.add("curDate",new Date(2001,10,1));

and when the page posts back you can then access the values that were set on the client maintaining the typing along the way.

Values can then be read back with:

   if (this.IsPostBack)
   {
       Response.Write(scriptVars.GetValue<string>("name"));
       Response.Write(scriptVars.GetItemValue<string>("Custom"));
       Response.Write(scriptVars.GetItemValue<string>("CustomValue"));
       Response.Write(scriptVars.GetItemValue<DateTime>("curDate"));
       Response.Write(scriptVars.GetValue<DateTime>("entered")); 
   }

Note that you can only use the Get functions in a Postback or else the values won't be set and throw.

The Update related stuff adds an additional dependency to this class on the wwScriptLibrary.js client library which is part of the West Wind Ajax Toolkit which provides the event hook up code as well as the JSON encoding on the client and the JSON serialization features on the server.

Here's the complete code for the udated wwScriptVariables class:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web;
using System.Reflection;
using Westwind.Tools;
 
namespace Westwind.Web.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>
        /// Determines how updates to the server from the client are performed.
        /// If enabled changes to the client side properties post back to the
        /// server on a full Postback. 
        /// 
        /// Options allow for none, updating the properties only or updating
        /// only the Items collection (use .add() on the client to add new items)
        /// </summary>
        public AllowUpdateTypes UpdateMode
        {
            get { return _UpdateMode; }
            set { _UpdateMode = value; }
        }
        private AllowUpdateTypes _UpdateMode = AllowUpdateTypes.None;
 
 
        /// <summary>
        /// Internal string of the postback value for the field values
        /// if AllowUpdates is true
        /// </summary>
        private string PostBackValue
        {
            get
            {
                if (this._PostBackValue == null)
                    this._PostBackValue = this.Page.Request.Form["__" + this.ClientObjectName];
 
                return this._PostBackValue;
            }
        }
        private string _PostBackValue = null;
 
 
        /// <summary>
        /// Internal instance of the Json Serializer used to serialize
        /// the object and deserialize the updateable fields
        /// </summary>
        private JSONSerializer JsonSerializer = new JSONSerializer();
 
 
        /// <summary>
        /// Internally tracked prefix code
        /// </summary>
        private StringBuilder sbPrefixScriptCode = new StringBuilder();
 
        private StringBuilder sbPostFixScriptCode = new StringBuilder();
 
 
        /// <summary>
        /// Internal counter for submit script embedded
        /// </summary>
        private int SubmitCounter = 0;
 
 
        /// <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();
        }
 
        /// <summary>
        /// Adds a property and value to the client side object
        /// to be rendered into JavaScript code. VariableName 
        /// becomes a property on the object and the value will
        /// be properly converted into JavaScript Compatible text.
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="value"></param>
        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>
        /// Adds all the client ids for a container as properties of the 
        /// client object. The name of the property is the ID + "Id"
        /// Example: txtNameId
        /// 
        /// Note that there's no attempt made to  resolve naming conflicts
        /// in different naming containers. If there's a naming conflict
        /// last one wins.
        /// </summary>
        /// <param name="control"></param>
        public void AddClientIds(Control container, bool recursive)
        {
            foreach (Control control in container.Controls)
            {
                string id = control.ID + "Id";
                if (!string.IsNullOrEmpty(id) && !this.ScriptVariables.ContainsKey(id + "Id"))
                    this.ScriptVariables.Add(id + "Id", id);
                else
                    this.ScriptVariables[id + "Id"] = id;
 
                // *** Drill into the hierarchy
                if (recursive)
                    this.AddClientIds(control, true);
            }
        }
 
        /// <summary>
        /// Adds all the client ids for a container as properties of the 
        /// client object. The name of the property is the ID + "Id"
        /// Example: txtNameId
        /// This version only retrieves ids for the specified container
        /// level - no hierarchical recursion of controls is performed.
        /// </summary>
        /// <param name="container"></param>
        public void AddClientIds(Control container)
        {
            this.AddClientIds(container, false);
        }
 
        /// <summary>
        /// Any custom JavaScript code that is to immediately preceed the
        /// client object declaration. This allows setting up of namespaces
        /// if necesary for scoping.
        /// </summary>
        /// <param name="scriptCode"></param>
        public void AddScriptBefore(string scriptCode)
        {
            this.sbPrefixScriptCode.AppendLine(scriptCode);
        }
 
        /// <summary>
        /// Any custom JavaScript code that is to immediately follow the
        /// client object declaration. This allows setting up of namespaces
        /// if necesary for scoping.
        /// </summary>
        /// <param name="scriptCode"></param>
        public void AddScriptAfter(string scriptCode)
        {
            this.sbPostFixScriptCode.AppendLine(scriptCode);
        }
 
        /// <summary>
        /// Returns a value that has been updated on the client 
        /// 
        /// Note this method will throw if it is not called
        /// during PostBack or if AllowUpdates is false.
        /// </summary>
        /// <typeparam name="TType"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public TType GetValue<TType>(string key)
        {
            if (this.UpdateMode == AllowUpdateTypes.None || this.UpdateMode == AllowUpdateTypes.ItemsOnly)
                throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true");
            if (!this.Page.IsPostBack)
                throw new InvalidOperationException("GetValue can only be called during postback");
 
            // *** Get the postback value which is __ + ClientObjectName
            string textValue = this.PostBackValue;
            if (textValue == null)
                return default(TType);
 
            // *** Retrieve individual Url encoded value from the bufer
            textValue = wwWebUtils.GetUrlEncodedKey(textValue, key);
            if (textValue == null)
                return default(TType);
 
            // *** And deserialize as JSON
            object value = this.JsonSerializer.Deserialize(textValue, typeof(TType));
 
            return (TType) value;
        }
 
        /// <summary>
        /// Returns a value from the client Items collection
        /// </summary>
        /// <typeparam name="TType"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public TType GetItemValue<TType>(string key)
        {
            if (this.UpdateMode == AllowUpdateTypes.None || this.UpdateMode == AllowUpdateTypes.PropertiesOnly)
                throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true");
            if (!this.Page.IsPostBack)
                throw new InvalidOperationException("GetValue can only be called during postback");
 
            // *** Get the postback value which is __ + ClientObjectName
            string textValue = this.PostBackValue;
            if (textValue == null)
                return default(TType);
 
            // *** Retrieve individual Url encoded value from the bufer
            textValue = wwWebUtils.GetUrlEncodedKey(textValue, "_Items");
            if (textValue == null)
                return default(TType);
 
            textValue = wwWebUtils.GetUrlEncodedKey(textValue, key);
            if (textValue == null)
                return default(TType);
 
            // *** And deserialize as JSON
            object value = this.JsonSerializer.Deserialize(textValue, typeof(TType));
 
            return (TType)value;
        }
 
 
        /// <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;
 
            ClientScriptProxy scriptProxy = ClientScriptProxy.Current;
 
            StringBuilder sb = new StringBuilder();
 
            // *** Check for any prefix code and inject it
            if (this.sbPrefixScriptCode.Length > 0)
                sb.Append(sbPrefixScriptCode.ToString());
 
            // *** If the name includes a . assignment is made to an existing
            // *** object or namespaced reference - don't create var instance.
            if (!this.ClientObjectName.Contains("."))
                sb.Append("var ");
 
            sb.AppendLine( 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) + ",");
            }
 
            if (this.UpdateMode != AllowUpdateTypes.None)
            {
                sb.AppendLine("\t\"_Items\":{},");
                sb.AppendLine("\t\"add\": function(key,value) { this._Items[key] = value; },");
            }
 
            // *** Strip off last comma plus CRLF
            if (sb.Length > 0)
                sb.Length -= 3;                
 
            sb.AppendLine("\r\n};");
 
            if (this.UpdateMode != AllowUpdateTypes.None)
            {
                // *** Requires wwScritpLibrary
                ControlResources.LoadwwScriptLibrary(this.Page);
 
                scriptProxy.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "submitServerVars", STR_SUBMITSCRIPT, true);
 
                scriptProxy.RegisterHiddenField(this.Page, "__" + this.ClientObjectName, "");
 
                string script = @"wwEvent.addEventListener(document.forms['{1}'],'submit',function() {{ __submitServerVars({0},'__{0}'); }},true);";
                sb.Append(string.Format(script, this.ClientObjectName, this.Page.Form.ClientID,this.SubmitCounter++));                
            }
 
            if (this.sbPostFixScriptCode.Length > 0)
                sb.AppendLine(this.sbPostFixScriptCode.ToString());
 
            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
            scriptProxy.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
 
            //this.Page.ClientScript.RegisterClientScriptBlock(typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
        }
 
        const string STR_SUBMITSCRIPT =            
@"
function __submitServerVars(inst,hiddenId)
 {
    var output = '';
    for(var prop in inst)
    {        
        if (prop == '_Items') 
        {
            var out = '';
            for(var p in inst._Items)            
                out += p + '=' + encodeURIComponent(JSON.serialize(inst._Items[p]) ) + '&';
            output += '_Items=' + encodeURIComponent(out) + '&';
        } else
        output += prop + '=' + encodeURIComponent(JSON.serialize(inst[prop])) + '&';
    }  
    $w(hiddenId).value = output;
 };
";
 
    }
 
    public enum AllowUpdateTypes
    {
        None,
        ItemsOnly,
        PropertiesOnly,
        All
    }
}

You can download the full code for this as part of the West Wind Ajax Toolkit component. As with the original version, this code has a few dependencies the toolkit, such as the JSON conversion on the client and server and wwScriptLibrary.js for the even hookups in the Update scenario. With a little bit of tweaking you can remove those dependencies though especially if you aren't interested in the update scenario.

One more thought crossed my mind as I'm writing this up - it probably would be nice if there was also a way to get the serialization to work over plain AJAX callbacks. Encoding values as POST vars is one of the easiest ways to pass data back and forth because it allows enumerating over values. Pure JSON works great if you have an existing object to parse the data into on the server, but failing that JSON for complex types is difficult to deal with. The above statebag approach along with the wwScriptVariable.Get<> and GetItem<> methods is a great way to pass generic data from client to server even in AJAX without losing type information... Hmmm... I guess there'll be one more refactoring at least <g>...

Posted in AJAX  ASP.NET  JavaScript  

The Voices of Reason


 

# Interesting 'control' for passing ASP.NET variables into client-side Javascript

Anyone using ASP.NET and Javascript combo should consider this to prevent code injection attacks... ...

Derek Morrison
April 10, 2008

# re: Embedding ASP.NET Server Variables in Client JavaScript, Part 2

Thanks for this great util! This really helped make my app a lot cleaner.

I did have a little problem with the recursive AddClientIds function, however. In wwScriptVariables.cs, I changed the following:

public void AddClientIds(Control container, bool recursive)
{
    foreach (Control control in container.Controls)
    {
        string id = control.ID;
        if (!string.IsNullOrEmpty(id) && !this.ScriptVariables.ContainsKey(id + "Id"))
            this.ScriptVariables.Add(id + "Id", id);
        else
            this.ScriptVariables[id + "Id"] = id;

        // *** Drill into the hierarchy
        if (recursive)
            this.AddClientIds(control, true);
    }
}


to this:

public void AddClientIds(Control container, bool recursive)
{
    foreach (Control control in container.Controls)
    {
        string id = control.ID;
        if (!string.IsNullOrEmpty(id) && !this.ScriptVariables.ContainsKey(id + "Id"))
            this.ScriptVariables.Add(id + "Id", control.ClientID);
        else
            this.ScriptVariables[id + "Id"] = control.ClientID;

        // *** Drill into the hierarchy
        if (recursive)
            this.AddClientIds(control, true);
    }
}


(I changed "id" to "control.ClientID" in the assignments)

Also, if the control ID is null (which I was getting for asp:literal controls), then it's assigned to the "Id" variable. Should this just be discarded? I guess it could go either way though.

Thanks again!

Rick Strahl
April 10, 2008

# re: Embedding ASP.NET Server Variables in Client JavaScript, Part 2

@Derek - Duh, yes that needs to be ClientID. I had fixed that a while back, but it hasn't gone into the toolset download yet.

Mark Ingalls
May 31, 2008

# re: Embedding ASP.NET Server Variables in Client JavaScript, Part 2

Hi Rick,

I'm trying to use a modified version of this code to place client ids in the page for easy access. I converted the code to VB.Net and changed it to merely place the generated client ids in the page like this:

var serverVars = {
    "GridView1" : $get('GridView1'),
    "UserName" : $get('UserName')
};


Well, when I have a GridView on the page, it gets two id attributes:
    <table id="sessList_ctl00" cellspacing="0" cellpadding="2" border="0" id="sessList" style="color:Black;background-color:LightGoldenrodYellow;border-color:Tan;border-width:1px;border-style:solid;border-collapse:collapse;">


The first id listed (sessList_ctl00 in the example above) only shows up when I use my version of the wwScriptVariables class. I was wondering if you've seen anything similar or if I've changed something about the way the class works when I converted it to VB and removed the non client Id related code.

thanks,
mark

Mark Ingalls
June 01, 2008

# re: Embedding ASP.NET Server Variables in Client JavaScript, Part 2

Just another quick update on this. By actually binding the gridview in your test page in the code download to a datasource, I'm able to reproduce the problem. It must have something to do with actually walking the control tree as I can comment out the actual rendering of the script and the second id still shows up in the gridview's
<table>
tag.

thanks,
mark

Drew
August 18, 2008

# re: Embedding ASP.NET Server Variables in Client JavaScript, Part 2

Hi. Wondering if you can help. I'm new to this (this is just a hobby), so please be gentle.

I'd like to use your class, but I'm having a slight problem. I can only pass variables to the client if the variables are manipulated within *a function* that is later triggered. Is this by design or am I doing something wrong?

For example, I would like to ultimately pass a value to the client, the value is manipulated and then as the page is posted back, the manipulated value is returned. What is actually happening is that I can only retrieve manipulated values if they are placed within a function and that function is triggered by say a submit button after the initial postback.
The error I get otherwise is:

serverVars is undefined.

Many thanks

Drew

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024