How to create a complete AJAX Server Control. Part 4.

Posted on Updated on

In this post I’m going to add functionality in the client side that will be executed in the server side (for previous posts of the series please refer to part 1, part 2 and part 3).

To achieve this, there are 2 interfaces that have been defined:

  • System.Web.UI.IPostBackEventHandler: used to implement postbacks
  • System.Web.UI.ICallbackEventHandler: used to implement callbacks

In the sample I’ll be implementing both of them; however the one that we are going to find more useful will be the one used for the postbacks, as it involves a change of state in the server control. Callbacks can be useful if we want to run something in the server and get a value as response but doesn’t require changes in the server side of control.

Before implementing the interfaces we need to define the events that we want to implement in the server side. To do that I have defined the ServerEvent enum with the 2 events that I want to capture in the JS and run in the server, DoubleClick and ItemAdded (I have also created 2 delegates for the events to show how to fully customise everything; ClickEventHandler and ItemAddedEventHandler). And then 2 methods to raise the events, OnDoubleClick and OnItemAdded.

So far, so good, but the client side needs to know which events have been hooked in the server side so that we only call the server side if it’s required. RunAtServerEvents checks which events have to be raised and sends to the client the list of events (a string with events separated by “;” and implemented as a client side property). Then we have the property AutoPostBack so we can decide if we want to execute them as postbacks or callbacks (for simplicity I’ll be assuming all as postbacks or all as callbacks but we could customise for each of them in the JS code). And finally we need the property CallbackID, required to implement the callbacks and postbacks (initialised to UniqueID).

Something interesting to notice is that on OnPreRender there is a piece of code that I found somewhere in the code of one of the AjaxControlToolkit controls, it seems that we need to add a call to Page.ClientScript.GetCallbackEventReference(this, “”, “”, “”) to make the callbacks work (required code to register callbacks in the client).

ShoppingCart.cs

	/// 
	/// Click delegate definition
	/// 
	/// 
	public delegate void ClickEventHandler(object sender);

	/// 
	/// Item added delegate
	/// 
	/// 
	/// new item
	public delegate void ItemAddedEventHandler(object sender, ShoppingCartItem addedItem);

	public class ShoppingCart : ScriptControlBase
	{

		/// 
		/// Server events raised from the client
		/// 
		enum ServerEvent
		{
			DoubleClick,
			ItemAdded
		}

		//...

		protected override void OnPreRender(EventArgs e)
		{
			if (!this.DesignMode)
			{
				if (ScriptManager.GetCurrent(Page) == null)
				{
					throw new HttpException("A ScriptManager control must exist on the current page.");
				}
				InitialiseControls();

				CreateHTMLItems();

				CallbackID = UniqueID; // needed for the callbacks and postbacks

				// Create JavaScript function for ClientCallBack WebForm_DoCallBack
				// Not sure why we need it, but the callback doesn't get registered on the client side properly without it.
				Page.ClientScript.GetCallbackEventReference(this, "", "", "");

			}

			base.OnPreRender(e);
		}

		#region Server Events
		/// 
		/// If we want to execute server events with a postback.
		/// True: A full postback happens and the page will be refreshed.
		/// Use UpdatePanel control to refresh certain areas.
		/// False: The events will run on the server with no postback (it'll use a Callback).
		/// If any data is updated a manual UI refresh needs to be forced (full page or UpdatePanel).
		/// 
		[Description("If we want to execute server events with a postback.")]
		[ExtenderControlProperty]
		public bool AutoPostBack
		{
			[DebuggerStepThrough]
			get { return GetPropertyValue("AutoPostback", true); }
			[DebuggerStepThrough]
			set { SetPropertyValue("AutoPostback", value); }
		}

		/// 
		/// Control ID to be used when doing callbacks from the client
		/// 
		[ExtenderControlProperty]
		//[ClientPropertyName("CallbackID")]
		[Browsable(false)]
		public string CallbackID
		{
			[DebuggerStepThrough]
			get { return GetPropertyValue("CallbackID", UniqueID); }
			[DebuggerStepThrough]
			set { SetPropertyValue("CallbackID", value); }
		}

		/// 
		/// Events that will be run at server
		/// 
		[Browsable(false)]
		[ExtenderControlProperty]
		[ClientPropertyName("runAtServerEvents")]
		public string RunAtServerEvents
		{
			get
			{
				string runAtServerEvents = "";

				if (DoubleClick != null) runAtServerEvents += "DoubleClick;";
				if (ItemAdded != null) runAtServerEvents += "ItemAdded;";

				if (runAtServerEvents.Length > 0)
					return runAtServerEvents.Substring(0, runAtServerEvents.Length - 1);
				else
					return "";
			}
			set { }
		}

		/// 
		/// Event raised when the a cell is double clicked
		/// 
		[Description("Event raised when the a cell is double clicked.")]
		public event ClickEventHandler DoubleClick;

		/// 
		/// Raise DoubleClick event
		/// 
		/// 
		protected virtual void OnDoubleClick()
		{
			if (DoubleClick != null)
			{
				DoubleClick(this);
			}
		}

		/// 
		/// Event raised when the a cell is double clicked
		/// 
		[Description("Event raised when the a cell is double clicked.")]
		public event ItemAddedEventHandler ItemAdded;

		/// 
		/// Raise DoubleClick event
		/// 
		/// 
		protected virtual void OnItemAdded(ShoppingCartItem item)
		{
			if (ItemAdded != null)
			{
				ItemAdded(this, item);
			}
		}
		#endregion
	}

