Pages

Friday, November 20, 2009

Embedding Advanced Find Views in Entity Forms (Version 6)

So, I decided to take another cue from Adi on the implementation of embedding Advanced Finds from his blog. My previous version would initialize and load the AF in the IFrame on the form's load. This caused trouble, in that if the IFrame was on a tab other than the first, the tab's code would cause the src attribute of the IFrame to load, and replace the AF content.

Originally, I had coded around this by setting the src to null, which effectively prevented the AF from being erased. However, even though my code loads the AF view asynchronously (a distinct advantage over Adi's code), it still adds unnecessary overhead to the form when any of its entities were opened.

So, falling back on Adi's technique of hooking into the onreadystatechange handler, I've reverted to the platform behavior of loading only if the tab containing the IFrame is displayed (including the first tab). The added bonus is that since I'm no longer overwriting the src attribute, the domain of the IFrame doesn't change, reducing the tricky cross-frame scripting permissions needed from IE to make it work.

Additionally, I added a few more optional parameters to the function that allows the "New" button on the Advanced Find view to establish child records connected to a parent record other than the one holding the view.

Version 6:

/// Summary:
/// Provides a mechanism for replacing the contents of any Iframe on an entity form
/// with any Advanced Find view.
/// 
/// Param            Description
/// ----------            -------------------
/// iFrameId            The id established for the target Iframe
/// entityName            The name of the entity to be found by the Advanced Find
/// fetchXml            FetchXML describing the query for the entity
/// layoutXml            LayoutXML describing the display of the entity
/// sortCol            The schema name of the entity attribute used for primary sorting
/// sortDescend         "true" if sorting the sortCol by descending values, or "false" if ascending
/// defaultAdvFindViewId    The GUID of an Advanced Find View for the entity; may be that of a saved view
/// entityTypeId        (Optional) The Object Type ID for the entity.  Setting this causes the system
///                to overwrite the functionality of the "New" button to establish related records
/// relatedTypeId        (Optional) The Object Type ID for the related entity on which to establish new
///                records.  Dependent on entityTypeId.  Defaults to crmForm.ObjectTypeCode
/// relatedObjectId        (Optional) The Object ID for the related entity on which to establish new records.
///                Dependent on entityTypeId.  Defaults to crmForm.ObjectId

function EmbedAdvancedFindView (iFrameId, entityName, fetchXml, layoutXml, sortCol, sortDescend, defaultAdvFindViewId, entityTypeId, relatedTypeId, relatedObjectId) {
  // Initialize our important variables
  var url = SERVER_URL + "/AdvancedFind/fetchData.aspx";
  var iFrame = document.getElementById(iFrameId);

  // Provide a global function within the parent scope to avoid XSS limitations
  // in updating the iFrame with the results from our HTTP request
  PushResponseContents = function (iFrame, httpObj, entityTypeId) {
    var win = iFrame.contentWindow;
    var doc = iFrame.contentWindow.document;
    var m_iFrameShowModalDialogFunc = null;
    var m_windowAutoFunc = null;

    // Write the contents of the response to the Iframe
    doc.open();
    doc.write(httpObj.responseText);
    doc.close();

    // Set some style elements of the Advanced Find window
    // to mesh cleanly with the parent record's form
    doc.body.style.padding = "0px";
    doc.body.scroll="no";

    // Should we overwrite the functionality of the "New" button?
    if ((typeof(entityTypeId) != "undefined") && (entityTypeId != null)) {
      var buttonId = "_MBopenObj" + entityTypeId;
      var newButton = doc.getElementById(buttonId);

      if (newButton != null) {
        if ((typeof(relatedTypeId) == "undefined") || (relatedTypeId == null)) {
          relatedTypeId = crmForm.ObjectTypeCode;
        }

        if ((typeof(relatedObjectId) == "undefined") || (relatedObjectId == null)) {
          relatedObjectId = crmForm.ObjectId;
        }

        eval("newButton.action = 'locAddRelatedToNonForm(" + entityTypeId + ", " + relatedTypeId + ", \"" + relatedObjectId + "\",\"\");'");
      }
    }

    // Swap the showModalDialog function of the iFrame 
    if (m_iFrameShowModalDialogFunc == null) {
      m_iFrameShowModalDialogFunc = win.showModalDialog;
      win.showModalDialog = OnIframeShowModalDialog;
    }

    if (m_windowAutoFunc == null) {
      m_windowAutoFunc = win.auto;
      win.auto = OnWindowAuto;
    }

    // Configure the automatic refresh functionality for dialogs
    function OnIframeShowModalDialog(sUrl, vArguments, sFeatures) {
      var returnVar = m_iFrameShowModalDialogFunc(sUrl, vArguments, sFeatures);
      if (sUrl.search(/OnDemandWorkflow/) < 0) {
        doc.all.crmGrid.Refresh();
      }
      return returnVar;
    }

    function OnWindowAuto(otc) {
      doc.all.crmGrid.Refresh();
 
      m_windowAutoFunc(otc);
    }
  }

  // Set the onreadystatechange handler of the IFrame to overwrite the contents dynamically
  iFrame.onreadystatechange = function() {
    if (iFrame.readyState == "complete") {
      var doc = iFrame.contentWindow.document;
      var httpObj = new ActiveXObject("Msxml2.XMLHTTP");
      
      iFrame.onreadystatechange = null;

      // Preload the iFrame with some HTML that presents a Loading image
      var loadingHtml = ""
        + "<table height='100%' width='100%' style='cursor:wait'>"
        + "  <tr>"
        + "    <td valign='middle' align='center'>"
        + "      <img alt='' src='/_imgs/AdvFind/progress.gif' />"
        + "      <div /><i>Loading View...</i>"
        + "    </td>"
        + "  </tr>"
        + "</table>";
    
      doc.open();
      doc.write(loadingHtml);
      doc.close();
    
      // Compile the FetchXML, LayoutXML, sortCol, sortDescend, defaultAdvFindViewId, and viewId into
      // a list of params to be submitted to the Advanced Find form
      var params = "FetchXML=" + fetchXml
        + "&LayoutXML=" + layoutXml
        + "&EntityName=" + entityName
        + "&DefaultAdvFindViewId=" + defaultAdvFindViewId
        + "&ViewType=1039"              // According to Michael Hohne over at Stunnware, this is static
        + "&SortCol=" + sortCol
        + "&SortDescend=" + sortDescend;
    
      // Establish an async connection to the Advanced Find form
      httpObj.open("POST", url, true);
    
      // Send the proper header information along with the request
      httpObj.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
      httpObj.setRequestHeader("Content-length", params.length);
    
      // Function to write the contents of the http response into the iFrame
      httpObj.onreadystatechange = function () {
        if (httpObj.readyState == 4 && httpObj.status == 200) {
          parent.PushResponseContents(iFrame, httpObj, entityTypeId);
        }
      }
    
      // Set it, and forget it!
      httpObj.send(params);
    }
  }
}

Wednesday, November 4, 2009

Easy to use JavaScript Retrieve and RetrieveMultiple functions

function MischiefMayhemSOAP(xmlSoapBody, soapActionHeader) {
  var xmlReq = "<?xml version='1.0' encoding='utf-8'?>"
    + "<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"
    + "  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
    + "  xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"
    + GenerateAuthenticationHeader()
    + "  <soap:Body>"
    + xmlSoapBody
    + "  </soap:Body>"
    + "</soap:Envelope>";

  var httpObj = new ActiveXObject("Msxml2.XMLHTTP");
    
  httpObj.open('POST', '/mscrmservices/2007/crmservice.asmx', false);

  httpObj.setRequestHeader('SOAPAction', soapActionHeader);
  httpObj.setRequestHeader('Content-Type', 'text/xml; charset=utf-8');
  httpObj.setRequestHeader('Content-Length', xmlReq.length);

  httpObj.send(xmlReq);

  var resultXml = httpObj.responseXML;

  var errorCount = resultXml.selectNodes('//error').length;
  if (errorCount != 0) {
    var msg = resultXml.selectSingleNode('//description').nodeTypedValue;
    alert("The following error was encountered: " + msg);
    return null;
  } else {
    return resultXml;
  }
}
function RetrieveRecord(entityName, entityId, attrArray) {
  var xmlSoapBody = ""
    + "    <Retrieve xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"
    + "      <entityName>" + entityName + "</entityName>"
    + "      <id>" + entityId + "</id>"
    + "      <columnSet xmlns:q1='http://schemas.microsoft.com/crm/2006/Query' xsi:type='q1:ColumnSet'>"
    + "        <q1:Attributes>";

  for (index in attrArray) {
    xmlSoapBody += "          <q1:Attribute>" + attrArray[index] + "</q1:Attribute>";
  }

  xmlSoapBody += ""
    + "        </q1:Attributes>"
    + "      </columnSet>"
    + "    </Retrieve>";

  var resultXml = MischiefMayhemSOAP(xmlSoapBody, 'http://schemas.microsoft.com/crm/2007/WebServices/Retrieve');

  if (resultXml != null) {
    var resultArray = new Array();

    for (index in attrArray) {
      if (resultXml.selectSingleNode("//q1:" + attrArray[index]) != null) {
        resultArray[index] = resultXml.selectSingleNode("//q1:" + attrArray[index]).nodeTypedValue;
      } else {
        resultArray[index] = null;
      }
    }

    return resultArray;
  } else {
    return null;
  }
}
function RetrieveMultiple(entityName, attrArray, distinct, criteriaXml)  {
  var xmlSoapBody = ""
    + "    <RetrieveMultiple xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"
    + "      <query xmlns:q1='http://schemas.microsoft.com/crm/2006/Query' xsi:type='q1:QueryExpression'>"
    + "        <q1:EntityName>" + entityName + "</q1:EntityName>"
    + "        <q1:ColumnSet xsi:type='q1:ColumnSet'>"
    + "          <q1:Attributes>";

  for (index in attrArray) {
    xmlSoapBody += "            <q1:Attribute>" + attrArray[index] + "</q1:Attribute>";
  }

  xmlSoapBody += ""
    + "          </q1:Attributes>"
    + "        </q1:ColumnSet>"
    + "        <q1:Distinct>" + distinct + "</q1:Distinct>"
    + criteriaXml
    + "      </query>"
    + "    </RetrieveMultiple>";

  var resultXml = MischiefMayhemSOAP(xmlSoapBody, 'http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple');

  if (resultXml != null) {
    var resultArray = new Array();
    var entityNodes = resultXml.selectNodes("//RetrieveMultipleResult/BusinessEntities/BusinessEntity");

    if (entityNodes.length > 0) {
        for (var index = 0; index < entityNodes.length; index++) {
          var entityNode = entityNodes[index];
          resultArray[index] = new Array();
    
          for (attrIndex in attrArray) {
            if (entityNode.selectSingleNode("q1:" + attrArray[attrIndex]) != null) {
              resultArray[index][attrIndex] = entityNode.selectSingleNode("q1:" + attrArray[attrIndex]).nodeTypedValue;
            } else {
              resultArray[index][attrIndex] = null;
            }
          }
        }
        
        return resultArray;
    } else {
      return null;
    }
  } else {
    return null;
  }
}
These functions are built around SOAP implementations of the Retrieve and RetrieveMultiple messages. I haven't annotated my code, so forgive any obvious lack of description of what the code is doing. I'll try to briefly summarize here:
MischiefMayhemSOAP
Parameters:
xmlSoapBody - (string) XML containing the body of the SOAP call
soapActionHeader - (string) The 'SOAPAction' specifying the type of SOAP call
Purpose:
Serves as a generic XMLHTTP interface to CRM's SOAP functions.
Returns:
(string) XML result from the CRM server.
RetrieveRecord
Parameters:
entityName - (string) The CRM-platform name of the desired entity
entityId - (string) The GUID of the record to retrieve
attrArray - (string[]) An Array of attributes to retrieve for the record
Purpose:
Easily retrieves a single record from CRM.
Returns:
(string[] or null) An Array of values from CRM, index-matching the attributes provided in attrArray. Returns null if there is an error with, or invalid return from, the SOAP call.
RetrieveMultiple
Parameters:
entityName - (string) The CRM-platform name of the desired entity
attrArray - (string[]) An Array of attributes to retrieve for the records
distinct - (string:"true" or "false") Directs CRM to return with or without duplicate records
criteriaXml - (string) XML containing any combination of Criteria or LinkEntities elements pertaining to the "RetrieveMultiple" SOAP message
Purpose:
Easily retrieves multiple records from CRM which match the rules defined in the criteriaXml.
Returns:
(string[n][] or null) An Array, with n matching-record elements, of an array of values from CRM, index-matching the attributes provided in attrArray. Returns null if there is an error with, or invalid return from, the SOAP call.