Common Custom tasks
Common_CustomTasks_CSharp\ScriptTask_CSharp\ScriptTask.cs
// Copyright 2010 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 

using System.Web.UI;
using System;
namespace ESRI.ADF.Samples.CustomTasks
{
    /// <summary>
    /// When implemented, this class automatically creates a client-side representation of your custom task.
    /// Any method or control included in the task can be made available on the client-side class by 
    /// adding the ClientMember attribute to the member declaration.  The client-side class is created as
    /// an AJAX control with a component ID matching the task's client ID, and can therefore be retrieved
    /// by a call to $find(task client ID).
    /// </summary>
    public abstract class ScriptTask : ESRI.ArcGIS.ADF.Web.UI.WebControls.FloatingPanelTask
    {

        #region ASP.NET WebControl Life Cycle Event Overrides - OnPreRender, Render

        // Declares the client-side task class and its members
        protected override void OnPreRender(System.EventArgs eventArgs)
        {
            base.OnPreRender(eventArgs);

            // Instantiate a StringBuilder to hold the initialization JavaScript
            System.Text.StringBuilder initializeClientTaskScript = new System.Text.StringBuilder();

            // Get the type of the task (sub-class) implementing ScriptTask
            System.Type subClassType = this.GetType();

            // Check whether the ClientTaskDefinitionExplicit custom attribute was declared on the 
            // sub-class.  If so, the attribute instance will be passed to the array.
            object[] definitionExplicitArray = 
                subClassType.GetCustomAttributes(typeof(ClientTaskDefinitionExplicit), false);

            // Check whether the ClientTaskJavaScriptResource custom attribute was declared on the 
            // sub-class.  If so, the attribute instance will be passed to the array.
            object[] javaScriptResourceArray =
                subClassType.GetCustomAttributes(typeof(ClientTaskJavaScriptResource), false);

            // If neither array contains any attributes, then the client task class will need to be
            // created (i.e. declared) here.  Set a boolean indicating whehter or not this is the case.
            bool createClientClass = (definitionExplicitArray.Length == 0) &&
                (javaScriptResourceArray.Length == 0);

            // Check whether the ClientTaskClassName custom attribute was declared on the 
            // sub-class.  If so, the attribute instance will be passed to the array.
            object[] clientClassNameArray =
                subClassType.GetCustomAttributes(typeof(ClientTaskClassName), false);
            if (clientClassNameArray.Length > 0)
            {
                // Store the class name specified in the attribute declaration
                ClientTaskClassName clientClassNameAttribute =
                    clientClassNameArray[0] as ClientTaskClassName;
                this.ClientClassName = clientClassNameAttribute.ClassName;
            }
            else
            {
                // Set the client class name to match that of the server-side task
                this.ClientClassName = this.GetType().ToString();
            }

            if (createClientClass)
            {
                // The sub-class has not explicitly declared a client-side task class (default).  Generate
                // JavaScript to declare the class and register it as an AJAX control inheriting from
                // ESRI.ADF.UI.FloatingPanel.
                string classDeclarationScript = CreateClassDeclarationScript();
                // Add the class declaration to the initialization script
                initializeClientTaskScript.Append(classDeclarationScript);
            }
            else
            {
                // Initialize a boolean indicating whether a script resource needs to be registered for the 
                // client-side task
                bool registerScriptResource = true;

                // Check whether the class was declared with a ClientTaskJavaScriptResource attribute
                if (javaScriptResourceArray.Length > 0)
                {
                    // Get the path specified by the ClientTaskJavaScriptResource
                    ClientTaskJavaScriptResource javaScriptResourceAttribute =
                        javaScriptResourceArray[0] as ClientTaskJavaScriptResource;
                    this.JavaScriptResourceName = javaScriptResourceAttribute.ResourceName;
                }
                else
                {
                    // Create a default resource name from the sub-class's task name and the JavaScript file
                    // extension
                    this.JavaScriptResourceName = 
                        string.Format("{0}.js", this.GetType().ToString());

                    // Attempt to retrieve an object containing information about the resource.  If the 
                    // resource does not exist, this will return null.
                    System.Reflection.ManifestResourceInfo resourceInfo = 
                        subClassType.Assembly.GetManifestResourceInfo(this.JavaScriptResourceName);

                    // VB may not prepend the namespace to the resource, so just retrieve using the basic resource name. 
                    if (resourceInfo == null)
                    {
                        this.JavaScriptResourceName = string.Format("{0}.js", this.GetType().Name.ToString());
                        resourceInfo = subClassType.Assembly.GetManifestResourceInfo(this.JavaScriptResourceName);
                    }

                    // Based on whether the resource was found, set a boolean indicating whether the resource
                    // should be registered
                    registerScriptResource = (resourceInfo != null);
                }

                // Register the client script resource, if necessary
                if (registerScriptResource)
                {
                    if (this.ScriptManager != null)
                    {
                        ScriptReference scriptRef = new ScriptReference(this.JavaScriptResourceName, subClassType.Assembly.FullName);
                        this.ScriptManager.Scripts.Add(scriptRef);
                    }
                    else
                    {
                        System.Web.UI.ScriptManager.RegisterClientScriptResource(this,
                            subClassType, this.JavaScriptResourceName);
                    }                    
                }
            }

            // Generate JavaScript that will add server-side members declared with the custom ClientMember
            // attribute to the client-side task.
            string addClientMembersScript = CreateAddClientMembersScript();
            // Add the member-adding logic to the intialization script
            initializeClientTaskScript.Append(addClientMembersScript);

            // Enclose the initialization script in a function declaration and add the function as a handler 
            // of the AJAX init event, so the logic executes during application initialization
            string classInitFunctionName = string.Format("{0}_initializeClientTaskMembers",
                subClassType.Name);
            initializeClientTaskScript.Insert(0, "function " + classInitFunctionName + "() {");
            initializeClientTaskScript.Append("} Sys.Application.add_init(" + classInitFunctionName + ");");

            string scriptKey = string.Format("initialize{0}", subClassType.ToString());
            if (!this.Page.ClientScript.IsStartupScriptRegistered(scriptKey))
            {
                // Register the initialization script with the client
                System.Web.UI.ScriptManager.RegisterStartupScript(this, this.GetType(),
                    scriptKey, initializeClientTaskScript.ToString(), true);
            }
        }

