다음을 통해 공유


AJAX Script Patterns: Service Agent

I’ve been thinking for a while about how people tend to build AJAX applications, as there seems to be something a lot of people have forgotten. This post examines how applying a pattern you probably know very well to AJAX could help – and leaves it to you to decide whether this is useful. Let me know what you think.

Setting the Scene

In your average application architecture, you’ll generally find a “Resource Access” layer that contains Service Agents (also known as Service Gateways) for accessing remote web services. What are these for? Well, I think the MSDN documentation on the pattern sums it up nicely in the Problem statement;

“How do you decouple the details of fulfilling the contract responsibilities defined by the [remote web] service from the rest of your application?”

The answer is of course that you create a class to manage the interaction with your service and hide the inner workings and contract implementations from the rest of your code. This class and any supporting entities make up your Service Agent.

AJAX: Why so Different?

With the growing popularity of AJAX, and the great features available in Microsoft AJAX, more and more web sites use script to call services hosted back on the ASP.NET server to load data, and dynamically update the page with DOM operations.

So how come so many sites have forgotten what we learnt years ago? This script calls a web service – and this web service forms a part of a larger system. It may change – the contracts, the implementation, the name, the location... Consider that these web services may also be shared between AJAX, Silverlight, ASP.NET, WPF, and/or other technologies as clients. So each of these could require tweaks to your shared web service (even if they’re using a different endpoint that uses a different binding, chances are the contracts and implementation are shared).

Microsoft AJAX helps us by auto-generating the web service proxy and related entities for calling a script-enabled web service. But if you change the web service, this changes the generated proxy, and you could find many pages in your web site suddenly need JavaScript changes to work correctly again. We don’t get the rich compile-time support in JavaScript that we get with .NET languages, so you might not find these problems until testing, or worse, live.

Therefore I see two options for handling this;

1. You could create a reusable ASP.NET AJAX server control that wraps up the web service calls and UI manipulation. This means you’ve just got one place to make changes, so is a great solution.

2. If the service is used differently in each place, perhaps it just doesn’t suit being a control. In which case, I propose creating a script-based Service Agent.

A Simple Example

To demonstrate this, I’ve created the bare minimum of a simple WCF service to be called from AJAX;

[ServiceContract(

Namespace =

"https://samples.microsoft.com/jslayering")]

[AspNetCompatibilityRequirements(

RequirementsMode =

AspNetCompatibilityRequirementsMode.Allowed)]

public class PersonService

{

    [OperationContract]

    public GetPersonResponse GetPerson(

GetPersonRequest request)

    {

        Implementation

    }

}

[MessageContract]

public class GetPersonRequest

{

    [MessageBodyMember]

    public int Identifier { get; set; }

}

[MessageContract]

public class GetPersonResponse

{

    [MessageBodyMember]

    public bool Found { get; set; }

    [MessageBodyMember]

    public Person Person { get; set; }

}

[DataContract]

public class Person

{

    [DataMember]

    public int Identifier { get; set; }

    [DataMember]

    public string Name { get; set; }

    [DataMember]

    public int Age { get; set; }

}

You can see I’ve also chosen to use Message Contracts... we’ll see why in a moment J

All that happens in this service is the client requests a Person’s details, and they either get Found=true and a Person entity back, or Found=false. In my implementation this is hard-coded and randomised, so excuse the grim code.

Creating a Service Agent

The first thing we need is a JavaScript entity to hold person details in. This is because the Person class above is part of the contract of the web service – so if we change it, the JavaScript entity generated by Microsoft AJAX will change too... so we need to protect the rest of our JavaScript code from this. Let’s see my simple entity;

/// <reference name="MicrosoftAjax.js"/>

/// <reference path="~/PersonService.svc" />

Type.registerNamespace("ServiceAgents");

ServiceAgents.Person = function(id, name, age) {

    this._id = id;

    this._name = name;

    this._age = age;

}

ServiceAgents.Person.prototype = {

    get_id: function() {

        return this._id;

    },

    get_name: function() {

        return this._name;

    },

    get_age: function() {

        return this._age;

    }

}

ServiceAgents.Person.registerClass(

'ServiceAgents.Person');

if (typeof (Sys) !== 'undefined')

Sys.Application.notifyScriptLoaded();

* Note the use of the <reference> tag so I get intellisense in Visual Studio 2008!

This is the simplest of objects, and just stored the three fields found on a Person entity. Next, we need a Service Agent. I’ve chosen to create this as a component (inherited from Sys.Component) mainly because I might want to add event handlers and more to it another time, so having it as a managed disposable object is potentially useful.

/// <reference name="MicrosoftAjax.js"/>

/// <reference path="~/PersonService.svc" />

/// <reference path="Person.js" />

Type.registerNamespace("ServiceAgents");

ServiceAgents.PersonServiceAgent = function() {

    ServiceAgents.PersonServiceAgent.

initializeBase(this);

}

