Passing Complex Objects in Callbacks

The wwMethodCallback control supports passing complex object to and from server through method calls. For example you can return a complex object that contains a child object or an array/collection of children. You can also return DataRow, DataTable and DataSet objects.

Returning a DataTable and Binding it

Let's start with returning a DataTable since this is such a common scenario. You go to the server to return a list of data to bind to a drop down list for example. To do this create a server side method called GetCustomerList() that looks like this:

[CallbackMethod] protected DataTable GetCustomerList() { busCustomer Customer = new busCustomer(); if ( Customer.GetCustomerList() < 0) throw new ApplicationException(Customer.ErrorMessage); return Customer.DataSet.Tables["TCustomerList"]; }

The method simply uses a business object to create a datatable internally and then returns this value. The [CallbackMethod] attribute makes this method accessible to the client.

For the client to call this method and databind the data to you can use code like this (ToolTip optional):

<script> function GetCustomerList() { var ToolTip = new wwToolTip('lstCustomerList'); ToolTip.show("Updating...",0); // *** Call the Server GetCustomerList method Proxy.GetCustomerList(GetCustomerList_Callback,OnPageError); } function GetCustomerList_Callback(Result) { // *** Clear the tooltip set when the request started var ToolTip = new wwToolTip('lstCustomerList'); ToolTip.hide(); // *** Our Result here is a DataTable which is now // *** an object that has an array of rows and properties // *** for each of the column values. var DataTable = Result; // *** Use a list control to databind easily var List = new wwList("lstCustomerList"); List.dataValueField = "CustomerId"; List.dataTextField = "CompanyName"; List.setData(DataTable); } </script>

The callback is made and the DataTable returned as an object to the callback method. This code uses a couple of DOM object wrappers in the wwScriptLibrary, so the code to bind the DataTable to a list control consists just of a few lines of code. This code hides the structure of the DataTable object returned, so if you were to manually loop through the items and add them you could also use code like this:

List.clear(); for(x = 0; x < DataTable.Rows.length; x++) { var Row = DataTable.Rows[x]; List.addItem(Row.CompanyName,Row.CustomerId); // Display/Value }

Using Complex Objects

You can also pass back complex objects from the server. For example here's a complex object that contains a number of fields, a child object (PhoneNumbers) and an array of child objects (Orders):

public class CustomerEntity : customersRow { public CustomerEntity() : base() { } public string Company = ""; public string Name = ""; public string Address = ""; public DateTime Entered = DateTime.MinValue; ... public PhoneNumber PhoneNumbers = new PhoneNumber(); public ordersRow[] Orders = null; public void LoadOrders() { busInvoice Invoice = new busInvoice(); if (Invoice.GetInvoicesForCustomer(this.Customerid) < 1) { this.Orders = null; return; } List<ordersRow> OrderList = new List<ordersRow>(); foreach (DataRow Row in Invoice.DataSet.Tables["TCustomerInvoiceList"].Rows) { ordersRow Order = new ordersRow(); Westwind.Tools.wwDataUtils.CopyObjectFromDataRow(Row, Order); OrderList.Add(Order); } this.Orders = OrderList.ToArray(); } }

You can use either properties or fields. You can now publish this object from a Server method like this:

[CallbackMethod] protected CustomerEntity GetCustomerEntity(string CustomerId) { busCustomer Customer = new busCustomer(); if (CustomerId == null || CustomerId == "") CustomerId = "ALFKI"; if (!Customer.Load(CustomerId)) throw new ApplicationException("Customer doesn't exist"); if (this.chkReturnOrderEntities.Checked) { // *** Load array of orders into Orders property of entity Customer.Entity.LoadOrders(); } return Customer.Entity; }

On the client you can start the request like this:

<asp:TextBox ID="txtCustomerId" runat="server" Width="126px">ALFKI</asp:TextBox> <input id="btnReturnCustomer" type="button" value="Return Customer Object" onclick="GetCustomerEntity();" />

and then using this code:

<script> // *** Global Customer reference so we can update the customer // *** and pass it back to the server var Customer = null; function GetCustomerEntity() { Proxy.GetCustomerEntity($w('txtCustomerId').value,GetCustomerEntity_Callback,OnPageError); } function GetCustomerEntity_Callback(Result) { // *** Update global Customer reference Customer = Result; // *** Note the $w() function is equal to document.getElementById() $w('txtCompanyName').value = Customer.Companyname; $w('txtContactName').value = Customer.Contactname; $w('txtAddress').value = Customer.Address; // *** Child object value $w('txtPhone').value = Customer.PhoneNumbers.HomePhone; var Panel = $w('OrderDetailPane'); if ( !$w('chkReturnOrderEntities').checked ) { Panel.style.display='none'; return; } Panel.style.display=''; // *** Now bind the line items to a listbox var List = new wwList("lstOrders"); List.clear(); for (x=0; x < Customer.Orders.length; x++ ) { var Order = Customer.Orders[x]; List.addItem( Order.Orderid.toString() + ' - ' + Order.Orderdate.toLocaleDateString() ); } } </script>

The customer callback proxy is used to pass a parameter to the server. The Customer Id is passed which the server uses to process the request and return a complex entity object. The callback returns to the callback function with the object as the result value. The object has the same structure as the server object so there are properties for CompanyName, ContactName and there's a PhoneNumbers subobject with a HomePhone property and if requested an Orders collection which is represented as a a plain array on the client.

Note that the object returned is only a 'Message' object so none of the server methods are available - only the data structure. Think of the data in the same way you do of Web Service proxies which return only proxy objects with the data

The code above takes the object and parses the base values into the form values into the appropriate single field controls. For the Orders array, you can use a wwList object to bind the values, or manually loop through the list and assign each value. In this example, I have to use the latter, because the value displayed is not a single field value but rather composite of two fields. You can't databind so manual looping is required. To do so you can loop through each Order item and then assign it to the list individually.

I'm using the wwList control because it makes things much easier, but you can of course use raw JavaScript and raw HTML DOM access to do this as well, but it requires a little more code and some workarounds for DOM incompatibilities.

Returning Objects to the Server

You can also pass objects back to the server. Assume for a second that you loaded the Customer entity object with the code above onto a form and you're now making a few changes to the data. When done you can now return this object back to the server. Above the returned object is stored on a global Customer reference. That reference stays available until the page is reloaded.

Here's the code to send to the server. The ReturnObject() function can be called from an onclick of a button:

// Pass the Customer Object loaded on the page back to // the server. function ReturnObject() { // *** Take the object with it's updated data // *** and send it to the server. if (Customer) { Customer.Companyname = $w('txtCompanyName').value; Customer.Contactname = $w('txtContactName').value; Customer.Address = $w('txtAddress').value; Customer.PhoneNumbers.HomePhone = $w('txtPhone').value; // *** Note we're passing the Customer Object as a parameter here Proxy.ReturnCustomerEntity(Customer,ReturnObject_Callback,OnPageError); } else alert('Customer Object doesn\'t exist'); }

The call now goes to the server to hit the ReturnCustomerEntity method which simply echos the content of the object back to the client as a string:

[CallbackMethod] protected string ReturnCustomerEntity(CustomerEntity Customer) { StringBuilder sb = new StringBuilder("Echo from Server (" + DateTime.Now.ToString("T") + ")\r\n\r\n" + Customer.Companyname + "\r\n" + Customer.Contactname + "\r\n" + Customer.Address + "\r\n" + Customer.PhoneNumbers.HomePhone ); if (Customer.Orders != null) { sb.Append("\r\n\r\nOrders returned:\r\n"); foreach(ordersRow Order in Customer.Orders) { sb.Append(Order.Orderid.ToString() + " - " + Order.Orderdate.ToString("d") + "\r\n"); } } return sb.ToString(); }

The client then gets this string result returned in the specified Callback function in this case ReturnObject_Callback which is a one liner:

function ReturnObject_Callback(Result) { alert(Result); }

So as you can see it's pretty easy to pass data back and forth between the client and the server.

Object Considerations

Note that although the control does a good job with many objects it doesn't work with every kind of object. Specifically supported are arrays of single values or Plain old CLR Objects (POCO), IList based objects (as arrays) and nested POCO objects. Some complex collections however will not properly persist. Any object or embedded object that doesn't have a parameterless constructor cannot be serialized/deserialized.

The parser also won't deal with recursive object structures.

Passing Data with the POST Buffer

Passing parameters works great in many cases, but in some situations it's just plain easier to access the form's POST data directly on the server. This can be more efficient and save extra code in reading values back into objects and then reading them back out on the server.

So rather than passing parameters you can also choose to pass the form's POST data to the server by setting the wwCallbackMethod Control's PostBackMode=PostbBackModes.Post or PostBackModes.PostNoViewState. This will send the current page data back to the server in addition to the method parameters.

You can then access the POST data directly from the server with Request.Form. It's often just as easy or easier to read form variables on the server and perform conversions than to do it on the client side so it can be more efficient to bypass parameters and use the POST data directly on the server.

For example, if you look back at the ReturnObject() method you'll notice that I make manual assignments to the Customer object - this assignment can be done on the client or the server and in the example, you are really duplicating work as well as causing the client to encode to JSON and the server to decode from JSON, when we could just simply read the POST data directly on the server.

The wwCallbackMethod control allows returning the current POST buffer to the client with or without ViewState information via the PostBackMode property setting. If ViewState info is passed back, control data gets reassigned from the POST data with the latest values this means that you can reference this.txtCompany.Text in a callback and expect to see the current value. If you use POSTNoViewState all of the ASP.NET state data is not passed back, which can significantly reduce the size of the request buffer. In this case you can use Request.Form[] to retrieve form content.


  Last Updated: 12/29/2007 | © West Wind Technologies, 2008