Showing posts with label JScript. Show all posts
Showing posts with label JScript. Show all posts

Thursday, 9 June 2011

CRM 2011 - Trigger Save On Silverlight Control

An issue that arises quite often when developing Silverlight controls for Microsoft Dynamics CRM 2011 is the ability to trigger a save on the Silverlight control when the form containing it is saved. The obvious method of doing this is to execute an exposed scriptable method on the silverlight controls itself to perform the update, and this is part way there.
Where an issue will arise is that supported method for saving records in silverlight is using either the SOAP or OData web services and the service model for silverlight is a asynchronous model.  This would mean the processes would appear as follows:
  1. Save button is clicked.
  2. Save is triggered on silverlight control
  3. Form save is under taken
  4. Form is refreshed.
This will cause an issue as the processing at step #3 will not wait until step #2 is completed.  This could cause the form to then refresh, which will end the web request and this may mean the silverlight save is cut off mid process. 
Because of this I have tried a number of options with no success, these include:
  1. Creating a javascript wait method to loop until the silverlight control is finish.  The issue with this method is that the javascript and silverlight control utilise the same thread and thus all you do is cause the browser to hang indefinitely.
  2. Putting a wait condition on the main thread within silverlight after the async process has started and to continue waiting until the process is complete.  The issue with this is the browsers main thread and the silverlight main thread are one and the same so there is nothing to tell the control that the time interval has elapse and to check again.  This will also cause the browser to hang.
  3. Attempt to wrap the whole process up to "fake" a synchronous process. This would cause the browser to appear to hang untill the process was complete.  This would partially work but not a friendly user experience.
With that said though with each failure you are one step closer to success, which happened to be the next option that was tried.... so here goes.


Process is as follows:
  1. User clicks save button.
  2. Javascript calls exposed method on silverlight control.
  3. Silverlight control checks if anything needs saving. If so...
    1. Provide feedback to user that control is saving.
    2. Cancel the save event on the form using the "preventDefault" method provided.
    3. Record the save method used to save using the "getSaveMethod" JScript method.
    4. Save the silverlight information.
    5. Set the silverlight control to "clean"
    6. Based on the save method call the JScript "save" method on the form.
What this will allow is the silverlight control to finish saving before the form saves, the "is dirty" function of the silverlight control will prevent the endless loop and finally there is no freezing of the users browser.


Example silverlight code:

        [ScriptableMember]
        public bool Save(object Context)
        {
            // only save a dirty control
            if (IsDirty && _AllowSave)
            {
                bsyIndicator.BusyContent = "Saving Changes...";
                bsyIndicator.IsBusy = true;
 
                ((dynamic)Context).preventDefault();
 
                _SaveMode = (Constants.SaveMode)int.Parse(((dynamic)Context).getSaveMode().ToString());
                
                // save the dirty control
                _recordHelper.Save_Complete += SaveComplete;
                _recordHelper.SaveRecord(record);
 
                return false;
            }
 
            IsDirty = true;
            return true;
        }


        private void SaveComplete(object sender, EventArgs e)
        {
            // do nothing on callback
            bsyIndicator.IsBusy = false;
 
            IsDirty = false;
 
            // call save again
            dynamic xrm = HtmlPage.Window.GetProperty("Xrm");
 
            switch(_SaveMode)
            {
                case Constants.SaveMode.Send:
                    xrm.Page.data.entity.save();
                    break;
                case Constants.SaveMode.SaveClose:
                    xrm.Page.data.entity.save("saveandclose");
                    break;
                case Constants.SaveMode.SaveNew:
                    xrm.Page.data.entity.save("saveandnew");
                    break;
                case Constants.SaveMode.SaveCompleted:
                case Constants.SaveMode.Deactivate:
                    // this is unsupported
                    HtmlPage.Window.Invoke("SaveAsCompleted");
                    break;
                default:
                    xrm.Page.data.entity.save();
                    break;
            }
 
            _RecordHelper.Save_Complete -= SaveComplete;
        }
 
Example JScript Code:

ExecuteSilverlightMethod = function (context, controlName, namespace, methodName) {
         var controlObj = Xrm.Page.ui.controls.get(controlName);
         silverLightControl = controlObj.getObject();
 
         eval("silverLightControl.Content." + namespace + "." + methodName + "(context.getEventArgs());");
     }