ServiceAgents.PersonServiceAgent.prototype = {

    initialize: function() {

        ServiceAgents.PersonServiceAgent.

callBaseMethod(this, 'initialize');

    },

    dispose: function() {

        ServiceAgents.PersonServiceAgent.

callBaseMethod(this, 'dispose');

    },

    GetPerson: function(identifier, callback) {

        var success = Function.createDelegate(

this, this._onSuccess);

        var failure = Function.createDelegate(

this, this._onFailure);

      // call the web service

        samples.microsoft.com.jslayering.PersonService.

GetPerson(identifier, success, failure, callback);

    },

    _onSuccess: function(result, context) {

      // check we got a result back

        if (!result.Found)

            this._onFailure({ get_message: function()

{ return 'Person not found'; } },

context);

        else {

            // translate to our Person entity

            var returnentity = new ServiceAgents.Person(

result.Person.Identifier,

result.Person.Name,

result.Person.Age);

            // call the callback that was passed in

            context(returnentity);

        }

    },

    _onFailure: function(result, context) {

        alert('A call to the Person Service failed: '

+ result.get_message());

    }

}

ServiceAgents.PersonServiceAgent.registerClass(

'ServiceAgents.PersonServiceAgent', Sys.Component);

// ensure an instance of this component is created

// when the page is initialising

if (typeof (Sys) !== 'undefined') {

    Sys.Application.add_init(

function() {

$create(

ServiceAgents.PersonServiceAgent,

{ id: 'PersonSA' },

null, null, null);

    });

    Sys.Application.notifyScriptLoaded();

}

There are four things to note in this component;

1. The only public method we’ve added is GetPerson. This literally sets up some call-backs and passes the call on to the auto-generated script proxy.

2. When the call succeeds, we check a result was found (using the Found property on the Message Contract) and if it was, we create a new instance of our own Person entity to pass back, by calling the supplied call-back method.

3. If there’s a failure we handle it; I could have let the caller do this, but this component is for illustrative purposes only... it is by no means perfect and I wanted to keep it quite brief!

4. We ensure an instance of our component is created when the page initialises, so that page script can easily consume it.

Using the Service Agent

Making use of this component is easy – we need to import the relevant services and scripts, and then get a reference to it using the $find method, and call it! First I register the scripts with a ScriptManager. We still need to point out on every page that we want to import the service, and now have to specify our Service Agent scripts too;

<asp:ScriptManager

ID="PageScriptManager"

runat="server">

<Services>

    <asp:ServiceReference

Path="~/PersonService.svc" />

</Services>

<Scripts>

    <asp:ScriptReference

Path="~/Scripts/Person.js" />

    <asp:ScriptReference

Path="~/Scripts/PersonServiceAgent.js" />

</Scripts>

</asp:ScriptManager>

Then I’ve got the following script on my page;

function getPerson() {

    var id = $get('RequestIdentifierTextBox').value;

    var serviceAgent = $find('PersonSA');

    serviceAgent.GetPerson(id, displayPerson);

    // prevent post-back as we've handled it client side

    return false;

}

function displayPerson(result) {

    $get('IdentifierLabel').innerHTML = result.get_id();

    $get('NameTextBox').value = result.get_name();

    $get('AgeTextBox').value = result.get_age();

}

... which drives a page that looks like this;

Screen Shot 

Breaking the Application

Well this is not worthwhile unless we can demonstrate a tangible benefit. Imagine that code similar to that above is scattered throughout our web site, and we have many pages to maintain. Now imagine that our web service needs to change. Maybe the XML namespace is updated... or maybe we decide to rip out all of our Message Contracts (a bit contrived, but it demonstrates what we’re talking about!). My service becomes;

[ServiceContract(

Namespace =

"https://samples.microsoft.com/jslayering")]

[AspNetCompatibilityRequirements(

RequirementsMode =

AspNetCompatibilityRequirementsMode.Allowed)]

public class PersonService

{

[OperationContract]

public Person GetPerson(int identifier)

{

Implementation

}

}

Immediately our auto-generated proxy changes and the JavaScript code breaks. There’s no Found property anymore, we just get a null Person back if there is no match. And we can’t access a Person property on the Message Contract because it isn’t there... we’ve got a Person instead.

Obviously applying these changes site-wide would be a real headache. But for us, it’s easy. We just change a few lines! We’ll just tweak our _onSuccess call-back in the Service Agent;

_onSuccess: function(result, context) {

// note we only check for a result, not result.Found

if (!result)

this._onFailure({ get_message: function()

{ return 'Person not found'; } },

context);

else {

// note the changes when building our return entity –

// no ".Person." message contract syntax

var returnentity =

new ServiceAgents.Person(

result.Identifier,

result.Name,

result.Age);

context(returnentity);

}

}

Hey presto! Our site works again, and you can leave the office at a sensible time!

Conclusion

Obviously this is extra coding effort to put together a Service Agent, but do you see the benefits? Is it worthwhile? What about other patterns? How about View-Presenter, for example? Do they apply to JavaScript? Do you use this kind of approach already?

I’d love to know what you think, and what you do right now... feel free to comment or blog back at me.

The attached code should demonstrate all this – there are “-before” and “-after” web sites, both containing implementations of this page with a Service Agent and using direct calls against the auto-generated proxy.

In the “after” site I’ve removed the Message Contracts and tweaked the Service Agent... but I haven’t fixed the direct proxy calls so you can see how it breaks.

Enjoy...

 

JsLayering.zip

Comments