        // Instantiates the client-side task
        protected override void Render(System.Web.UI.HtmlTextWriter writer)
        {
            base.Render(writer);

            string instatiationFuncName = string.Format("create{0}", this.UniqueID);

            // FloatingPanelTask, from which ScriptTask inherits, has already instantiated a client-side 
            // ESRI.ADF.UI.FloatingPanel with an AJAX component ID that matches the task's client ID.
            // Since the client-side script task inherits from ESRI.ADF.UI.FloatingPanel, we can replace
            // the already instantiated FloatinPanel with a an instance of our script task.  First, we
            // must retrieve the existing FloatingPanel and copy its initialization properties.  Then,
            // the FloatingPanel must be disposed.  Finally, we instantiate the client-side script task
            // via the AJAX $create function, specifying the client ID of the ScriptTask's sub-class and
            // the properties of the disposed FloatingPanel.
            //
            // NOTE: this step must be performed after base.Render because that is where the client-side 
            // FloatingPanel is instantiated.
            string createClientTaskScript = @"function {0}() {{ 
                    var floatingPanel = $find('{1}');
                    var properties = {{ id:'{1}', ""transparency"":floatingPanel.get_transparency(),
                        ""callbackFunctionString"":floatingPanel.get_callbackFunctionString(),
                        ""expandedImage"":floatingPanel.get_expandedImage(),
                        ""collapsedImage"":floatingPanel.get_collapsedImage(),
                        ""dockedImage"":floatingPanel.get_dockedImage(),
                        ""undockedImage"":floatingPanel.get_undockedImage(),
                        ""dockedContextMenuID"":floatingPanel.get_dockedContextMenuID(),
                        ""dockParent"":floatingPanel.get_dockParent(), 
                        ""forcePNG"":floatingPanel.get_forcePNG(), 
                        ""isDocked"":floatingPanel.get_isDocked() }};
                    floatingPanel.dispose();
                    $create({2}, properties, null, null, $get('{1}'));
                }}
                Sys.Application.add_init({0});";
            createClientTaskScript = string.Format(createClientTaskScript, instatiationFuncName, 
                this.ClientID, this.ClientClassName);

