Wednesday, January 2, 2008

Calling page methods from javascript by method name (ASP.NET AJAX)

Last time we have created a page with ability to call server-side static method from javascript. Now I want to expand the functionality and add the feature that will allow us to call the set of server-side methods with different parameters for each of them from single javascript method.
I suppose the following format for js method:

CallServer(methodName, targetMethod, parameters)

Here methodName is string that matches the name of server method,
targetMethod is javascript method name (string), that should be executed when server returns the results of execution,
parameters - is the set of parameters(object) that are passed to server method.

First let's create a specialized c# class, that will contain all the methods that can be called from the client browser. The class looks like:

using System;
using System.Reflection;

namespace Devarchive_net
{
    public class Command
    {
        #region Command functionality

        private string m_CommandName = "";

        public Command(string commandName)
        {
            m_CommandName = commandName;
        }

        public static Command Create(string commandName)
        {
            return new Command(commandName);
        }

        public object Execute(object data)
        {
            Type type = this.GetType();
            MethodInfo method = type.GetMethod(m_CommandName);
            object[] args = new object[] { data };
            try
            {
                return method.Invoke(this, args);
            }
            catch (Exception ex)
            {
                // TODO: Add logging functionality
                throw;
            }
        }

        #endregion

        #region Public execution commands

        public object GetTime(object data)
        {
            return DateTime.Now;
        }

        public object GetProductNameFromDatabase(object data)
        {
            // TODO: add real functionality later
            if ((int)data == 1)
            {
                return "Computer";
            }
            else
            {
                return "Unknown";
            }
        }

        #endregion
    }
}


In this class I have separated functionality to two regions.
Region "Command functionality" contains constructor, that initializes private variable "m_CommandName" with string - name of method to be called. There is also public static method "Create" that initializes and returns new instance of "Command" class. And there is a method named "Execute". This method does all the magic :). It uses reflection to retrieve type information from itself. This means it loads type information from "Command" class, creates "MethodInfo" object named "method", and tries to Invoke it with parameter "data" of type object. This parameter is passed to the "Execute" method. All the methods that should be called from this class, should take one parameter of type object, and return object. This gives us the ability to spread the usability of this architecture. In fact we can pass an arrays from javascript, and we can return arrays, or more complex types back. the ASP.NET framework itself will serialize result to JSON for use in client browser, and serialize parameters when they are send from the browser. So we just cast objects to expected types in methods to work with them.
Region "Public execution commands" contains commands to be executed. They have similar signature. method named "GetTime" just returns server time, and method "GetProductNameFromDatabase" imitates database call. It actually does nothing but returns some string if incoming object data equals to 1, and another otherwise. I am sure there are more useful scenarios in which the idea can be used. I use it for example to retrieve and save profile data for current user.

Now we have to add static method "ExecuteCommand" to our page (or it's parent class), there is an implementation of page's code behind class:

using System;
using Devarchive_net;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }

    [System.Web.Services.WebMethod]
    public static object[] ExecuteCommand(string commandName, string targetMethod, object data)
    {
        try
        {
            object[] result = new object[2];
            result[0] = Command.Create(commandName).Execute(data);
            result[1] = targetMethod;
            return result;
        }
        catch (Exception ex)
        {
            // TODO: add logging functionality 
            throw;
        }
    }
}


Now lets create javascript file(actually we will modify the file we created in this article), that will allow us to call the server method.
The script will look like this:

/// --------------------------------------------------
/// mainScreen object
/// --------------------------------------------------
var mainScreen =
{
    result : null
}

