Server-side Render to Create Client-Side Snippets of code (For Handlers)

  1. #1

    Server-side Render to Create Client-Side Snippets of code (For Handlers)

    I posted the following feature request here: http://forums.ext.net/showthread.php...lers)&p=124113

    The idea in a nutshell is to allow the developer to use server-side code to generate client-side code that can be used by Control Listeners. Example:
        var Renderer = new ClientScript( Context );
        Renderer.Script( () => {
                // Any server-side code placed in here will render to client-side string
                X.MessageBox.Alert( "Test", "Post-2nd Click Test." ).Show();
        } );
        Button1.OnClientClick = Renderer.ToScript();
    I got a little impatient and decided to see if I could implement a simple version myself that did not require modifying any of the Ext.Net code. I changed some of the names from what I originally proposed and extended it to support all the modes of operation I described in the proposal.

    I haven't done a lot of robust testing yet but it seems to work in the sample I will post at the end. It works by swapping out the base resource manager with a temporary one bound to a SelfRenderingPage.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.UI;
    using Ext.Net;
    
    
    namespace Ext.Net.Extension {
    
        /// <summary>
        /// Create Client Script string using Ext.Net Server-side api that can be attached to Control Handlers
        /// </summary>
        public class ClientScript : IDisposable {
    
            protected HttpContext Context = null;
            protected string script = null;
            protected ResourceManager internalrm = null;
            protected ResourceManager baserm = null;
    
            public ClientScript() : this( HttpContext.Current ) { }
            public ClientScript( HttpContext context ) {
                this.Context = context;
                SaveBaseRM();
            }
    
            /// <summary>
            /// Prefered method to indicate Server-side api will be scripted to string that can be used by a Control Handler.
            /// </summary>
            /// <param name="ScriptFn">Function or delegate who's internal contents will be scripted</param>
            public virtual void Script( Action ScriptFn ) {
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Create the temporary rm if necessary
                InitRM();
                // Set the Current RM to the internal rm before calling script fn
                SetRM( internalrm );
                // Run the code you want to convert to script
                ScriptFn();
                // Restore Current RM to base
                SetRM( baserm );
            }
    
    
    #if SupportingStartEnd
            public bool Running { get; private set; }
    
    
            /// <summary>
            /// Begin scripting to Client-side code. All server-side api calls between this call and ScriptEnd will be rendered to a string.
            /// Make sure to call ScriptEnd() or else Dispose will be left to the Garbage-Collector and that could be dangerous.
            /// </summary>
            public virtual void ScriptStart() {
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Create the temporary rm if necessary
                InitRM();
                // Set the Current RM to the internal rm before calling script fn
                Running = true;
                SetRM( internalrm );
            }
            /// <summary>
            /// End scripting to Client-side code. All server-side api calls will be rendered back to response writer.
            /// </summary>
            public virtual void ScriptEnd() {
                if( internalrm == null )
                    throw new Exception( "Script Rendering was already Started." );
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Set the Current RM to the internal rm before calling script fn
                Running = false;
                SetRM( baserm );
            }
    
            /// <summary>
            /// Begin scripting to Client-side code the section inside a using statement. using( var cs = ClientScript.ScriptUsing(Context)) ) { ... }
            /// Make sure you are using a using section.
            /// </summary>
            /// <param name="Context"></param>
            /// <returns></returns>
            public static ClientScript ScriptUsing( HttpContext Context ) {
                ClientScript cs = new ClientScript( Context );
                cs.ScriptStart();
                return cs;
            }
    #endif
    
            /// <summary>
            /// Render the script to a string that can be used for Control Handlers
            /// </summary>
            /// <returns></returns>
            public virtual string ToScript() {
                // Build the script if required
                if( script == null && internalrm != null ) {
    #if SupportingStartEnd
                    // If Script was started, we need to end it
                    if( Running )
                        ScriptEnd();
    #endif
                    script = internalrm.ToScript( true );
                    Dispose();
                }
                return this.script;
            }
    
            protected void SaveBaseRM() {
                // Check if we're already storing the BaseRM
                object o = Context.Items["BaseRM"];
                if( o == null )
                    // Save the original ResourceManager
                    Context.Items["BaseRM"] = baserm = ResourceManager.GetInstance( Context );
                else
                    baserm = (ResourceManager)o;
            }
    
            protected void InitRM() {
                // If the temp rm is already initialized, exit
                if( internalrm != null )
                    return;
                // Set scope of ResourceManager to new SelfRendering Page Context
                System.Web.UI.Page pageHolder = (System.Web.UI.Page)new SelfRenderingPage();
                internalrm = new ResourceManager( true );
                internalrm.RenderScripts = ResourceLocationType.None;
                internalrm.RenderStyles = ResourceLocationType.None;
                internalrm.IDMode = internalrm.IDMode;
                pageHolder.Controls.Add( internalrm );
            }
    
            protected void SetRM( ResourceManager rm ) {
                if( Context.CurrentHandler is Page )
                    ( (Page)Context.CurrentHandler ).Items[typeof( ResourceManager )] = rm;
                else
                    Context.Items[typeof( ResourceManager )] = rm;
            }
    
            public void Dispose() {
                // Do nothing if already disposed
                if( Context == null )
                    return;
                // If script not already rendered, render it now
                if( this.script == null )
                    this.script = ToScript();
                // Release resources
                internalrm = null;
                baserm = null;
                Context = null;
            }
    
        }
    }
    If you want to support the Start / End mechanism, define SupportingStartEnd at the beginning of the code. Even though it works, I decided it was a lot less safe than using a delegate.
    Last edited by michaeld; Feb 03, 2014 at 11:22 PM.
  2. #2

    Sample showing it works...

    <%@ Page Language="C#" EnableViewState="false" ClassName="Test45" %>
    <%@ Import Namespace="Ext.Net.Extension" %>
    
    
    <script runat="server">
        protected void Page_Load( object sender, EventArgs e ) {
        }
        [DirectMethod( IDMode = DirectMethodProxyIDMode.None, ShowMask = true )]
        public void Click() {
            X.MessageBox.Alert( "Test", "Inside Test." ).Show();
            var Renderer = new ClientScript( Context );
            Renderer.Script( () => {
                // Any server-side code placed in here will render to client-side string
                X.MessageBox.Alert( "Test", "Post-2nd Click Test." ).Show();
            } );
            Button1.OnClientClick = Renderer.ToScript();
            Button1.Text = "MsgBox";
            Button1.Update();
        }
    </script>
    
    
    <!DOCTYPE html>
    <html>
    <head id="Head1" runat="server">
        <title>Test45 Sample</title>
    </head>
    <body>
        <form id="Form1" runat="server">
    
    
            <ext:ResourceManager ID="ResourceManager1" runat="server" ScriptMode="Development" SourceFormatting="true" IDMode="Explicit" />
            <ext:Viewport ID="vp" runat="server" Layout="HBoxLayout">
                <Items>
    
    
                    <ext:Panel ID="LP" runat="server" Border="true" Padding="5" Flex="1" Layout="FitLayout" Title="Test45.aspx">
                        <Items>
                            <ext:Label ID="Label1" runat="server" Html="Outer" />
                        </Items>
                        <Buttons>
                            <ext:Button ID="Button1" runat="server" Text="Click" OnClientClick="App.direct.Click();" />
                        </Buttons>
                    </ext:Panel>
    
    
                </Items>
            </ext:Viewport>
        </form>
    </body>
    </html>
    Note that after the first click of Button1, it replaces the server-side MessageBox Alert listener with the code generated by the server-side for the second click.
  3. #3

    Limitations....

    Today, I attempted to see if I can update other controls on the page inside the Script delegate.
    Test Example:
            Label1.Text = "First Change";
            Renderer.Script( () => {
                // Any server-side code placed in here will render to client-side string
                X.MessageBox.Alert( "Test", "Post-2nd Click Test." ).Show();
                Label1.Text = "Changed Label";
            } );
    That didn't work. Label1 got set to "Change Label" on the first click. I'm understanding this is probably because Label1 is setting its own ProxyScripts which is attached to the main page's ResourceManager. I don't have a way around this. Even if I was to temporarily move the children to the temp SelfRenderingPage, I'd also have to swap in and out the ProxyScripts for every control and their internal.

    So it's looking like, at least for now, the limitation is I can only do X.AddScript based calls unless I figure out another way around the way the control setters. I'm still experimenting, but I think the only answer will be to modify BaseControl.AddScript(string script) to test for the InnerDelegateRenderCondition and call the current ResourceManager's ProxyScripts.Add().
    Last edited by michaeld; Feb 04, 2014 at 9:04 AM.
  4. #4

    No Limitation Version...

    As I indicated in the last message, to get this working, I had to make a tweak to AddScript(string script) in IScriptable.cs.
    This works by keeping all the AddScript calls using the temporary ResourceManager's ProxyScripts instead of the individual controls. There may be an impact to this I haven't thought of yet, but so far it's handled everything I've thrown at it.

    Unified dif shows as follows:
            public virtual void AddScript(string script)
            {
                if (this.IsProxy || this.AlreadyRendered)
                {
                    if (HttpContext.Current == null)
                    {
                        ResourceManager.AddInstanceScript(script);
                        return;
                    }
    
                    ResourceManager rm = ResourceManager.GetInstance(HttpContext.Current);
    
                    if (HttpContext.Current.CurrentHandler is Page && rm != null)
                    {
                        rm.AddScript(script);
                    }
                    else
                    {
                        ResourceManager.AddInstanceScript(script);
                    }
    
                    return;
                }
     
    +            var csrm = ResourceManager.ClientScriptResourceManager;
    +            if( csrm != null ) {
    +                csrm.ProxyScripts.Add( ResourceManager.ScriptOrderNumber, TokenUtils.ReplaceRawToken( TokenUtils.ParseTokens( script, this ) ) );
    +                return;
    +            }
    +
                if (script.IsNotEmpty() && !this.IsParentDeferredRender && this.Visible)
                {
                    if (this.AlreadyRendered && this.HasResourceManager)
                    {
                        this.ResourceManager.RegisterOnReadyScript(ResourceManager.ScriptOrderNumber, TokenUtils.ReplaceRawToken(TokenUtils.ParseTokens(script, this)));
                    }
                    else
                    {
                        this.ProxyScripts.Add(ResourceManager.ScriptOrderNumber, TokenUtils.ReplaceRawToken(TokenUtils.ParseTokens(script, this)));    
                    }
                }
            }
    Also added the following file to Ext.Net project which extends ResourceManager with a new property:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.UI;
    
    namespace Ext.Net {
    
        public partial class ResourceManager {
            public static ResourceManager ClientScriptResourceManager {
                get {
                    object o = HttpContext.Current.Items["LocalClientScriptRM"];
                    return o == null ? null : (ResourceManager)o;
                }
                set {
                    if( value == null )
                        HttpContext.Current.Items.Remove( "LocalClientScriptRM" );
                    else
                        HttpContext.Current.Items["LocalClientScriptRM"] = value;
                }
            }
        }
    
    }
    Now my revised version of the code as ClientScript.cs:
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.UI;
    using Ext.Net;
    
    namespace Ext.Net.Extension {
    
        /// <summary>
        /// Create Client Script string using Ext.Net Server-side api that can be attached to Control Handlers
        /// </summary>
        public class ClientScript : IDisposable {
    
            protected HttpContext Context = null;
            protected string script = null;
            protected ResourceManager internalrm = null;
            protected ResourceManager baserm = null;
    
            public ClientScript() : this( HttpContext.Current ) { }
            public ClientScript( HttpContext context ) {
                this.Context = context;
                SaveBaseRM();
            }
    
            /// <summary>
            /// Prefered method to indicate Server-side api will be scripted to string that can be used by a Control Handler.
            /// </summary>
            /// <param name="ScriptFn">Function or delegate who's internal contents will be scripted</param>
            public virtual void Script( Action ScriptFn ) {
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Create the temporary rm if necessary
                InitRM();
                // Set the Current RM to the internal rm before calling script fn
                SetRM( internalrm, true );
                // Run the code you want to convert to script
                ScriptFn();
                // Restore Current RM to base
                SetRM( baserm, false );
            }
    
    #if SupportingStartEnd
            public bool Running { get; private set; }
    
            /// <summary>
            /// Begin scripting to Client-side code. All server-side api calls between this call and ScriptEnd will be rendered to a string.
            /// Make sure to call ScriptEnd() or else Dispose will be left to the Garbage-Collector and that could be dangerous.
            /// </summary>
            public virtual void ScriptStart() {
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Create the temporary rm if necessary
                InitRM();
                // Set the Current RM to the internal rm before calling script fn
                Running = true;
                SetRM( internalrm, true );
            }
            /// <summary>
            /// End scripting to Client-side code. All server-side api calls will be rendered back to response writer.
            /// </summary>
            public virtual void ScriptEnd() {
                if( internalrm == null )
                    throw new Exception( "Script Rendering was already Started." );
                if( Context == null )
                    throw new Exception( "Script Rendering was already Disposed." );
                // Set the Current RM to the internal rm before calling script fn
                Running = false;
                SetRM( baserm, false );
            }
    
            /// <summary>
            /// Begin scripting to Client-side code the section inside a using statement. using( var cs = ClientScript.ScriptUsing(Context)) ) { ... }
            /// Make sure you are using a using section.
            /// </summary>
            /// <param name="Context"></param>
            /// <returns></returns>
            public static ClientScript ScriptUsing( HttpContext Context ) {
                ClientScript cs = new ClientScript( Context );
                cs.ScriptStart();
                return cs;
            }
    #endif
    
            /// <summary>
            /// Render the script to a string that can be used for Control Handlers
            /// </summary>
            /// <returns></returns>
            public virtual string ToScript() {
                // Build the script if required
                if( script == null && internalrm != null ) {
    #if SupportingStartEnd
                    // If Script was started, we need to end it
                    if( Running )
                        ScriptEnd();
    #endif
                    SetRM( internalrm, false );
                    script = internalrm.ToScript( true );
                    SetRM( baserm, false );
                    Dispose();
                }
                return this.script;
            }
    
            protected void SaveBaseRM() {
                // Check if we're already storing the BaseRM
                object o = Context.Items["BaseRM"];
                if( o == null )
                    // Save the original ResourceManager
                    Context.Items["BaseRM"] = baserm = ResourceManager.GetInstance( Context );
                else
                    baserm = (ResourceManager)o;
            }
    
            protected void InitRM() {
                // If the temp rm is already initialized, exit
                if( internalrm != null )
                    return;
                // Set scope of ResourceManager to new SelfRendering Page Context
                System.Web.UI.Page pageHolder = (System.Web.UI.Page)new SelfRenderingPage();
                internalrm = new ResourceManager( true );
                internalrm.RenderScripts = ResourceLocationType.None;
                internalrm.RenderStyles = ResourceLocationType.None;
                internalrm.IDMode = baserm.IDMode;
                pageHolder.Controls.Add( internalrm );
            }
    
            protected void SetRM( ResourceManager rm, bool setClientScripting ) {
                // Replace the Page/Context ResourceManager with the one specified
                if( Context.CurrentHandler is Page )
                    ( (Page)Context.CurrentHandler ).Items[typeof( ResourceManager )] = rm;
                else
                    Context.Items[typeof( ResourceManager )] = rm;
                // SetClientScriptResourceManager to internal if true
                ResourceManager.ClientScriptResourceManager = setClientScripting ? internalrm : null;
            }
    
            public void Dispose() {
                // Do nothing if already disposed
                if( Context == null )
                    return;
                // If script not already rendered, render it now
                if( this.script == null )
                    this.script = ToScript();
                // Release resources
                internalrm = null;
                baserm = null;
                Context = null;
            }
    
        }
    }

    Now here's the revised TestCode.cs.
    Note that I now assign the client-side DirectMethod as a server-side X.AddScript during Refresh. Also, inside the DirectMethod, I now also set the same label in both the ClientScript delegate and outside of it and both behave correctly.
    <%@ Page Language="C#" EnableViewState="false" ClassName="Test45" %>
    <%@ Import Namespace="Ext.Net.Extension" %>
    
    <script runat="server">
        protected void Page_Load( object sender, EventArgs e ) {
            // Shows that ClientRender works in Page_Load (Page Refresh)
            var Renderer = new ClientScript( Context );
            Renderer.Script( () => {
                // Any server-side code placed in here will render to client-side string
                X.AddScript( "App.direct.Click();" );
            } );
            Button1.OnClientClick = Renderer.ToScript();
        }
        [DirectMethod( IDMode = DirectMethodProxyIDMode.None, ShowMask = true )]
        public void Click() {
            X.MessageBox.Alert( "Test", "Inside Test." ).Show();
            Label1.Text = "Changed Label Inside Test";
            var Renderer = new ClientScript( Context );
            Renderer.Script( () => {
                // Any server-side code placed in here will render to client-side string
                X.MessageBox.Alert( "Test", "Post-2nd Click Test." ).Show();
                Label1.Text = "Changed Label Click Test";
            } );
            Button1.OnClientClick = Renderer.ToScript();
            Button1.Text = "MsgBox";
            Button1.Update();
        }
    </script>
    
    <!DOCTYPE html>
    <html>
    <head id="Head1" runat="server">
        <title>Test45 Sample</title>
    </head>
    <body>
        <form id="Form1" runat="server">
    
            <ext:ResourceManager ID="ResourceManager1" runat="server" ScriptMode="Development" SourceFormatting="true" IDMode="Explicit" />
            <ext:Viewport ID="vp" runat="server" Layout="HBoxLayout">
                <Items>
                    <ext:Panel ID="LP" runat="server" Border="true" Padding="5" Flex="1" Layout="FitLayout" Title="Test45.aspx">
                        <Items>
                            <ext:Label ID="Label1" runat="server" Html="Outer" />
                        </Items>
                        <Buttons>
                            <ext:Button ID="Button1" runat="server" Text="Click"  />
                        </Buttons>
                    </ext:Panel>
                </Items>
            </ext:Viewport>
        </form>
    </body>
    </html>
    Last edited by michaeld; Feb 04, 2014 at 9:40 AM.
  5. #5

    If you want a ComponentLoader implementation as well...

    For completeness, I implemented an extension for ComponentLoader to render code in addition to AbstractControls and UserControls. This code is untested though because I don't use ComponentLoader, but the intuition should follow.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.UI;
    using Ext.Net;
    using Ext.Net.Utilities;
    
    namespace Ext.Net.Extension {
    
        public partial class XComponentLoader : ComponentLoader {
            public XComponentLoader() { }
    
            public static void Render( HttpContext Context, Action ScriptFn ) {
                CompressionUtils.GZipAndSend( ToConfig( Context, ScriptFn ) );
            }
            public static void Render( Action ScriptFn ) {
                CompressionUtils.GZipAndSend( ToConfig( ScriptFn ) );
            }
    
            public static string ToConfig( Action ScriptFn ) {
                return ToConfig( HttpContext.Current, ScriptFn );
            }
            public static string ToConfig( HttpContext Context, Action ScriptFn ) {
                var Renderer = new ClientScript( Context );
                Renderer.Script( ScriptFn );
                return Renderer.ToScript();
            }
    
            public static void Render( HttpContext Context, ClientScript Renderer ) {
                CompressionUtils.GZipAndSend( ToConfig( Context, Renderer ) );
            }
            public static void Render( ClientScript Renderer ) {
                CompressionUtils.GZipAndSend( ToConfig( Renderer ) );
            }
    
            public static string ToConfig( ClientScript Renderer ) {
                return ToConfig( HttpContext.Current, Renderer );
            }
            public static string ToConfig( HttpContext Context, ClientScript Renderer ) {
                return Renderer.ToScript();
            }
    
        }
    
    }
    Last edited by michaeld; Mar 07, 2014 at 10:00 AM.

Similar Threads

  1. Replies: 3
    Last Post: Dec 26, 2011, 1:32 PM
  2. Replies: 1
    Last Post: Dec 01, 2010, 5:14 PM
  3. Replies: 4
    Last Post: Mar 19, 2010, 11:35 AM
  4. Replies: 6
    Last Post: Sep 01, 2009, 1:06 PM

Posting Permissions