CRM 2011 - Setting State from JScript

With the introduction of the new OData service endpoint for Microsoft Dynamics CRM 2011 has made making simple queries, creates, deletes and updates from JScript a lot cleaner and quicker than using the older SOAP method.
With that said though setting the state of a record cannot be performed using the OData endpoint.  You can write an OData request that will not error when you try to set the state of the record but it will simply do nothing.  The answer to this problem is to use the new SOAP service endpoint and the SetStateRequest.
The SDK comes with a nice tool call SoapLogger that will output a properly formed SOAP message from C# code.  See the SDK on how to use this tool to generate the message. Once you have your message all you need to do is copy the message into you JScript library, modify the required values and your on your way.


The following is an example of and update appointment state message as generated by the SoapLogger tool:

<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="b:SetStateRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.microsoft.com/crm/2011/Contracts">
        <a:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
          <a:KeyValuePairOfstringanyType>
            <c:key>EntityMoniker</c:key>
            <c:value i:type="a:EntityReference">
              <a:Id>cecbc035-978d-e011-9465-000c297f4e3a</a:Id>
              <a:LogicalName>appointment</a:LogicalName>
              <a:Name i:nil="true" />
            </c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>State</c:key>
            <c:value i:type="a:OptionSetValue">
              <a:Value>2</a:Value>
            </c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>Status</c:key>
            <c:value i:type="a:OptionSetValue">
              <a:Value>4</a:Value>
            </c:value>
          </a:KeyValuePairOfstringanyType>
        </a:Parameters>
        <a:RequestId i:nil="true" />
        <a:RequestName>SetState</a:RequestName>
      </request>
    </Execute>
  </s:Body>
</s:Envelope>

This was then converted into a JScript/JQuery request to the server to cancel the current appointment:


function closeAppointment() {
 
    if (Xrm.Page.data.entity.getIsDirty()) {
        alery("Please save your changes before cancelling the appointment.");
        return;
    }
 
    // create the request
    var request = "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">";
    request += "<s:Body>";
    request += "<Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">";
    request += "<request i:type=\"b:SetStateRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\" xmlns:b=\"http://schemas.microsoft.com/crm/2011/Contracts\">";
    request += "<a:Parameters xmlns:c=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">";
    request += "<a:KeyValuePairOfstringanyType>";
    request += "<c:key>EntityMoniker</c:key>";
    request += "<c:value i:type=\"a:EntityReference\">";
    request += "<a:Id>" + Xrm.Page.data.entity.getId() + "</a:Id>";
    request += "<a:LogicalName>appointment</a:LogicalName>";
    request += "<a:Name i:nil=\"true\" />";
    request += "</c:value>";
    request += "</a:KeyValuePairOfstringanyType>";
    request += "<a:KeyValuePairOfstringanyType>";
    request += "<c:key>State</c:key>";
    request += "<c:value i:type=\"a:OptionSetValue\">";
    request += "<a:Value>2</a:Value>";
    request += "</c:value>";
    request += "</a:KeyValuePairOfstringanyType>";
    request += "<a:KeyValuePairOfstringanyType>";
    request += "<c:key>Status</c:key>";
    request += "<c:value i:type=\"a:OptionSetValue\">";
    request += "<a:Value>4</a:Value>";
    request += "</c:value>";
    request += "</a:KeyValuePairOfstringanyType>";
    request += "</a:Parameters>";
    request += "<a:RequestId i:nil=\"true\" />";
    request += "<a:RequestName>SetState</a:RequestName>";
    request += "</request>";
    request += "</Execute>";
    request += "</s:Body>";
    request += "</s:Envelope>";
 
    //send set state request
    $.ajax({
        type: "POST",
        contentType: "text/xml; charset=utf-8",
        datatype: "xml",
        url: Xrm.Page.context.getServerUrl() + "/XRMServices/2011/Organization.svc/web",
        data: request,
        beforeSend: function (XMLHttpRequest) {
            XMLHttpRequest.setRequestHeader("Accept""application/xml, text/xml, */*");
            XMLHttpRequest.setRequestHeader("SOAPAction""http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
        },
        success: function (data, textStatus, XmlHttpRequest) {
            Xrm.Page.ui.close();
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert(errorThrown);
        }
    });    
}

This particular example has allowed me to override the default function of the close appointment.