Introduction
So you start playing around with jQuery and Ajax and you discover that it is pretty cool stuff. Soon you discover that by using AJAX and web methods, you can send a .NET object to the client and it will automatically decode into a nice JavaScript object for you. This is all fine and good, but when you get right down to it, most of your objects actually begin their life on the client. Our applications aren't just about displaying data that already exists. The real power in the application is the ability to get the data into the application. What would be nice is to have our client objects and the server objects be one and the same, or at least as close as we can get to that using two different languages.
Background
In the past, my strategy to achieve this was to, simply by brute force, recreate my .NET objects in JavaScript on the client-side. This works, but it is a lot of work! And if you misspell a variable name or accidentally delete a required variable, the server will not be able to decode the object back to its native .NET type. I once spent more time than I care to admit to debugging an issue where I typed a zero instead of an O
. Also, if you extend the object on the server, you then have to go to the client and manually keep everything in synch. Even on a small project, you end up with a code management nightmare.
Then it hit me! What if I were able to "construct" the object on the server? I should be able to take advantage of the servers ability to pass me back an object. The only issue I would have is only the data members of the .NET class come across. Which is fine because the implementation of the objects functions would have to change anyway. If I could just code the functions on the client and have the Data Members populated from the server, that would be better.
Then I read an article about how to use jquery.extend()
. (Unfortunately, I have not been able to locate the article, to give credit where it is due.) I knew that jquery.extend()
was used to create plugins for jQuery, where you basically build your plugin and use extend to add it to the jQuery
API. What I didn't know was that extend
can do the same thing for your objects. Finally, I have all the pieces I need to "construct" my client objects on the server.
Using the Code
The sample project is a simple interface that will allow you to add Person
objects to a table. All this is done using Web Methods on the server side and JQuery
on the client-side!
Business Layer Objects
I derive all my business layer objects off of PersistableObject
. At least all the objects in the Business Layer that I will store to the database. This PersistableObject
class gives me two Data Members Id
and InSync
. Id
is used to store an Id
for the object. Most of my database tables are built to include an auto incremented id
for each row. This auto incremented id
is what will be stored here. The InSync
flag is not an homage to 80’s boy bands, it simply allows me to keep track of whether or not I'm synchronized with the database. If I modify a Data Member on the derived object, I will set this flag to false
.
I also have two virtual functions that will allow me to either Persist
the object or Drop
the object from the database. Notice that there are no separate Insert
and Update
commands. I plan on the data layer being smart enough to figure that out for me. Which is very easy since if the data layer sees an Id
it needs to do an update. No Id
means this is a new object and needs to be inserted.
namespace BLL
{
[DataContract]
public class PersistableObject
{
protected int? _id = null;
[DataMember]
public int? Id
{
get { return _id; }
set { _id = value; }
}
protected bool _inSync = false;
[DataMember]
public bool InSync
{
get { return _inSync; }
set { _inSync = value; }
}
public virtual int Persist()
{
throw new Exception
("PersistableObject derived class has not implemented the Persist function!");
}
public virtual int Drop()
{
throw new Exception
("PersistableObject derived class has not implemented the Delete function!");
}
}
}
So for the sample project, we will need a Person
object. So I derived that object from my PersistableObject
class. I have overridden the base class’s Persist
and Drop
functions. For this demonstration, I simulate storing the object into a database. I didn't want the code to get lost in a sea of database code. This Person
object simply gives me a few Data Members that will allow me to store the First name, Last name, and birthdate of the person entered from the client.
namespace BLL
{
[DataContract]
public class Person : PersistableObject
{
#region Accessors
private string _firstName = null;
[DataMember]
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
InSync = false;
_firstName = value;
}
}
}
private string _lastName = null;
[DataMember]
public string LastName
{
get { return _lastName; }
set
{
if (_lastName != value)
{
InSync = false;
_lastName = value;
}
}
}
private DateTime? _birthdate = null;
[DataMember]
public DateTime? Birthdate
{
get { return _birthdate; }
set
{
if (_birthdate != value)
{
InSync = false;
_birthdate = value;
}
}
}
#endregion
public override int Persist()
{
if (!InSync)
{
var random = new Random();
this.Id = random.Next(100, 10000);
this.InSync = true;
}
return Convert.ToInt32(this.Id);
}
public override int Drop()
{
this.Id = null;
this.InSync = false;
return 0;
}
}
}
jQuery / JavaScript Helper Functions
Before continuing on to the actual JavaScript object, I need to mention a couple of helper functions that I use for my Ajax calls. The first thing to point out is I put all my Web Methods in one file instead of peppering them all over my project. This allows me to create a single function for my Ajax calls. In this function, I send in the Method Name(m
), the Data(d
), and the Success Callback(c
). I do not pass in a Failure callback because I handle all my Ajax failures using the same function. This error handler will pop up a dialog to tell me there is an error. Not the best way to handle an error, but it works for this demonstration.
One thing to point out is I have two variations of the Method
helper. I have the plain Method
helper, which does a "true" Ajax call and keeps everything asynchronous. But, I also have a SerialMethod
helper that allows me to wait for the return. This will be very important when I create my JavaScript object and the constructor will need to call a Server side Web Method to build the object.
cd.Ajax.Method = function (m, d, c) {
$.ajax({
type: "POST"
, url: "/WebServices/WebMethods.aspx/" + m
, contentType: "application/json; charset=utf-8"
, data: d
, dataType: "json"
, success: function (msg) { c(msg.d); }
, error: function (msg) { cd.Ajax.AjaxFailed(msg); }
});
};
cd.Ajax.SerialMethod = function (m, d, c) {
$.ajax({
type: "POST"
, url: "/WebServices/WebMethods.aspx/" + m
, contentType: "application/json; charset=utf-8"
, data: d
, async: false
, dataType: "json"
, success: function (msg) { c(msg.d); }
, error: function (msg) { cd.Ajax.AjaxFailed(msg); }
});
};
Client Side Objects
So here is my JavaScript variation of the Person
object. Notice that I again have the Persist
and Drop
function. But on the client side, these functions call Web Methods on the server side using the helper functions and Ajax. Both Persist
and Drop
require callback functions which get called when the Web Methods have completed.
Next you will notice that there is a Create
function. This will also call a Web Method on the server; but this time we will use the SerialMethod
to wait for a return. Once we get the object returned from the server, we then call our Clone
function. Clone
's main purpose is to call jQuery's extend method to copy all the data members of the server object into our new object. Another benefit of the Clone
function is that we can also clean up some of our variables that come back. For instance, we know dates will come back in a weird notation which will need to be converted into a JavaScript date. We take care of that conversion here in the Clone
function.
One last thing to note here is that we can create this object from an already existing object on the client. If we already have a person
object and we want to copy that object we don't really have to go back to the server for the constructor.
Creating a new object from the Server
:
var p = new cd.Objects.Person();
Creating a new object from an existing Person
:
var p = new cd.Objects.Person(aPersonObject);
cd.Objects.Person = function (info) {
this.Persist = function (callback) {
cd.Ajax.Method("UpdatePerson"
, "{'person': " + JSON.stringify(this) + "}"
, function (person) {
var p = new cd.Objects.Person(person);
callback(p);
});
};
this.Drop = function (callback) {
cd.Ajax.Method("DeletePerson"
, "{'person': " + JSON.stringify(this) + "}"
, callback);
};
this.Create = function () {
var p;
cd.Ajax.SerialMethod("CreatePerson", undefined, function (person) { p = person });
this.Clone(p);
}
this.Clone = function (info) {
if (typeof (info.Birthdate) !== "undefined") {
info.Birthdate = cd.Ajax.convertJSONDate(info.Birthdate);
}
jQuery.extend(this, info);
}
if (info !== undefined) {
this.Clone(info);
}
else {
this.Create();
}
};
The WebMethods
One of the benefits of putting together the Business Layer and a Client Layer to handle these objects is that your Web Methods turn out to be very simple and easy to read. One thing to note here is that the UpdatePerson
Web Method returns the person
object that was just persisted. This will return us the Id
that was created for this object. It will also return any calculated fields that we may have added to our object.
[WebMethod]
public static Person CreatePerson()
{
var p = new Person();
return p;
}
[WebMethod]
public static Person UpdatePerson(Person person)
{
person.Persist();
return person;
}
[WebMethod]
public static bool DeletePerson(Person person)
{
person.Drop();
return true;
}
Extending the Person Object
Now that we have everything hooked up, we can more easily extend the Person
Object. For example, if we want to add a Read-Only accessor for age, we only have to add the accessor to the server and it will be available for us to use on the client the next time we create a new object.
[DataMember]
public TimeSpan Age
{
get
{
var t = new TimeSpan();
if (_birthdate != null)
{
t = System.DateTime.Now - (DateTime) _birthdate;
}
return t;
}
}
}
Points of Interest
I have a tendency to reinvent the wheel and to do things the hard way. So I have no doubt that I may get suggestions on how I could have done this easier or that there is already an established way of handling this issue. I welcome creative criticism. If you are interested in looking through the code, I would recommend that you use Firefox with the Firebug add-on installed. That is by far the easiest way to view what is actually going on with this code. Examine the code using firebug, then go into the Person
class on the server side, and check out the results. Then add some new accessors to the Person
objects, and check out the result again.
History
- 14th August, 2011: Initial version