            System.Web.UI.ScriptManager.RegisterStartupScript(this, this.GetType(),
                this.ClientID + "_createClientTask", createClientTaskScript, true);

            string addClientFieldsScript = GetAddClientFieldsScript();
            System.Web.UI.ScriptManager.RegisterStartupScript(this, this.GetType(),
                this.ClientID + "_addClientFields", addClientFieldsScript, true);
        }

        private string GetAddClientFieldsScript()
        {
            // Instantiate a filter that will be used to find members of the sub-class with the 
            // ClientMember attribute.  The MemberHasCustomAttribute method passed into the constructor
            // contains the filtering logic.
            System.Reflection.MemberFilter customAttributeFilter =
                new System.Reflection.MemberFilter(MemberHasCustomAttribute);

            // Find the methods and fields (instance variables) that are marked with the ClientMember
            // attribute.  The first parameter to FindMembers specifies that instance, static, public,
            // and non-public fields and methods are to be searched; the second specifies the filter
            // to use; and the third specifies the attribute to look for.
            System.Reflection.MemberInfo[] clientMemberInfoArray = this.GetType().FindMembers(
                System.Reflection.MemberTypes.Method | System.Reflection.MemberTypes.Field,
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static |
                System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic,
                customAttributeFilter, typeof(ClientMember));

            System.Reflection.MethodInfo clientMethodInfo = null;
            System.Reflection.FieldInfo clientFieldInfo = null;

            System.Text.StringBuilder addClientFieldsScript = new System.Text.StringBuilder();
            addClientFieldsScript.Append("Sys.Application.add_init(function() {\n");
            addClientFieldsScript.AppendFormat("var task = $find('{0}');", this.ClientID);

            // Iterate through the members found
            foreach (System.Reflection.MemberInfo memberInfo in clientMemberInfoArray)
            {
                // Check whether the current member is a method
                clientMethodInfo = memberInfo as System.Reflection.MethodInfo;
                if (clientMethodInfo == null)
                {
                    // Since the member is not a method, and only methods and fields were sought in the
                    // FindMembers call, the member must be a field.  Cast the member to the FieldInfo
                    // type, from which field-specific information can be retrieved.
                    clientFieldInfo = memberInfo as System.Reflection.FieldInfo;

                    // Make sure the field is a control.  If it is not, skip it.
                    if (typeof(System.Web.UI.Control).IsAssignableFrom(clientFieldInfo.FieldType))
                    {
                        // Create JavaScript to add a function to the new client-side property that will return the
                        // client-side task instance (AJAX component).  This allows easy retrieval of the parent
                        // task when only the element referenced by the property is available.
                        addClientFieldsScript.AppendFormat("task.get_{1}().get_task = function() {{ return $find('{0}'); }};\n",
                            this.ClientID, clientFieldInfo.Name);
                    }
                }
            }

            addClientFieldsScript.Append("});\n");
            return addClientFieldsScript.ToString();
        }

        #endregion

        #region ICallbackEventHandler Members - GetCallbackResult

        // Invokes server-side methods of the ScriptTask's sub-class that have been exposed on the client via
        // the custom ClientMember attribute.
        public override string GetCallbackResult()
        {
            // Parse the message sent to the server via the callback using the Web ADF's parsing utility
            System.Collections.Specialized.NameValueCollection callbackArgsCollection =
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackUtility.ParseStringIntoNameValueCollection(
                this.CallbackEventArgument);

            // Make sure the callback event argument is "InvokeMethod" before executing method invocation 
            // logic
            if ((callbackArgsCollection["EventArg"] != null) &&
                (callbackArgsCollection["EventArg"] == "InvokeMethod"))
            {
                // Get the name of the method to be invoked and the parameters to pass to the method from
                // the callback argument
                string methodName = callbackArgsCollection["MethodName"];

                string[] methodParameterStrings = callbackArgsCollection["Parameters"].Split('|');


                // Instantiate a list to store the methods after they have been cast to the types expected
                // by the method.  When they are first received from the client, they are all strings.
                System.Collections.Generic.List<object> methodParameterList = 
                    new System.Collections.Generic.List<object>();
                // Iterate through the methods exposed to the client 
                foreach (System.Reflection.MethodInfo clientMethodInfo in this.ClientMethodInfoList)
                {
                    // Check whether the current method is the one to be invoked
                    if (clientMethodInfo.Name == methodName)
                    {
                        int i = 0;
                        // Iterate through the method's parameters
                        foreach (System.Reflection.ParameterInfo parameterInfo in clientMethodInfo.GetParameters())
                        {
                            // Cast the current parameter's string representation to the type expected by the 
                            // method and store it in the parameter list
                            methodParameterList.Add(System.Convert.ChangeType(methodParameterStrings[i],
                                parameterInfo.ParameterType));
                            i++;
                        }

                        break;
                    }
                }
                
                // Convert the parameter list to an array so it can be passed to InvokeMember
                object[] methodParameterArray = methodParameterList.ToArray();
                // Get the sub-class type of the task instance that implemented ScriptTask
                System.Type subClassType = this.GetType();

                // Invoke the method on the sub-class.  The first parameter specifies the method to be invoked;
                // the second specifies the type of members to look for when finding the method to invoke; the
                // fourth specifies the instance on which to invoke the method; and the fifth specifies the 
                // arguments to pass to the method invocation.
                object result = subClassType.InvokeMember(methodName, System.Reflection.BindingFlags.InvokeMethod |
                    System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static |
                    System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null,
                    this, methodParameterArray);

                // Convert the method result to a string
                string resultString = (result == null) ? "" : result.ToString();

                // Construct the JavaScript necessary to fire the method completion logic on the client-side
                // script task, passing the method result to the client function.
                string fireMethodCompleteEvent = string.Format("var task = $find('{0}'); " +
                    "task._on{1}Complete('{2}');", this.ClientID, methodName, resultString);

                // Create a callback result enapsulating the JavaScript and add it to the task's callback results
                // so it is executed when program flow returns to the client.
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult methodCompleteEventCallbackResult =
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(fireMethodCompleteEvent);
                this.CallbackResults.Add(methodCompleteEventCallbackResult);
            }
            
            return base.GetCallbackResult();
        }

        #endregion

        #region FloatingPanelTask Members - ExecuteTask, GetGISResourceItemDependencies

        // Overriden here so it does not have to be implemented by sub-classes.
        public override void ExecuteTask()
        {
            return;
        }

        // Overriden to return an empty list, indicating that the task has no resource dependencies.  
        // Overriden here so it does not have to be implemented by sub-classes.
        public override System.Collections.Generic.List<
            ESRI.ArcGIS.ADF.Web.UI.WebControls.GISResourceItemDependency> GetGISResourceItemDependencies()
        {
            return new System.Collections.Generic.List<ESRI.ArcGIS.ADF.Web.UI.WebControls.GISResourceItemDependency>();
        }

        #endregion

        #region Instance Properties - ClientMethodInfoList, JavaScriptResourceName, ClientClassName

        /// <summary>
        /// Stores information about all the methods made accessible on the client via the 
        /// ClientMember attribute
        /// </summary>
        private System.Collections.Generic.List<System.Reflection.MethodInfo> ClientMethodInfoList
        {
            get
            {
                return StateManager.GetProperty("ClientMethodInfo") as
                    System.Collections.Generic.List<System.Reflection.MethodInfo>;
            }
            set
            {
                StateManager.SetProperty("ClientMethodInfo", value);
            }
        }

        /// <summary>
        /// Stores the fully qualified name of the JavaScript file containing the client task's
        /// definition
        /// </summary>
        private string JavaScriptResourceName
        {
            get
            {
                return StateManager.GetProperty("JavaScriptResourceName") as string;
            }
            set
            {
                StateManager.SetProperty("JavaScriptResourceName", value);
            }
        }

        /// <summary>
        /// Stores the class name of the client task
        /// </summary>
        private string ClientClassName
        {
            get
            {
                return StateManager.GetProperty("ClientClassName") as string;
            }
            set
            {
                StateManager.SetProperty("ClientClassName", value);
            }
        }

        #endregion

        #region Instance Methods - MemberHasCustomAttribute, Script Generation Methods

        /// <summary>
        /// Method used as a delegate to check whether a class member has a particular custom 
        /// attribute
        /// </summary>
        /// <param name="memberInfo">Contains information about the class member to be checked
        /// </param>
        /// <param name="filterAttributeObject">The custom attribute sought</param>
        /// <returns></returns>
        private static bool MemberHasCustomAttribute(System.Reflection.MemberInfo memberInfo,
            object filterAttributeObject)
        {
            // Get the custom attribute as a type
            System.Type filterAttributeType = (System.Type)filterAttributeObject;

            // Check whether the member referred to by the passed in MemberInfo object has the custom
            // attribute.  
            object[] customAttributeArray = memberInfo.GetCustomAttributes(filterAttributeType, false);

            // If the member has the custom attribute, the method call above will place it in the array.
            // So return true or false based on whether the array has a length greater than 0.
            return (customAttributeArray.Length > 0);
        }

        #region Client Task Script Generation Methods

        /// <summary>
        /// Generates JavaScript to declare the client-side task such that it is registered as 
        /// an AJAX control inheriting from ESRI.ADF.UI.FloatingPanel
        /// </summary>
        private string CreateClassDeclarationScript()
        {
            string clientTaskNamespace = "";
            string namespaceRegistrationScript = "";
            // Check whether the client-side task's class name contains a "."  If so, then we need to
            // ensure that everything preceding the last "." is registered as an AJAX namespace on the
            // clent.
            if (this.ClientClassName.Contains("."))
            {
                // Get the text preceding the last "." and construct the JavaScript needed to register
                // that text as an AJAX namespace.
                clientTaskNamespace = this.ClientClassName.Substring(0, 
                    this.ClientClassName.LastIndexOf("."));
                namespaceRegistrationScript = string.Format("Type.registerNamespace('{0}');", 
                    clientTaskNamespace);
            }

            // Create JavaScript to declare the client-side task class and register it as an AJAX
            // control inheriting from ESRI.ADF.UI.FloatingPanel
            string classDeclarationScript = string.Format(@"
                    {0} = function(element) {{
                        {0}.initializeBase(this, [element]);
                    }}
                    {0}.prototype = {{    
                        initialize : function() {{          
                            // Initialize the base class
                            {0}.callBaseMethod(this, 'initialize'); 
                        }},  
                        // Returns the object's id.  Required for AJAX component registration.
                        get_id : function () {{
                            return this._id;
                        }},                    
                        // Sets the object's id.  Required for AJAX component registration.
                        set_id : function (value) {{
                            this._id = value;
                        }}  
                    }};

                    // Register the task class, specifying inheritance from ESRI.ADF.UI.FloatingPanel.
                    // This will give the task all the properties of FloatingPanel and will make it
                    // an AJAX control, since FloatingPanel is an AJAX control.
                    {0}.registerClass('{0}', ESRI.ADF.UI.FloatingPanel);",
                this.ClientClassName);

            // Return the namespace and class declaration syntax
            return namespaceRegistrationScript + classDeclarationScript;
        }

        /// <summary>
        /// Generates JavaScript to expose server-side members that have been declared with the ClientMember
        /// attribute to the client-side task
        /// </summary>
        private string CreateAddClientMembersScript()
        {
            // Instantiate a StringBuilder to store the JavaScript
            System.Text.StringBuilder clientMemberDeclarationScript = new System.Text.StringBuilder();

            // Instantiate a filter that will be used to find members of the sub-class with the 
            // ClientMember attribute.  The MemberHasCustomAttribute method passed into the constructor
            // contains the filtering logic.
            System.Reflection.MemberFilter customAttributeFilter =
                new System.Reflection.MemberFilter(MemberHasCustomAttribute);
            
            // Get the type of the task implementing ScriptTask
            System.Type subClassType = this.GetType();
            
            // Find the methods and fields (instance variables) that are marked with the ClientMember
            // attribute.  The first parameter to FindMembers specifies that instance, static, public,
            // and non-public fields and methods are to be searched; the second specifies the filter
            // to use; and the third specifies the attribute to look for.
            System.Reflection.MemberInfo[] clientMemberInfoArray = subClassType.FindMembers(
                System.Reflection.MemberTypes.Method | System.Reflection.MemberTypes.Field,
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static |
                System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic,
                customAttributeFilter, typeof(ClientMember));

            System.Reflection.MethodInfo clientMethodInfo = null;
            System.Reflection.FieldInfo clientFieldInfo = null;
            // Iterate through the members found
            foreach (System.Reflection.MemberInfo memberInfo in clientMemberInfoArray)
            {
                // Check whether the current member is a method
                clientMethodInfo = memberInfo as System.Reflection.MethodInfo;
                if (clientMethodInfo == null)
                {
                    // Since the member is not a method, and only methods and fields were sought in the
                    // FindMembers call, the member must be a field.  Cast the member to the FieldInfo
                    // type, from which field-specific information can be retrieved.
                    clientFieldInfo = memberInfo as System.Reflection.FieldInfo;
                    
                    // Make sure the field is a control.  If it is not, program flow will go to the next
                    // loop iteration.
                    if (typeof(System.Web.UI.Control).IsAssignableFrom(clientFieldInfo.FieldType))
                    {
                        // Generate the JavaScript needed to expose the control on the client-side task.
                        // The control will have the same name as specified in the server-side declaration
                        // and will refere to the control's client-side DOM element.
                        string addControlScript = CreateAddClientControlScript(clientFieldInfo);
                        clientMemberDeclarationScript.Append(addControlScript);
                    }
                }
                else
                {
                    // The current member is a method.

                    // If the list storing information about all the methods exposed to the client hasn't 
                    // been isntantiated, do so now
                    if (this.ClientMethodInfoList == null)
                    {
                        this.ClientMethodInfoList =
                            new System.Collections.Generic.List<System.Reflection.MethodInfo>();
                    }

                    // Add the method to the list
                    this.ClientMethodInfoList.Add(clientMethodInfo);

                    // Generate JavaScript to expose the method on the client-side task.  The method will
                    // have the same signature as that specified in its server-side declaration and, when
                    // called, will fire a callback that automatically passes the client-side parameters
                    // to the equivalent server-side method.
                    string addClientMethodScript = CreateAddClientMethodScript(clientMethodInfo);
                    clientMemberDeclarationScript.Append(addClientMethodScript);

                    // Generate JavaScript to add eventing for server-side method calls to the client-side 
                    // task.  This will allow the specification of handlers that execute when the method 
                    // has fired on the server and program flow returns to the client.  Handlers can be 
                    // added via <task>.add_on<methodName>Complete and removed via 
                    // <task>.remove_on<methodName>Complete.
                    string addClientMethodEventScript = CreateAddMethodEventScript(clientMethodInfo);
                    clientMemberDeclarationScript.Append(addClientMethodEventScript);
                }
            }

            return clientMemberDeclarationScript.ToString();
        }

        /// <summary>
        /// Generates JavaScript to add the control specified to the client-side task
        /// </summary>
        /// <param name="controlMemberInfo">Contains information about the control to expose</param>
        private string CreateAddClientControlScript(System.Reflection.FieldInfo controlMemberInfo)
        {
            // Instantiate a StringBuilder to store the JavaScript
            System.Text.StringBuilder addClientControlScript = new System.Text.StringBuilder();

            // Get the control instance
            System.Web.UI.Control control = controlMemberInfo.GetValue(this) as
                System.Web.UI.Control;

            // Create JavaScript to create a property on the client-side task that refers to the
            // DOM element corresponding to the passed-in control and has the same name as the
            // server-side field (instance variable) storing the control.
            string id = null;
            try
            {
                id = control.ID.ToString();
            }
            catch (Exception)
            {
                control.ID = controlMemberInfo.Name;
            }

            string s = string.Format("{0}.prototype.get_{1} = function()" +
                "{{ return $get(this.get_id() + '_{2}'); }};\n",
                this.ClientClassName, controlMemberInfo.Name, control.ID);
            addClientControlScript.AppendFormat("{0}.prototype.get_{1} = function()" +
                "{{ return $get(this.get_id() + '_{2}'); }};\n",
                this.ClientClassName, controlMemberInfo.Name, control.ID);

            return addClientControlScript.ToString();
        }

        /// <summary>
        /// Generates JavaScript to add the method specified to the client-side task.  When called, this
        /// method will invoke the server-side method of the same name.
        /// </summary>
        /// <param name="clientMethodInfo">Contains information about the method to expose</param>
        private string CreateAddClientMethodScript(System.Reflection.MethodInfo clientMethodInfo)
        {
            // Instantiate StringBuilder to store the JavaScript
            System.Text.StringBuilder addClientMethodScript = new System.Text.StringBuilder();

            // Declare a function on the client-side task that has the same name as that passed-in
            addClientMethodScript.AppendFormat("{0}.prototype.{1} = function(",
                this.ClientClassName, clientMethodInfo.Name);

            // Iterate through the method's parameters, adding the name of each to the function signature,
            // separated by commas
            foreach (System.Reflection.ParameterInfo parameterInfo in clientMethodInfo.GetParameters())
            {
                addClientMethodScript.Append(parameterInfo.Name + ", ");
            }

            // Remove the last space and comma if the method has parameters
            if (clientMethodInfo.GetParameters().Length > 0)
                addClientMethodScript.Remove(addClientMethodScript.Length - 2, 2);

            // Close the function signature and add the opening curly brace
            addClientMethodScript.Append(") {\n");

            // Add the beginning of a setTimeout statement.  Note that we put the callback invocation in
            // a timeout with a delay of 0 to ensure proper execution of nested callbacks.  Otherwise,
            // callbacks that are invoked via ScriptTask ClientMembers before a previous callback has 
            // finished processing on the client would throw an error.
            addClientMethodScript.Append("window.setTimeout(String.format(\"var context = '{0}';\", this.get_id()) + ");

            // Declare the argument variable and start setting its value.  The string will be in key-value
            // pairs in the format <key1>=<value1>&<key2>=<value2>&...  The string is left incomplete here
            // because the names of the method parameters need to be added to it
            addClientMethodScript.AppendFormat("\"var argument = 'EventArg=InvokeMethod&MethodName={0}" +
                "&Parameters=\" + ", clientMethodInfo.Name);

            // Iterate through the method's parameters, adding the name of each to the argument string.
            // Note that these are not placed in quotes so they are evaluated at run-time.  Each is 
            // connected with " + \"|\" +" so that, at run-time, the result is a string with the value
            // of each parameter separated by a vertical line.
            foreach (System.Reflection.ParameterInfo parameterInfo in clientMethodInfo.GetParameters())
            {
                addClientMethodScript.Append(parameterInfo.Name + " + \"|\" + ");
            }

            // If the method has parameters, remove the last separator.  And add a closing double quote.
            // Otherwise, remove the plus sign.
            if (clientMethodInfo.GetParameters().Length > 0)
            {
                addClientMethodScript.Remove(addClientMethodScript.Length - 9, 9);
                addClientMethodScript.Append("+ \"");
            }
            else
            {
                addClientMethodScript.Remove(addClientMethodScript.Length - 4, 4);
            }

            addClientMethodScript.Append("';");

            // Add an invocation of the task's callback function string.  This will pass the argument 
            // variable to the server-side ScriptTask's GetCallbackResult function, where the 
            // server-side method specified by the MethodName parameter will be invoked.  Note that, since
            // the statement is being passed to a timeout, we need to add an extra pair of backslashes so
            // the quotes surrounding the callback string are escaped properly at run-time.

            addClientMethodScript.Append("eval(\\\"\" + this.get_callbackFunctionString() + \"\\\");\", 0);\n");
            //addClientMethodScript.AppendFormat("eval(\\\"{0}\\\");\", 0);\n", this.CallbackFunctionString);


            // Add the function's closing curly brace
            addClientMethodScript.Append("};\n");

            return addClientMethodScript.ToString();
        }

        /// <summary>
        /// Generates JavaScript to add eventing for the method specified to the client-side task.  This
        /// allows for easily specifying logic to execute after a server-side method has been invoked 
        /// and program flow has returned to the client.
        /// </summary>
        /// <param name="clientMethodInfo">Contains information about the method to expose</param>
        private string CreateAddMethodEventScript(System.Reflection.MethodInfo clientMethodInfo)
        {
            // Create JavaScript that will add event-related methods to the task
            string addMethodInvocationEventScript = @"
                // Adds a handler that will fire after the {1} method has executed on the server
                {0}.prototype.add_on{1}Complete = function(handler, before) {{
                    if (!this._on{1}CompleteHandlers)
                        this._on{1}CompleteHandlers = new Array();

                    if (before)
                        this._on{1}CompleteHandlers.splice(0, 0, handler);
                    else
                        this._on{1}CompleteHandlers.push(handler);
                }};

                // Removes the passed-in handler
                {0}.prototype.remove_on{1}Complete = function(handler) {{
                    for (var i = 0; i < this._on{1}CompleteHandlers.length; i++)
                    {{
                        if (this._on{1}CompleteHandlers[i] == handler)
                        {{
                            this._on{1}CompleteHandlers.splice(i, 1);
                            break;
                        }}
                    }}
                }};

                // Called by ScriptTask after the {1} method has executed on the server.  Fires
                // all handlers added via add_on{1}Complete.
                {0}.prototype._on{1}Complete = function(result)
                {{
                    // Iterate through the task's handlers array for the on{1}Complete event
                    for (var i = 0; i < this._on{1}CompleteHandlers.length; i++)
                    {{
                        // Assign the current handler to a temporary property on the task.  We do 
                        // this so that, when the handler is called, the task is the caller.  
                        // Therefore, references to ""this"" in a handler will refer to the task.
                        this._temp = this._on{1}CompleteHandlers[i];

                        // Execute the handler and check to see whether it returns false. If false 
                        // is returned, exit the loop, skipping execution of any other handlers.
                        if (this._temp(result) == false)
                            break;
                    }}
                    
                    this._temp = 'undefined';
                }};";

            // Substitute the client-side task name and method name into the script and return it
            addMethodInvocationEventScript = string.Format(addMethodInvocationEventScript,
                this.ClientClassName, clientMethodInfo.Name);
            return addMethodInvocationEventScript;
        }
        
        #endregion

        #endregion

        #region Custom Attributes - ClientMember, ClientTaskDefinitionExplicit, ClientTaskJavaScriptResource, ClientTaskClassName

        /// <summary>
        /// Custom attribute specifying that the associated member that should be made available on the
        /// task's client-side representation.  Can only be declared on methods and fields (instance
        /// members) that are controls.
        /// </summary>
        [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Field,
            AllowMultiple = false)]
        public class ClientMember : System.Attribute { }

        /// <summary>
        /// Custom attribute specifying that the client task has been explicitly declared in the 
        /// ScriptTask implementation.  When neither this attribute nor ClientTaskJavaScriptResource 
        /// are present, the client task is declared automatically.  Only valid for classes.
        /// </summary>
        [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
        public class ClientTaskDefinitionExplicit : System.Attribute { }

        /// <summary>
        /// Custom attribute specifying the fully qualified name of the JavaScript file containing 
        /// the client task declaration.  When this attribute and ClientTaskDefinitionExplicit are 
        /// not present, the client task is declared automatically.  Only valid for classes.
        /// </summary>
        [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
        public class ClientTaskJavaScriptResource : System.Attribute
        {
            private string _resourceName;

            public ClientTaskJavaScriptResource(string resourceName)
            {
                _resourceName = resourceName;
            }

            public string ResourceName
            {
                get { return _resourceName; }
            }
        }

        /// <summary>
        /// Custom attribute specifying the name of the client task class.  If not declared, the
        /// client task class name will automatically match that of the server-side task.
        /// </summary>
        [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
        public class ClientTaskClassName : System.Attribute
        {
            private string _className;

            public ClientTaskClassName(string className)
            {
                _className = className;
            }

            public string ClassName
            {
                get { return _className; }
            }
        }

        #endregion
    }
}