I spent a bit of time playing around with Jason Diamonds My AJAX.NET library in light of my Portland .NET User group session tonight. Although the talk is on Script Callbacks in ASP.NET 2.0 I wanted to also show a number of ways to do the same thing in ASP.NET 1.1.
I had looked previously at Michael Schwarz’s Ajax.NET library which is similar. What’s nice about Jason’s library is that it’s much more compact, comes with source code, and is beautiful in its simplicity.
Some of the features:
- Attribute Based
Add an attribute to a method and it becomes client callable.
- Mapped Client Object
The client side implements a class that matches methods for all of your attributed server methods. Methods have the same signature as the server equivalent (plus an extra parameter to specify the callback function to receive the result)
- Result Marshalling
The result from a server method is marshaled to a real type. Most simple types are supported as are DataSet/DataTable/DataRow, IEnumable (arrays of simple types). Currently there’s no support for complex types.
- Exception Marshalling
Exceptions can be marshaled to the client as well. (I had to change the behavior to use innerException data to get meaningful messages)
- Tied into the Page framework
The library hooks into the current page framework and participates in regular POSTBACK behavior. This means when you all the Ajax method on the server, you get full page state including control state, so you can read form controls (this.txtName.Text). This is great because you don’t have to pass parameters in many cases as the page is in context. On the downside, every request is a POST which increases the size of data going to the server and there’s no way to disable that.
- Fully self contained
The entire library is contained in a small source file. Sweet and simple – you can add a single source file to your project and off you go. There are no external dependencies on HTTP Handlers, and no script resources that load, everything is self contained in a single page.
I whipped up a quick sample using the Northwind database in a couple of hours that does display and updates to the NW Customer table. You can take a look and check out the code that goes with the sample, which is surprisingly little. In fact, I think there’s actually less code in this AJAX implementation than it would have taken using a plain ASP.NET implementation.
I ended up modifying a few things in the library to add support for DBNull and passing full exception messages back to the client, but overall the base code works great.
What I really like is the fact that you can pass back common .NET types like DataSet/DataTable/DataRow as objects, that are picked up on the client using the same object hierarchy. So you can actually talk to an object on the client like this:
<script type="text/javascript">
var CustRow = null;
function GetCustomerListCallback(Result)
{
var Table = Result.value; // DataTable
var List = document.getElementById('lstCustomers');
if (List == null)
return;
for (x=List.options.length-1; x > -1; x--)
{
List.remove(0);
}
for (x=0; x < Table.Rows.length; x++ )
{
var Row = Table.Rows[x]; // Mozilla needs to assign
var option = document.createElement("option");
option.text = Row.CompanyName;
option.value = Row.CustomerId;
if ( window.navigator.appName.toLowerCase().indexOf("microsoft") > -1)
List.add(option);
else
List.add(option, null);
}
// *** Show First Item
if (List.length > 0)
{
List.selectedIndex = 0;
List.onchange();
}
}
function GetCustomerRowCallback(Result)
{
CustRow = Result.value; // DataRpw
AssignValue( "txtContactName",CustRow.ContactName);
AssignValue( "txtCompany",CustRow.CompanyName);
AssignValue( "txtAddress",CustRow.Address);
AssignValue( "txtCity",CustRow.City);
AssignValue( "txtCountry",CustRow.Country);
AssignValue( "txtRegion",CustRow.Region);
AssignValue( "txtPostalCode",CustRow.PostalCode);
var TitleSpan = document.getElementById("DialogTitle");
if (TitleSpan)
TitleSpan.innerHTML = CustRow.CompanyName;
}
function AssignValue(ControlName,Value)
{
if (Value == null)
Value = "";
var Ctl = document.getElementById(ControlName);
if (Ctl)
Ctl.value = Value;
}
function SaveCustomerCallback(Result)
{
// if (Result.error)
// {
// alert(Result.error);
// return;
// }
if (Result.value == null)
return;
if (Result.value)
alert("Customer Saved!");
else
alert("Result.value.error");
}
</script>
To trigger a server requests and get these callbacks called you can use simple functions like this:
<script type="text/javascript">
Callback.GetCustomerList(GetCustomerListCallback);
</script>
which is the same method signature as on the server, plus the name of the callback function on the client to call.
The DataTable and DataRow ‘serializations’ are not full object copies of the server side counterparts, they are meant to only transfer the data to the client, so you only get Rows and Values, not the whole rest of the object hierarchy.
Current objects results are not supported but I’m sure either Jason or somebody in the community will provide an implementation in short order.
The server code for the class is minimal too:
public partial class CustomerList : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Ajax.Manager.Register(this, "Callback", Ajax.Debug.Errors);
}
[Ajax.Method]
public DataTable GetCustomerList()
{
busCustomer Customer = new busCustomer();
if (Customer.GetCustomerList() < 0)
return null;
return Customer.DataSet.Tables["TCustomerList"];
}
[Ajax.Method]
public DataRow GetCustomerRow(string CustomerId)
{
busCustomer Customer = new busCustomer();
if (!Customer.Load(CustomerId))
return null;
return Customer.DataRow;
}
[Ajax.Method]
public bool SaveCustomer()
{
string CustomerId = Request.Form["lstCustomers"];
busCustomer Customer = new busCustomer();
if (CustomerId == null)
throw new ApplicationException("Invalid CustomerId");
if (CustomerId == "")
{
if (!Customer.New())
throw new ApplicationException("Couldn't create new customer.");
;
}
else
{
if (!Customer.Load(CustomerId))
throw new ApplicationException("Couldn't load customer");
}
Customer.Entity.Contactname = this.txtContactName.Text;
Customer.Entity.Companyname = this.txtCompany.Text;
Customer.Entity.Address = this.txtAddress.Text;
Customer.Entity.City = this.txtCity.Text;
if (!Customer.Validate())
throw new ApplicationException(Customer.ValidationErrors.ToString());
if (!Customer.Save())
throw new ApplicationException(Customer.ErrorMessage);
return true;
}
}
This sample is ASP.NET 2.0 (because the session I’m doing is on script callbacks in ASP.NET 2.0 mainly), but the above code works the same in 1.1. I think this sample is taking no more code than the equivalent ASP.NET server code would have taken. In fact, if I used my custom data controls on the form I could even take advantage of custom databinding and reduced the code even further. Slick.
I actually ran into a post from Peter Bromberg over at Egghead Café, which turned me on to Jason’s library. Peter has an interesting take on the whole AJAX terminology which I tend to agree with, but then again, let’s not get wrapped up in terminology. Remote Scripting, Callbacks, AJAX – it’s all good. With tools like Ajax.NET and the Jasons My Ajax.NET library
Jason put the thing out as a sample, it’s not meant as a ‘product’. There’s no license, no copyright – it’s free, use it as you see fit. It’s as is although Jason has been responding to queries and provided a number of fixes.