#region License /* ************************************************************** * Author: Rick Strahl * © West Wind Technologies, 2008 * 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 // *** undefine this constant to remove the code that allows for // *** optional script compression with the wwScriptingModule // *** when MS Ajax is not available. #define IncludeScriptCompressionModuleSupport using System; using System.Web; using System.Web.UI; using System.Reflection; using System.Text; using System.IO; using System.Collections.Generic; using Westwind.Utilities; namespace Westwind.Web.Controls { /// /// This is a proxy object for the Page.ClientScript and MS Ajax ScriptManager /// object that can operate when MS Ajax when present otherwise falling back to /// Page.ClientScript. Because MS Ajax may not be available accessing the /// methods directly is not possible and we are required to indirectly /// reference client script methods through this class. /// /// This class should be invoked at the Control's start up and be used to /// replace all calls Page.ClientScript. Scriptmanager calls are made through /// Reflection indirectly so there's no dependency on the script manager. /// /// This class also provides a few additional page injection helpers like the /// abillity to load scripts in the page header (rather than in the body) and /// to use script compression using wwScriptCompressionModule without using MS /// Ajax. /// public class ClientScriptProxy { private const string STR_CONTEXTID = "__ClientScriptProxy"; private static Type scriptManagerType = null; // *** Register proxied methods of ScriptManager private static MethodInfo RegisterClientScriptBlockMethod; private static MethodInfo RegisterStartupScriptMethod; private static MethodInfo RegisterClientScriptIncludeMethod; private static MethodInfo RegisterClientScriptResourceMethod; private static MethodInfo RegisterHiddenFieldMethod; private static MethodInfo GetCurrentMethod; //private static MethodInfo RegisterPostBackControlMethod; //private static MethodInfo GetWebResourceUrlMethod; /// /// Determines the default script rendering mode that is uses if no script rendering mode /// is explicitly provided on the control. /// /// This setting is global and should be set only once in Appplication_Start or /// a static constructor. /// public static ScriptRenderModes DefaultScriptRenderMode = ScriptRenderModes.Script; /// /// List of ResourceToFile map items that map script resources loaded via Resources to just /// script filenames. These filenames can be compared against real file based scripts /// (when loaded through ScriptContainer) to be detected and only be loaded once. /// /// Note this is a List<> because there may be multiple supported values for a single /// resource (ie. not unique). /// public static List ScriptResourceAliases = new List() { new ScriptResourceAlias() { Alias = "jquery", Resource = ControlResources.JQUERY_SCRIPT_RESOURCE, FileId="jquery.js"}, new ScriptResourceAlias() { Alias = "ww.jquery", Resource = ControlResources.WWJQUERY_SCRIPT_RESOURCE, FileId = "ww.jquery.js" }, // The following are the min.js versions new ScriptResourceAlias() { Alias = "NONE", Resource = ControlResources.JQUERY_SCRIPT_RESOURCE, FileId = "jquery.min.js" }, new ScriptResourceAlias() { Alias = "NONE", Resource = ControlResources.WWJQUERY_SCRIPT_RESOURCE, FileId = "ww.jquery.min.js" } }; /// /// Internal global static that gets set when IsMsAjax() is /// called. The result is cached once per application so /// we don't have keep making reflection calls for each access /// private static bool _IsMsAjax = false; /// /// Flag that determines whether check was previously done /// private static bool _CheckedForMsAjax = false; /// /// Cached value to see whether the script manager is /// on the page. This value caches here once per page. /// private bool _IsScriptManagerOnPage = false; private bool _CheckedForScriptManager = false; private List _loadedScripts = new List(); /// /// Current instance of this class which should always be used to /// access this object. There are no public constructors to /// ensure the reference is used as a Singleton to further /// ensure that all scripts are written to the same clientscript /// manager. /// public static ClientScriptProxy Current { get { if (HttpContext.Current == null) return new ClientScriptProxy(); if (HttpContext.Current.Items.Contains(STR_CONTEXTID)) return HttpContext.Current.Items[STR_CONTEXTID] as ClientScriptProxy; ClientScriptProxy proxy = new ClientScriptProxy(); HttpContext.Current.Items[STR_CONTEXTID] = proxy; return proxy; } } /// /// No public constructor - use ClientScriptProxy.Current to /// get an instance to ensure you once have one instance per /// page active. /// protected ClientScriptProxy() { } #region ScriptManager Detection routines /// /// Checks to see if MS Ajax is registered with the current /// Web application. /// /// Note: Method is static so it can be directly accessed from /// anywhere. If you use the IsMsAjax property to check the /// value this method fires only once per application. /// /// public static bool IsMsAjax() { if (_CheckedForMsAjax) return _IsMsAjax; // *** Easiest but we don't want to hardcode the version here // scriptManagerType = Type.GetType("System.Web.UI.ScriptManager, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false); // *** To be safe and compliant we need to look through all loaded assemblies Assembly ScriptAssembly = null; // Assembly.LoadWithPartialName("System.Web.Extensions"); foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies()) { string fn = ass.FullName; if (fn.StartsWith("System.Web.Extensions")) { ScriptAssembly = ass; break; } } if (ScriptAssembly == null) return false; scriptManagerType = ScriptAssembly.GetType("System.Web.UI.ScriptManager"); if (scriptManagerType == null) { _IsMsAjax = false; _CheckedForMsAjax = true; return false; } // *** Method to check for current instance on a page - cache // *** since we might call this frequently GetCurrentMethod = scriptManagerType.GetMethod("GetCurrent"); _IsMsAjax = true; _CheckedForMsAjax = true; return true; } /// /// Checks to see if a script manager is on the page /// /// /// public bool IsScriptManagerOnPage(Page page) { // *** Check is done only once per page if (this._CheckedForScriptManager) return _IsScriptManagerOnPage; // *** Must check whether MS Ajax is available // *** at all first. Method sets up scriptManager // *** and GetCurrentMethod on success. if (!IsMsAjax()) { this._CheckedForScriptManager = true; this._IsScriptManagerOnPage = false; return false; } // *** Now check and see if we can get a ref to the script manager object sm = GetCurrentMethod.Invoke(null, new object[1] { page }); if (sm == null) this._IsScriptManagerOnPage = false; else this._IsScriptManagerOnPage = true; this._CheckedForScriptManager = true; return this._IsScriptManagerOnPage; } #endregion /// /// High level helper function that is used to load script resources for various AJAX controls /// Loads a script resource based on the following scriptLocation values: /// /// * WebResource /// Loads the Web Resource specified out of ControlResources. Specify the resource /// that applied in the resourceName parameter /// /// * Url/RelativeUrl /// loads the url with ResolveUrl applied /// /// * empty string (no value) /// No action is taken and nothing is embedded into the page. Use this if you manually /// want to load resources /// /// The control instance for which the resource is to be loaded /// WebResource, a Url or empty (no value) /// The name of the resource when WebResource is used for scriptLocation /// Determines if scripts are loaded into the header whether they load at the top or bottom public void LoadControlScript(Control control, string scriptLocation, string resourceName, ScriptRenderModes renderMode) { // *** Specified nothing to do if (string.IsNullOrEmpty(scriptLocation)) return; if (scriptLocation == "WebResource") { RegisterClientScriptResource(control, control.GetType(), resourceName,renderMode); return; } RegisterClientScriptInclude(control, control.GetType(), control.ResolveUrl(scriptLocation), renderMode); } public void LoadControlScript(Control control, string scriptLocation, string resourceName) { this.LoadControlScript(control, scriptLocation, resourceName, ScriptRenderModes.Inherit); } public void LoadControlScript(Control control, string scriptLocation) { this.LoadControlScript(control, scriptLocation, null, ScriptRenderModes.Inherit); } /// /// Returns a WebResource or ScriptResource URL for script resources that are to be /// embedded as script includes. /// /// /// /// public void RegisterClientScriptResource(Control control, Type type, string resourceName, ScriptRenderModes renderMode) { #if IncludeScriptCompressionModuleSupport // *** If ScriptCompression Module is loaded use it to compress // *** script resources by using wcSC.axd Url the module intercepts if (ScriptCompressionModule.ScriptCompressionModuleActive) { string resName = HttpUtility.UrlEncode(resourceName); // Resources from this assembly don't need assembly name/id if (type.Assembly == this.GetType().Assembly) RegisterClientScriptInclude(control, type,"wwSC.axd?r=" + resName, renderMode ); //Convert.ToBase64String(Encoding.ASCII.GetBytes())); else { string typName = HttpUtility.UrlEncode(type.Assembly.FullName); RegisterClientScriptInclude(control, type, string.Format("wwSC.axd?r={0}&t={1}",resName,typName),renderMode ); //Convert.ToBase64String(Encoding.ASCII.GetBytes(resName)) + //"&t=" + //Convert.ToBase64String(Encoding.ASCII.GetBytes(type.Assembly.FullName))); } return; } #endif if (this.IsScriptManagerOnPage(control.Page)) { // *** NOTE: If MS Ajax is referenced, but no scriptmanager is on the page // script no compression will occur. With a script manager // on the page compression will be handled by MS Ajax. if (RegisterClientScriptResourceMethod == null) RegisterClientScriptResourceMethod = scriptManagerType.GetMethod("RegisterClientScriptResource", new Type[3] { typeof(Control), typeof(Type), typeof(string) }); RegisterClientScriptResourceMethod.Invoke(null, new object[3] { control, type, resourceName }); return; } // Otherwise just embed a script reference into the page using ClientScript control.Page.ClientScript.RegisterClientScriptResource(type, resourceName); } /// /// Registers a script include tag into the page for an external script url. /// /// This version embeds only in the body of the HTML document - ie. underneath the form tag /// /// /// /// /// public void RegisterClientScriptInclude(Control control, Type type, string key, string url) { if (IsMsAjax()) { if (RegisterClientScriptIncludeMethod == null) RegisterClientScriptIncludeMethod = scriptManagerType.GetMethod("RegisterClientScriptInclude", new Type[4] { typeof(Control), typeof(Type), typeof(string), typeof(string) }); RegisterClientScriptIncludeMethod.Invoke(null, new object[4] { control, type, key, url }); } else control.Page.ClientScript.RegisterClientScriptInclude(type, key, url); } /// /// Registers a client script reference in the header instead of inside the document /// before the form tag. /// /// The script tag is embedded at the bottom of the HTML header. /// /// /// /// /// Determines if the resource is laoded at the to of the header or the bottom public void RegisterClientScriptInclude(Control control, Type type, string Url, ScriptRenderModes renderMode) { if (renderMode == ScriptRenderModes.Inherit) renderMode = DefaultScriptRenderMode; // Extract just the script filename string fileId = null; // Check resource IDs and try to match to mapped file resources // Used to allow scripts not to be loaded more than once whether // embedded manually (script tag) or via resources with ClientScriptProxy if (Url.Contains(".axd?r=")) { string res = StringUtils.ExtractString(Url,"?r=","&",false,true); foreach (ScriptResourceAlias item in ScriptResourceAliases) { if (item.Resource == res) { fileId = item.Alias + ".js"; break; } } if (fileId == null) fileId = Url.ToLower(); } else fileId = Path.GetFileName(Url).ToLower(); // No dupes - ref script include only once const string identifier = "script_"; if (HttpContext.Current.Items.Contains( identifier + fileId ) ) return; HttpContext.Current.Items.Add(identifier + fileId, string.Empty); if (control.Page.Header == null || renderMode == ScriptRenderModes.Script || renderMode == ScriptRenderModes.Inline) { this.RegisterClientScriptInclude(control, type, Url, Url); return; } // *** Retrieve script index in header object val = HttpContext.Current.Items["__ScriptResourceIndex"]; int index = 0; if (val != null) index = (int)val; StringBuilder sb = new StringBuilder(256); // *** Embed in header sb.AppendLine(@""); if (renderMode == ScriptRenderModes.HeaderTop) control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString())); else control.Page.Header.Controls.Add(new LiteralControl(sb.ToString())); index++; HttpContext.Current.Items["__ScriptResourceIndex"] = index; } /// /// Registers a CSS Web Resource in the page /// /// /// /// public void RegisterCssResource(Control control, Type type, string resourceName) { // *** Otherwise just embed a script reference into the page using standard page methods string resourceUrl = this.GetClientScriptResourceUrl(control,type, resourceName); this.RegisterCssLink(control, type, resourceName, resourceUrl); } /// /// Registers a CSS stylesheet in the page header and if that's not accessible inside of the form tag. /// /// /// /// /// public void RegisterCssLink(Control control, Type type, string key, string url) { Control container = control.Page.Header; if (container == null) container = control.Page.Form; if (container == null) throw new InvalidOperationException("There's no header or form to add CSS to Register Resource CSS on the page."); string output = "\r\n"; container.Controls.Add(new LiteralControl(output)); } /// /// Registers a client script block in the page. /// /// /// /// /// /// public void RegisterClientScriptBlock(Control control, Type type, string key, string script, bool addScriptTags) { if (IsMsAjax()) { if (RegisterClientScriptBlockMethod == null) RegisterClientScriptBlockMethod = scriptManagerType.GetMethod("RegisterClientScriptBlock", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) }); RegisterClientScriptBlockMethod.Invoke(null, new object[5] { control, type, key, script, addScriptTags }); } else control.Page.ClientScript.RegisterClientScriptBlock(type, key, script, addScriptTags); } /// /// Registers a startup code snippet that gets placed at the bottom of the page /// /// /// /// /// /// public void RegisterStartupScript(Control control, Type type, string key, string script, bool addStartupTags) { if (IsMsAjax()) { if (RegisterStartupScriptMethod == null) RegisterStartupScriptMethod = scriptManagerType.GetMethod("RegisterStartupScript", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) }); RegisterStartupScriptMethod.Invoke(null, new object[5] { control, type, key, script, addStartupTags }); } else control.Page.ClientScript.RegisterStartupScript(type, key, script, addStartupTags); } /// /// Returns a WebResource URL for non script resources /// /// /// /// /// public string GetWebResourceUrl(Control control, Type type, string resourceName) { return control.Page.ClientScript.GetWebResourceUrl(type, resourceName); } /// /// Works like GetWebResourceUrl but can be used with javascript resources /// to allow using of resource compression (if the module is loaded). /// /// /// /// /// public string GetClientScriptResourceUrl(Control control, Type type, string resourceName) { #if IncludeScriptCompressionModuleSupport // *** If wwScriptCompression Module through Web.config is loaded use it to compress // *** script resources by using wcSC.axd Url the module intercepts if (ScriptCompressionModule.ScriptCompressionModuleActive) { string url = "wwSC.axd?r=" + HttpUtility.UrlEncode(resourceName); if (type.Assembly != this.GetType().Assembly) url += "&t=" + HttpUtility.UrlEncode(type.FullName); return url; } #endif return control.Page.ClientScript.GetWebResourceUrl(type, resourceName); } /// /// Injects a hidden field into the page /// /// /// /// public void RegisterHiddenField(Control control, string hiddenFieldName, string hiddenFieldInitialValue) { if (IsMsAjax()) { if (RegisterHiddenFieldMethod == null) RegisterHiddenFieldMethod = scriptManagerType.GetMethod("RegisterHiddenField", new Type[3] { typeof(Control), typeof(string), typeof(string) }); RegisterHiddenFieldMethod.Invoke(null, new object[3] { control, hiddenFieldName, hiddenFieldInitialValue }); } else control.Page.ClientScript.RegisterHiddenField(hiddenFieldName, hiddenFieldInitialValue); } } /// /// Determines how scripts are included into the page /// public enum ScriptRenderModes { /// /// Inherits the setting from the control or from the ClientScript.DefaultScriptRenderMode /// Inherit, /// Renders the script include at the location of the control /// Inline, /// /// Renders the script include into the header of the page. Bottom of header. /// Header, /// /// Renders the script include into the header of the page /// HeaderTop, /// /// Uses ClientScript or ScriptManager to embed the script include /// Script } public struct ScriptResourceAlias { /// /// An alias/shortcut resource name /// public string Alias; /// /// The name of the script file that this resource maps to. Should be just /// the filename (ie. jquery.js or ww.jquery.js) as well as min.js versions /// if those files are loaded as well /// public string FileId; /// /// The full resource name to the resourceToFileItem /// public string Resource; /// /// Any type in the assembly that contains the script resource /// If null looks in the current executing assembly. /// public Type ControlType; } }