#region License /* ************************************************************** * Author: Rick Strahl * © West Wind Technologies, 2008 - 2009 * http://www.west-wind.com/ * * Created: 09/04/2008 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. ************************************************************** */ #endregion using System; using System.Collections.Generic; using System.Text; using System.Web.UI; using System.Web; using Westwind.Utilities; using Westwind.Web.JsonSerializers; namespace Westwind.Web { /// /// 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. /// /// This component supports:<<ul>> /// <<li>> Creating individual client side variables /// <<li>> Dynamic values that are 'evaluated' in OnPreRender to /// pick up a value /// <<li>> Creating properties of ClientIDs for a given container /// <<li>> Changing the object values and POSTing them back on /// Postback /// <</ul>> /// /// You create a script variables instance and add new keys to it: /// <<code lang="C#">> /// ScriptVariables scriptVars = new ScriptVariables(this,"scriptVars"); /// /// // Simple value /// scriptVars.Add("userToken", UserToken); /// /// AmazonBook tbook = new AmazonBook(); /// tbook.Entered = DateTime.Now; /// /// // Complex value marshalled /// scriptVars.Add("emptyBook", tbook); /// /// scriptVars.AddDynamic("author", txtAuthor,"Text"); /// /// // Cause all client ids to be rendered as scriptVars.formFieldId vars (Id /// postfix) /// scriptVars.AddClientIds(Form,true); /// <</code>> /// /// In client code you can then access these variables: /// <<code lang="JavaScript">>$( function() { /// alert(scriptVars.book.Author); /// alert(scriptVars.author); /// alert( $("#" + scriptVars.txtAmazonUrlId).val() ); /// });<</code>> /// public class ScriptVariables { /// Edit /// Internally holds all script variables declared /// Dictionary ScriptVars = new Dictionary(); /// /// Internally tracked reference to the Page object /// Page Page = null; /// /// The name of the object generated in client script code /// public string ClientObjectName { get { return _ClientObjectName; } set { _ClientObjectName = value; } } private string _ClientObjectName = "serverVars"; /// /// Determines whether the output object script is rendered /// automatically as part of Page PreRenderComplete. If false /// you can manually call the GetClientScript() method to /// retrieve the script as a string and embed it yourself. /// public bool AutoRenderClientScript { get { return _AutoRenderClientScript; } set { _AutoRenderClientScript = value; } } private bool _AutoRenderClientScript = true; /// /// 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) /// public AllowUpdateTypes UpdateMode { get { return _UpdateMode; } set { _UpdateMode = value; } } private AllowUpdateTypes _UpdateMode = AllowUpdateTypes.None; /// /// Internal string of the postback value for the field values /// if AllowUpdates is true /// private string PostBackValue { get { if (_PostBackValue == null) _PostBackValue = HttpContext.Current.Request.Form["__" + ClientObjectName]; return _PostBackValue; } } private string _PostBackValue = null; /// /// Internal instance of the Json Serializer used to serialize /// the object and deserialize the updateable fields /// private JSONSerializer JsonSerializer; /// /// Internally tracked prefix code /// private StringBuilder sbPrefixScriptCode = new StringBuilder(); private StringBuilder sbPostFixScriptCode = new StringBuilder(); /// /// Internal counter for submit script embedded /// private int SubmitCounter = 0; /// /// Full constructor that receives an instance of any control object /// and the client name of the generated script object that contains /// the specified properties. /// /// /// public ScriptVariables(Control control, string clientObjectName) { if (control == null) // Note: this will fail if called from Page Contstructor // ie. wwScriptVariables scripVars = new wwScriptVariables(); Page = HttpContext.Current.CurrentHandler as Page; else Page = control.Page; if (Page != null) { // Force RenderClientScript to be called before the page renders Page.PreRenderComplete += new EventHandler(Page_PreRenderComplete); } if (!string.IsNullOrEmpty(clientObjectName)) ClientObjectName = clientObjectName; // we have to use the West Wind parser since dates use new Date() formatting as embedded JSON 'date string' JsonSerializer = new JSONSerializer(SupportedJsonParserTypes.WestWindJsonSerializer); JsonSerializer.DateSerializationMode = JsonDateEncodingModes.NewDateExpression; } /// /// This constructor only takes an instance of a Control. The name of the /// client object will be serverVars. /// /// public ScriptVariables(Control control) : this(control, "serverVars") { } /// /// 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. /// public ScriptVariables() : this(null, "serverVars") { } /// /// Implemented after Page's OnPreRender() has fired to ensure all /// page code has a chance to write script variables. /// /// /// private void Page_PreRenderComplete(object sender, EventArgs e) { if (AutoRenderClientScript) RenderClientScript(); } /// /// 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. /// Class ScriptVariables /// /// /// The name of the property created on the client object. /// /// /// The value that is to be assigned. Can be any simple type and most complex /// objects that can be serialized into JSON. /// /// /// &lt;&lt;code /// lang=&quot;C#&quot;&gt;&gt;ScriptVariables scriptVars = new /// ScriptVariables(this,&quot;serverVars&quot;); /// /// // Add simple values /// scriptVars.Add(&quot;name&quot;,&quot;Rick&quot;); /// scriptVars.Add(&quot;pageLoadTime&quot;,DateTime.Now); /// /// // Add objects /// AmazonBook amazon = new AmazonBook(); /// bookEntity book = amazon.New(); /// /// scripVars.Add(&quot;book&quot;,book); /// &lt;&lt;/code&gt;&gt; /// public void Add(string variableName, object value) { ScriptVars.Add(variableName, value); } /// /// 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. /// Class ScriptVariables /// /// /// Name of the property created on the client object. /// /// /// Object or Control reference that is to be evaluated. Note this object needs /// to be protected or public in order to be serialized depending on trust /// settings in ASP.NET (medium trust can't look at protected members). /// /// /// The name of the property that is to be evaluated as a string. /// /// /// &lt;&lt;code /// lang=&quot;C#&quot;&gt;&gt;ScriptVariables scriptVars = new /// ScriptVariables(this,&quot;serverVars&quot;); /// /// // Add control values /// scriptVars.AddDynamic(&quot;name&quot;,txtName,&quot;Text& /// amp;quot;); /// /// // Add an object's value /// scriptVars.AddDynamic(&quot;ItemTotal&quot;,Invoice,&quot; /// ItemTotal&quot;) /// &lt;&lt;/code&gt;&gt; /// public void AddDynamicValue(string variableName, object control, string property) { // Special key syntax: .varName.Property syntax to be picked up by parser ScriptVars.Add("." + variableName + "." + property, control); } /// /// 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. /// Class ScriptVariables /// /// /// The container from which to retrieve Client IDs. You can use Form or /// this for the top level. /// /// /// Determines whether ClientIDs are retrieved recursively by drilling into /// containers. Use with care - large pages with many controls may take a long /// time to find and serialize all control Ids. It's best to focus on the /// controls you are interested and if necesary use multiple AddClientIds() /// calls. /// public void AddClientIds(Control container, bool recursive) { foreach (Control control in container.Controls) { string id = control.ID + "Id"; if (string.IsNullOrEmpty(id)) continue; if (!ScriptVars.ContainsKey(id )) ScriptVars.Add(id, control.ClientID); else ScriptVars[id] = control.ClientID; // Drill into the hierarchy if (recursive) AddClientIds(control, true); } } /// /// 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. /// Class ScriptVariables /// /// /// The container for which to retrieve client IDs. /// public void AddClientIds(Control container) { AddClientIds(container, false); } /// /// Any custom JavaScript code that is to immediately preceed the /// client object declaration. This allows setting up of namespaces /// if necesary for scoping. /// /// public void AddScriptBefore(string scriptCode) { sbPrefixScriptCode.AppendLine(scriptCode); } /// /// Any custom JavaScript code that is to immediately follow the /// client object declaration. This allows setting up of namespaces /// if necesary for scoping. /// /// public void AddScriptAfter(string scriptCode) { sbPostFixScriptCode.AppendLine(scriptCode); } /// /// 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. /// /// /// /// public TType GetValue(string key) { HttpRequest Request = HttpContext.Current.Request; if (UpdateMode == AllowUpdateTypes.None || UpdateMode == AllowUpdateTypes.ItemsOnly) throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true"); if (Request.HttpMethod != "POST") throw new InvalidOperationException("GetValue can only be called during postback"); // Get the postback value which is __ + ClientObjectName string textValue = PostBackValue; if (textValue == null) return default(TType); // Retrieve individual Url encoded value from the bufer textValue = WebUtils.GetUrlEncodedKey(textValue, key); if (textValue == null) return default(TType); // And deserialize as JSON object value = JsonSerializer.Deserialize(textValue, typeof(TType)); return (TType) value; } /// /// Returns a value from the client Items collection /// /// /// /// public TType GetItemValue(string key) { HttpRequest Request = HttpContext.Current.Request; if (UpdateMode == AllowUpdateTypes.None || UpdateMode == AllowUpdateTypes.PropertiesOnly) throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true"); if (Request.HttpMethod != "POST") return default(TType); // throw new InvalidOperationException("GetValue can only be called during postback"); // Get the postback value which is __ + ClientObjectName string textValue = PostBackValue; if (string.IsNullOrEmpty(textValue)) return default(TType); // Retrieve individual Url encoded value from the buffer textValue = WebUtils.GetUrlEncodedKey(textValue, "_Items"); if (string.IsNullOrEmpty(textValue)) return default(TType); textValue = WebUtils.GetUrlEncodedKey(textValue, key); if (textValue == null) return default(TType); // And deserialize as JSON object value = JsonSerializer.Deserialize(textValue, typeof(TType)); return (TType)value; } /// /// Returns the rendered JavaScript for the generated object and name. /// Note this method returns only the generated object, not the /// related code to save updates. /// /// You can use this method with MVC Views to embedd generated JavaScript /// into the the View page. /// If provided wraps the script text with script tags /// public string GetClientScript(bool addScriptTags) { if (!AutoRenderClientScript || ScriptVars.Count == 0) return string.Empty; StringBuilder sb = new StringBuilder(); if (addScriptTags) sb.AppendLine("\r\n"); if (UpdateMode != AllowUpdateTypes.None) { string clientID = "__" + ClientObjectName; string script = string.Format(@"$(document.forms[0]).submit(function() {{ __submitServerVars({0},'__{0}'); }});", ClientObjectName); sb.AppendLine( ""); sb.AppendFormat(@"" + "\r\n",clientID); } return sb.ToString(); // Use ClientScriptProxy to be MS Ajax compatible - otherwise use ClientScript //scriptProxy.RegisterClientScriptBlock(Page, typeof(ControlResources), "ClientObject_" + ClientObjectName, sb.ToString(), true); } /// /// 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 /// private void RenderClientScript() { ClientScriptProxy scriptProxy = ClientScriptProxy.Current; string script = GetClientScript(false); // TODO: This has to be fixed for ww.jquery.js if (UpdateMode != AllowUpdateTypes.None) { ControlResources.LoadjQuery(Page); ControlResources.LoadwwjQuery(Page); scriptProxy.RegisterClientScriptBlock(Page, typeof(ControlResources), "submitServerVars", STR_SUBMITSCRIPT, true); scriptProxy.RegisterHiddenField(Page, "__" + ClientObjectName, ""); script += string.Format(@"$(document.forms['{1}']).submit(function() {{ __submitServerVars({0},'__{0}'); }});", ClientObjectName, Page.Form.ClientID, SubmitCounter++); } scriptProxy.RegisterClientScriptBlock(Page, typeof(ControlResources), "ClientObject_" + ClientObjectName,script, 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.stringifyWithDates(inst._Items[p]) ) + '&'; output += '_Items=' + encodeURIComponent(out) + '&'; } else output += prop + '=' + encodeURIComponent(JSON.stringifyWithDates(inst[prop])) + '&'; } $('#' + hiddenId).val(output); }; "; } public enum AllowUpdateTypes { None, ItemsOnly, PropertiesOnly, All } }