Sunday, May 8, 2011

WCF + REST

Good grief...after a long night and most of the morning I finally figured out (via trial-and-error, of course) how to correctly define my WCF contract to work with the dojox.rpc.Rest, dojox.rpc.Service and dojox.data.JsonRestStore.

Here is my interface for a "Echo" resource:


    [DataContract]
    public class EchoValue
    {
        [DataMember]
        public string Id;
        [DataMember]
        public string Value;
        [DataMember]
        public UInt64 Version;
    }

    [ServiceContract]
    public interface IDataServices
    {
        [OperationContract]
        [WebInvoke(Method = "DELETE", UriTemplate = "echo/{id}")]
        void DeleteEcho(string id);

        [OperationContract]
        [WebInvoke(Method = "PUT", UriTemplate = "echo/{id}", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
        EchoValue PutEcho(string id, EchoValue data);

        [OperationContract]
        [WebInvoke(Method = "POST", UriTemplate = "echo", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
        EchoValue AddEcho(EchoValue data);

        [OperationContract]
        [WebGet(ResponseFormat = WebMessageFormat.Json, UriTemplate="echo/{id}")]
        EchoValue GetEcho(string id);

        [OperationContract]
        [WebGet(ResponseFormat = WebMessageFormat.Json, UriTemplate = "echo")]
        List GetEchos();
    }

First, I hand-coded a service object to understand the details...

    var echoService = function (id) {
        return dojo.xhrGet({
            url: url + "/" + id,
            handleAs: "json",
            load: onLoad,
            error: onError
        });
    };
    dojo.mixin(echoService, {
        put: function (id, value) {
            return dojo.xhrPut({
                url: url + "/" + id,
                handleAs: "json",
                contentType: "application/json",
                putData: dojo.toJson({ Value: value }),
                load: onLoad,
                error: onError
            });
        },
        post: function (id, value) {
            return dojo.xhrPost({
                url: url,
                handleAs: "json",
                contentType: "application/json",
                postData: dojo.toJson({ Id: id, Value: value }),
                load: onLoad,
                error: onError
            });
        },
        "delete": function (id) {
            return dojo.xhrDelete({
                url: url + "/" + id,
                handleAs: "json",
                load: onLoad,
                error: onError
            });
        }
    });
And tested it...

    echoService["delete"]("X").then(function () {
        echoService.post("X", ++key).then(function () {
            echoService["delete"]("X").then(function () {
                echoService.post("X", ++key).then(function () {
                    echoService.put("X", ++key).then(function () {
                        echoService("X").then(function () {
                            dojo.publish("/ready");
                        }); ;
                    });
                });
            });
        });
    });

I also experimented with dojox.rpc.Rest...

  var restService = dojox.rpc.Rest(url + "/", true);
And tested it on its own and with dojox.data.JsonRestStore...

    var restStore = new dojox.data.JsonRestStore({ service: restService });
    restService.put("Y", dojo.toJson({ Value: ++key })).then(function () {
        restStore.fetchItemByIdentity({
            identity: "Y",
            onItem: function (data) {
                restStore.setValue(data, "Value", key++);
                restStore.save();
            },
            onError: onError
        });
    });

I also tested dojox.data.JsonRestStore on its own...

 var store = new dojox.data.JsonRestStore({ target: url, idAttribute: "id" });
 store.newItem({ id: "Z", Value: ++key });
 store.save();
 dojo.publish("/ready");
 
 dojo.subscribe("/ready", function () {
     store.fetchItemByIdentity({
         identity: "Z",
         onItem: function (data) {
             dojo.publish("/update", [data]);
         },
         onError: onError
     });
 });
 
 dojo.subscribe("/update", function (item) {
     store.setValue(item, "Value", ++key);
     store.save();
     dojo.publish("/delete", [item]);
 });
 
 dojo.subscribe("/delete", function (item) {
     store.deleteItem(item);
     store.save();
 });

Things to notice:
The dojox.rpc.Rest requires the URL to end in a "/"
The dojox.data.JsonRestStore defines idAttribute
If you define your WCF REST services "correct" the JsonRestStore works without requiring a service.
The POST and PUT body style is WebMessageBodyStyle.Bare, indicating that the parameter name "data" is not part of the request. Without this it is necessary to wrap the parameter in a "Data" container, for example, putData: dojo.toJson({ Data: {Value: 1} }).

links:
http://docs.dojocampus.org/dojox/rpc/Rest
http://docs.dojocampus.org/dojox/rpc/Service
http://dojotoolkit.org/reference-guide/dojox/data/JsonRestStore.html
http://dojotoolkit.org/reference-guide/dojo/data/ItemFileWriteStore.html
http://dojotoolkit.org/reference-guide/dojo/data/api/Write.html
using-the-dojo-toolkit-with-microsofts-wcf/
http://www.sitepen.com/blog/2008/11/21/effective-use-of-jsonreststore-referencing-lazy-loading-and-more/
http://www.sitepen.com/blog/2008/06/13/restful-json-dojo-data/
http://www.sitepen.com/blog/2008/03/19/pluggable-web-services-with-smd/

Dijit.ByType

I couldn't find anything like this in the dijit library so here's a way to get a dijit with a certain declaredClass:

        dijit.byType = function (type, scope) {
            var scope = scope || dojo.body();
            var result = [];
            var f = function (scope) {
                dojo.forEach(dijit.findWidgets(scope), function (w) {
                    if (w.declaredClass === type) result.push(w);
                    f(w.domNode);
                });
            };
            f(scope);
            return result;
        };

Usage:

dijit.byType("dijit.Tree", dojo.body());

Sunday, May 1, 2011

WCF + JSONP

How to call a WCF service running on one server from the dojo web application hosted on another server? Use JSONP.

Thanks to http://www.gutgames.com/ this was a fairly painless process.

First I had to create the WCF service:

    public class JsonServices : IJsonServices
    {
        [WebGet(ResponseFormat=WebMessageFormat.Json)]
        public string Echo(string value)
        {
            return value;
        }
    }

Then I had to make my web.config changes:
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="Dojo16.services.JsonServices" behaviorConfiguration="JsonServicesBehavior">
        <endpoint address="" binding="webHttpBinding" contract="Dojo16.services.IJsonServices" behaviorConfiguration="JsonServicesBehavior" bindingConfiguration="JsonServicesBinding"/>
      </service>
    </services>
    <bindings>
      <webHttpBinding>
        <binding name="JsonServicesBinding" crossDomainScriptAccessEnabled="true"></binding>
      </webHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="JsonServicesBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="JsonServicesBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
</configuration>

Finally, I had to test if from a browser:
<div id="workArea">Hello World!</div>
    <script type="text/javascript">
        var djConfig = { parseOnLoad: true };
    </script>
    <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojo/dojo.xd.js" type="text/javascript"></script>
    <script type="text/javascript">
        dojo.require("dojo.string");
        dojo.require("dojo.io.script");
        dojo.ready(function () {
            dojo.byId("workArea").innerHTML = dojo.string.substitute("<b>Hello ${0}!</b>", ["Dojo"]);
            var targetNode = dojo.create("div", { innerHTML: "Target" });
            dojo.place(targetNode, "workArea", "after");
            var jsonpArgs = {
                url: "http://localhost:64290/Infor/Quiz/services/JsonServices.svc/Echo",
                callbackParamName: "callback",
                content: {
                    value: "dojo toolkit"
                },
                load: function (data) {
                    targetNode.innerHTML = "<pre>" + dojo.toJson(data, true) + "</pre>";
                },
                error: function (error) {
                    targetNode.innerHTML = "An unexpected error occurred: " + error;
                }
            };
            setTimeout(function () {
                dojo.io.script.get(jsonpArgs);
            }, 2000);
        });
    </script>

Links
http://www.gutgames.com/post/Enabling-JSONP-in-WCF.aspx