The next step is the implementation of the interfaces code that will be called by the AJAX framework, IPostBackEventHandler and ICallbackEventHandler. To keep it simple I’ll implement the same code in both.

The method that does the work is ExecuteServerEvent, in where the parameter coming from the client will define the event name and the parameters (I’ve kept it simple and I’ve used “eventname;jsonParameters”). Double click doesn’t receive any parameters, but ItemAdded receives as a parameter the new item that has been added to the shopping cart. From there, all is standard C#.

ShoppingCart.cs

	public class ShoppingCart : ScriptControlBase, IPostBackEventHandler, ICallbackEventHandler
	{

		//...
		#region Server Events

		//...

		/// 
		/// Executes the server event
		/// 
		/// 
		/// Event Parameters created in the client that follows this structure.
		/// EventName;JSON object serialised
		/// i.e:
		///		ItemAdded;JSON serialised
		/// 
		/// 
		private string ExecuteServerEvent(string eventFromClient)
		{
			// Get the event name
			int separator = eventFromClient.IndexOf(";");
			string serverEvent = eventFromClient.Substring(0, separator);
			ServerEvent e = (ServerEvent)Enum.Parse(typeof(ServerEvent), serverEvent);

			// Get the event args (JSON serialised string)
			string eventArgs = eventFromClient.Substring(separator + 1);

			switch (e)
			{
				case ServerEvent.DoubleClick:
					OnDoubleClick();
					break;
				case ServerEvent.ItemAdded:
					ShoppingCartItem addedItem = new JavaScriptSerializer().Deserialize(eventArgs);
					AddItem(addedItem);
					break;
				default:
					throw new Exception("Wrong event!");
					break;
			}
			return "";
		}

		/// 
		/// Adds an item
		/// 
		/// 
		private void AddItem(ShoppingCartItem addedItem)
		{
			Items.Add(addedItem);

			OnItemAdded(addedItem);
		}
		#endregion 

		#region IPostBackEventHandler Members

		/// 
		/// Raise postback event in the server.
		/// 
		/// argument from JScript
		public void RaisePostBackEvent(string eventArgument)
		{
			ExecuteServerEvent(eventArgument);
		}
		#endregion

		#region ICallbackEventHandler Members
		/// 
		/// Return value for the callback call
		/// 
		string callbackResult = "";

		/// 
		/// Returns the value generated by the event (if any).
		/// 
		/// 
		string ICallbackEventHandler.GetCallbackResult()
		{
			return callbackResult;
		}

		/// 
		/// Raises the server event in the server.
		/// 
		/// argument from JScript
		void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)
		{
			callbackResult = ExecuteServerEvent(eventArgument);
		}
		#endregion
	}

In the client side we need to add some code to raise the events (and implement JS properties). The method that will call the server is _raiseServerEvent.

  1. It checks the the event that we want to fire has been hooked up in the server side
  2. It creates the parameters with the event name to call and the parameters required for that event
  3. Depending if we want to execute a postback or a callback it calls the server in a different way.
  4. As Callbacks run asynchronously, we need to provide the methods to execute in case it was successful and if there was an error.

ShoppingCart.js

// -------------------------------------- Client side Properties --------------------------------------
$ShoppingCart.prototype.get_items = function() { return this._items; }
$ShoppingCart.prototype.set_items = function(value)
{
	this._items = Sys.Serialization.JavaScriptSerializer.deserialize(value);
}

$ShoppingCart.prototype.get_CallbackID = function() { return this._CallbackID; }
$ShoppingCart.prototype.set_CallbackID = function(value) { this._CallbackID = value; }

