Tuesday, March 15, 2011

CRM 2011 Jscript Soap Request Formatter RELEASED!!

I know some of you have seen the crazy Jscript libraries I have been putting up here recently.  What if you could take the XML from a captured request and create one of those things instantaniously?  Well, now you can with my CRM 2011 Jscript SOAP Request Formatter, hosted on CodePlex, it takes much of the tediousness out of formatting straight XML for Jscript SOAP calls within CRM 2011.

Link to CodePlex project: http://crm2011soap.codeplex.com/

How Do You Use It.

It would be nice to just use fiddler for viewing traffic, but if you view the traffic on the new organization service endpoint  you can't see any of the traffic as it is encrypted
In the SDK, if you go to /SDK/helpercode/cs/client, there is a soaplogger solution included that allows you to capture decrypted SOAP envelope requests and responses that you send to and from the CRM 2011 SOAP Endpoint.  

There is a tutorial on creating interactive web resource libraries from the soaplogger captured requests here:

http://msdn.microsoft.com/en-us/library/gg594434.aspx

When you finish with the soaplogger you get an output text that contains a request envelope in the "HTTP REQUEST" section of the document that might look like this:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <request i:type="a:InsertOptionValueRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
        <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
          <a:KeyValuePairOfstringanyType>
            <b:key>Label</b:key>
            <b:value i:type="a:Label">
              <a:LocalizedLabels>
                <a:LocalizedLabel>
                  <a:IsManaged i:nil="true" />
                  <a:Label>testoptionCode9</a:Label>
                  <a:LanguageCode>1033</a:LanguageCode>
                </a:LocalizedLabel>
              </a:LocalizedLabels>
              <a:UserLocalizedLabel i:nil="true" />
            </b:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <b:key>AttributeLogicalName</b:key>
            <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema%22%3Enew_testoptionset%3C/b:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <b:key>EntityLogicalName</b:key>
            <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema%22%3Eaccount%3C/b:value>
          </a:KeyValuePairOfstringanyType>
        </a:Parameters>
        <a:RequestId i:nil="true" />
        <a:RequestName>InsertOptionValue</a:RequestName>
      </request>
    </Execute>
  </s:Body>
</s:Envelope>

Ok, you can open the .exe file contained in the /bin/debug folder from the downloaded release now.