mainScreen.Init = function() {
    /// <summary>
    /// Initializes mainScreen variables
    /// </summary>
    
    // TODO: add initialization code here
};
mainScreen.ExecuteCommand = function (methodName, targetMethod, parameters) {
    /// <summary>
    /// Executes method on the server
    /// </summary>
    /// <param name="methodName">
    /// Page method name
    /// </param>
    /// <param name="targetMethod">
    /// Javascript method name that will be executed on 
    /// client browser, when server returns result
    /// </param>
    /// <param name="parameters">Data to pass to the page method</param>
    PageMethods.ExecuteCommand(methodName, targetMethod, parameters, mainScreen.ExecuteCommandCallback, mainScreen.ExecuteCommandFailed);
};
mainScreen.ExecuteCommandCallback = function (result) {
    /// <summary>
    /// Is called when server sent result back
    /// </summary>
    /// <param name="result">Result of calling server command</param>
    if(result) {
        try {
            mainScreen.result = result[0];
            eval(result[1]+"(mainScreen.result);");
        } catch(err) {
            ; // TODO: Add error handling
        }
    }
};
mainScreen.ExecuteCommandFailed = function (error, userContext, methodName) {
    /// <summary>
    /// Callback function invoked on failure of the page method 
    /// </summary>
    /// <param name="error">error object containing error</param>
    /// <param name="userContext">userContext object</param>
    /// <param name="methodName">methodName object</param>
    if(error) {
        ;// TODO: add error handling, and show it to the user
    }
};


/// --------------------------------------------------
/// Page events processing
/// --------------------------------------------------

Sys.Application.add_load(applicationLoadHandler);
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endRequestHandler);
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(beginRequestHandler);

function applicationLoadHandler() {
    /// <summary>
    /// Raised after all scripts have been loaded and 
    /// the objects in the application have been created 
    /// and initialized.
    /// </summary>
    mainScreen.Init()
}

function endRequestHandler() {
    /// <summary>
    /// Raised before processing of an asynchronous 
    /// postback starts and the postback request is 
    /// sent to the server.
    /// </summary>
    
    // TODO: Add your custom processing for event
}

function beginRequestHandler() {
    /// <summary>
    /// Raised after an asynchronous postback is 
    /// finished and control has been returned 
    /// to the browser.
    /// </summary>

    // TODO: Add your custom processing for event
}


As you see the most important method here is "mainScreen.ExecuteCommand", it does the job by calling server method. The second method "mainScreen.ExecuteCommandCallback" receives result and calls corresponding method with eval function.
Lets see how we actually call these methods, here is the markup of Default.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager 
        EnablePageMethods="true" 
        ID="MainSM" 
        runat="server" 
        ScriptMode="Release"
        LoadScriptsBeforeUI="true">
        <Scripts>
            <asp:ScriptReference Path="~/Scripts/Main.js" />
        </Scripts>
    </asp:ScriptManager>
    <script type="text/javascript">
    var methodHandlers = {};
        methodHandlers.ShowTime = function(obj) {
            /// <summary>
            /// method that shows result from 
            /// page method "GetTime"
            /// </summary>
            document.getElementById("timeDiv").innerHTML = obj;
        };
        methodHandlers.ShowProduct = function(obj) {
            /// <summary>
            /// method that shows result from 
            /// page method "GetProductNameFromDatabase"
            /// </summary>
            document.getElementById("productDiv").innerHTML = obj;
        };   
    </script>
    Click on the button to get time from the server.
    <br />
    <br />
    <input 
        value="Get Time" 
        type="button" 
        onclick="mainScreen.ExecuteCommand(
            'GetTime', 
            'methodHandlers.ShowTime', 
            null);" 
        />
    <br />
    <br />
    <div 
        style="border: dashed 1px black;" 
        id="timeDiv">
        &nbsp;
    </div>
    <br />
    <br />
    Click on the button to get product name 
    from the server for specified id.
    <br />
    <br />
    <input 
        type="text" 
        id="inpProductId" 
        value="1" />
    <input 
        value="Get Product" 
        type="button"
        onclick="mainScreen.ExecuteCommand(
            'GetProductNameFromDatabase', 
            'methodHandlers.ShowProduct', 
            parseInt($get('inpProductId').value));" 
        />
    <br />
    <br />
    <div 
        style="border: dashed 1px black;" 
        id="productDiv">
        &nbsp;
    </div>
    <br />
    </form>
</body>
</html>


It's simple :). just call the method ExecuteCommand, passing in server method name, js method name for processing and parameter object.

See it in action!

You can download source code here

3 comments:

  1. This is very clever - brought a smile to my face...

    ReplyDelete
  2. It looks like an extremely complicated way of doing this:

    http://www.geekzilla.co.uk/View7B75C93E-C8C9-4576-972B-2C3138DFC671.htm

    ReplyDelete
  3. Thank you so much. Very nicely explained :)

    ReplyDelete