$ShoppingCart.prototype.get_AutoPostBack = function() { return this._AutoPostBack; }
$ShoppingCart.prototype.set_AutoPostBack = function(value) { this._AutoPostBack = value; }

// -------------------------------------- Server Side Events --------------------------------------
$ShoppingCart.prototype.get_runAtServerEvents = function()
{
	return this._runAtServerEvents;
}

$ShoppingCart.prototype.set_runAtServerEvents = function(value)
{
	this._runAtServerEvents = value.split(';');
}

$ShoppingCart.prototype._raiseServerEvent = function(eventName, eventArgs)
{
	if (!Array.contains(this._runAtServerEvents, eventName))	return;

	var eventArgsSerialised = Sys.Serialization.JavaScriptSerializer.serialize(eventArgs);
	var args = String.format("{0};{1}", eventName, eventArgsSerialised);
	var id = this._CallbackID;

	if (this._AutoPostBack)
	{// calls IPostBackEventHandler.RaisePostBackEvent in the server control
		__doPostBack(id, args);
	}
	else
	{
		// calls ICallbackEventHandler.RaiseCallbackEvent
		// the return value from ICallbackEventHandler.GetCallbackResult() is sent
		// to the callback handler this._receiveServerData
		WebForm_DoCallback(id, args, this._receiveServerData, this, this._onCallbackError, true)
	}
}

// Handler for successful return from callback
$ShoppingCart.prototype._receiveServerData = function(arg, context)
{
	// context is the instance of the class (this)

	// update the UI or Raise an event to inform that we finished the callback
	alert("Back from the callback!");
	//context.updateUI();
	//context.raiseEndClientCallback(arg);
}

// Error handler for the callback
$ShoppingCart.prototype._onCallbackError = function(message, context)
{
	alert(String.format(AjaxControlToolkit.Resources.Rating_CallbackError, message));
}

// -------------------------------------- Control methods --------------------------------------
$ShoppingCart.prototype.addItem = function(itemText, itemDescription, itemQuantity, itemTotalPrice)
{
	var newItem = new $ShoppingCartItem(itemText, itemDescription, itemQuantity, itemTotalPrice);

	this._raiseServerEvent("ItemAdded", newItem);
}

// Classes to handle event arguments and data transfer to the server ---------------------------------
var $ShoppingCartItem = AjaxControl.ShoppingCartItem = function(itemText, itemDescription, itemQuantity, itemTotalPrice)
{
	this.Text = itemText;
	this.Description = itemDescription;
	this.Quantity = itemQuantity;
	this.TotalPrice = itemTotalPrice;
}

To test it, let’s add some code to our web page. I have added 4 text boxes to introduce the values and and a button that will add the new item. Additionally I added a GridView control that is Databinded with the items in the shopping cart (server event of ItemAdded).

ShoppingCart.aspx


    

        function addNewItem() 
        {
            $find("Cart1").addItem(
                    $get("newItemText").value,
                    $get("newItemDescription").value,
                    $get("newItemQuantity").value,
                    $get("newItemTotalPrice").value
                    );
        
        }
    



    
    
    
    <div>
        <table>
            <tr>
                <td>Text</td><td></td>
            </tr>
            <tr>
                <td>Description</td><td></td>
            </tr>
            <tr>
                <td>Quantity</td><td></td>
            </tr>
            <tr>
                <td>TotalPrice</td><td></td>
            </tr>
            <tr>
                <td colspan="2" align="right"></td>
            </tr>
         </table>
    </div>
    <div>
        

        
    </div>
    

ShoppingCart.aspx.cs

	public partial class ShoppingCart : System.Web.UI.Page
	{
		protected void Page_Load(object sender, EventArgs e)
		{
			if(! IsPostBack)
			{
				var items = new List
				{
					//...
				};

				Cart1.Items = items;

				GridView1.DataSource = Cart1.Items;
				GridView1.DataBind();
			}
		}

		protected void Cart1_ItemAdded(object sender, ShoppingCartItem addedItem)
		{
			GridView1.DataSource = Cart1.Items;
			GridView1.DataBind();
		}
	}

As you will notice the old items are lost after the postback and only the latest item is rendered. This is the expected behaviour (Gridview implementation behaves the same). Next post I’ll show how to make the control remember the previous items.

Well, that’s all. I know that there’s a lot to digest here but believe me, if you had to figure it out by yourself it would take a lot longer.

You can download the code here.
Important: I couldn’t upload the file as it was a zip file, so I added the extension jpg. After saving the file, remove the extension and unzip it normally.

One thought on “How to create a complete AJAX Server Control. Part 4.

    yodantt said:
    December 25, 2010 at 13:18

    Great thing🙂 thx a lot!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s