Next, if you copy this envelope into the Soap Formatter UI (right now you have to do all copy and past and select-all operations using right click on the textbox in the Soap Formatter UI :( I am sure this will be fixed at some later point) and type in a namespace (we will use "EXAMPLE") and a function name (we will use "InsertOptionValue").  It should look like this.




Now if you hit generate it will tranform the insert XML into the JScript call that is ready to be imported in CRM as a Web Resource and utilized on your forms.  This is what it will look like using our example.

if (typeof (SDK) == "undefined")
   { SDK = { __namespace: true }; }
       //This will establish a more unique namespace for functions in this library. This will reduce the 
       // potential for functions to be overwritten due to a duplicate name when the library is loaded.
       SDK.EXAMPLE = {
           _getServerUrl: function () {
               ///<summary>
               /// Returns the URL for the SOAP endpoint using the context information available in the form
               /// or HTML Web resource.
               ///</summary>
               var OrgServicePath = "/XRMServices/2011/Organization.svc/web";
               var serverUrl = "";
               if (typeof GetGlobalContext == "function") {
                   var context = GetGlobalContext();
                   serverUrl = context.getServerUrl();
               }
               else {
                   if (typeof Xrm.Page.context == "object") {
                         serverUrl = Xrm.Page.context.getServerUrl();
                   }
                   else
                   { throw new Error("Unable to access the server URL"); }
                   }
                  if (serverUrl.match(/\/$/)) {
                       serverUrl = serverUrl.substring(0, serverUrl.length - 1);
                   } 
                   return serverUrl + OrgServicePath;
               }, 
           InsertOptionValueRequest: function () {
               var requestMain = ""
               requestMain += "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">";
               requestMain += "  <s:Body>";
               requestMain += "    <Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">";
               requestMain += "      <request i:type=\"a:InsertOptionValueRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">";
               requestMain += "        <a:Parameters xmlns:b=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">";
               requestMain += "          <a:KeyValuePairOfstringanyType>";
               requestMain += "            <b:key>Label</b:key>";
               requestMain += "            <b:value i:type=\"a:Label\">";
               requestMain += "              <a:LocalizedLabels>";
               requestMain += "                <a:LocalizedLabel>";
               requestMain += "                  <a:IsManaged i:nil=\"true\" />";
               requestMain += "                  <a:Label>testoptionCode9</a:Label>";
               requestMain += "                  <a:LanguageCode>1033</a:LanguageCode>";
               requestMain += "                </a:LocalizedLabel>";
               requestMain += "              </a:LocalizedLabels>";
               requestMain += "              <a:UserLocalizedLabel i:nil=\"true\" />";
               requestMain += "            </b:value>";
               requestMain += "          </a:KeyValuePairOfstringanyType>";
               requestMain += "          <a:KeyValuePairOfstringanyType>";
               requestMain += "            <b:key>AttributeLogicalName</b:key>";
               requestMain += "            <b:value i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">new_testoptionset</b:value>";
               requestMain += "          </a:KeyValuePairOfstringanyType>";
               requestMain += "          <a:KeyValuePairOfstringanyType>";
               requestMain += "            <b:key>EntityLogicalName</b:key>";
               requestMain += "            <b:value i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">account</b:value>";
               requestMain += "          </a:KeyValuePairOfstringanyType>";
               requestMain += "        </a:Parameters>";
               requestMain += "        <a:RequestId i:nil=\"true\" />";
               requestMain += "        <a:RequestName>InsertOptionValue</a:RequestName>";
               requestMain += "      </request>";
               requestMain += "    </Execute>";
               requestMain += "  </s:Body>";
               requestMain += "</s:Envelope>";
               var req = new XMLHttpRequest();
               req.open("POST", SDK.EXAMPLE._getServerUrl(), true)
               // Responses will return XML. It isn't possible to return JSON.
               req.setRequestHeader("Accept", "application/xml, text/xml, */*");
               req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
               req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
               var successCallback = null;
               var errorCallback = null;
               req.onreadystatechange = function () { SDK.EXAMPLE.InsertOptionValueResponse(req, successCallback, errorCallback); };
               req.send(requestMain);
           },
       InsertOptionValueResponse: function (req, successCallback, errorCallback) {
               ///<summary>
               /// Recieves the assign response
               ///</summary>
               ///<param name="req" Type="XMLHttpRequest">
               /// The XMLHttpRequest response
               ///</param>
               ///<param name="successCallback" Type="Function">
               /// The function to perform when an successfult response is returned.
               /// For this message no data is returned so a success callback is not really necessary.
               ///</param>
               ///<param name="errorCallback" Type="Function">
               /// The function to perform when an error is returned.
               /// This function accepts a JScript error returned by the _getError function
               ///</param>
               if (req.readyState == 4) {
               if (req.status == 200) {
               if (successCallback != null)
               { successCallback(); }
               }
               else {
                   errorCallback(SDK.EXAMPLE._getError(req.responseXML));
               }
           }
       },
       _getError: function (faultXml) {
           ///<summary>
           /// Parses the WCF fault returned in the event of an error.
           ///</summary>
           ///<param name="faultXml" Type="XML">
           /// The responseXML property of the XMLHttpRequest response.
           ///</param>
           var errorMessage = "Unknown Error (Unable to parse the fault)";
           if (typeof faultXml == "object") {
               try {
                   var bodyNode = faultXml.firstChild.firstChild;
                   //Retrieve the fault node
                   for (var i = 0; i < bodyNode.childNodes.length; i++) {
                       var node = bodyNode.childNodes[i];
                       //NOTE: This comparison does not handle the case where the XML namespace changes
                       if ("s:Fault" == node.nodeName) {
                       for (var j = 0; j < node.childNodes.length; j++) {
                           var faultStringNode = node.childNodes[j];
                           if ("faultstring" == faultStringNode.nodeName) {
                               errorMessage = faultStringNode.text;
                               break;
                           }
                       }
                       break;
                   }
               }
           }
           catch (e) { };
        }
        return new Error(errorMessage);
     },
 __namespace: true
};



You can now parse in your form fields or values as needed and you can call the function in the eventhandler setup by using the syntax SDK.{namespace}.{function name}Request, or in our case:

SDK.EXAMPLE.InsertOptionValueRequest

I hope this helps!
I also would love to have a few volunteers help me build the thing out further.

7 comments:

  1. Hi Jamie, I have to spend more time with the tool, but so far I reckon is a great thing to have handy and a great start to probably something bigger. I'm glad to see people contributing like you to the CRM community :)

    ReplyDelete
  2. Thanks, same to you! How's Avanade going for you? I have interviewed with them once or twice. If I was a single guy without a wife and kids I would be all for the travel they require. I am having a good time at Sogeti though too. They were Microsoft Global Enterprise Partner of the Year 2010.

    ReplyDelete
  3. Oh, and if you want to help with the tool at all I can add you as a developer in codeplex for it.

    ReplyDelete
  4. Hi, thank you for this post, really interesting!
    I am having a question.
    What if I want to use this method dynamically?
    Let's say I want to assign Accounts to users but I want to pass the guids as parameters.
    How is it feasible?

    ReplyDelete
  5. You can add the parameters to the functions and parse them into the XML just like you could any other function. The article does assume a certain knowledge of jscript and development fundamentals. The articles on this blog are there then to bridge the gap between general knowledge and how to apply the SDK.

    ReplyDelete
  6. How to capture header part in soaplogger code

    ReplyDelete
    Replies
    1. Auth Header was a CRM 4.0 thing. This is 2011. There is some header data that is sent across the wire that is visible in fiddler but it's not the same sort of thing you had to deal with in 4.0.

      Delete