Pages

Friday, March 6, 2009

Microsoft CRM: Embedding Advanced Find Views in Entity Forms

[UPDATE: See Version 2 of the code in this post at http://crmentropy.blogspot.com/2009/03/microsoft-crm-embedding-advanced-find_10.html]
CRM. What a beautiful beast. I regularly describe the product as Microsoft's best. My exposure and experience with it has been considerable over the last 2 years, and I've found a vast number of helpful resources online to assist me in developing solutions for it.
Finally, it appears as if I may have something to offer the community. Something that has relatively little explanation or resource, and for which I've done a great deal of research and hacking to obtain. So, it is with great pleasure that I introduce my method of embedding Advanced Find views into CRM entity forms:
1. Place an Iframe into the CRM form, with the source "about:blank"
2. Copy the following function to the OnLoad() event for the form:
/// 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 that of a saved view

function EmbedAdvancedFindView (iFrameId, entityName, fetchXml, layoutXml, sortCol, sortDescend, defaultAdvFindViewId) {
// Initialize our important objects
  var iFrame = document.getElementById(iFrameId);
  var httpObj = new ActiveXObject("Msxml2.XMLHTTP");
  var url = "/AdvancedFind/fetchData.aspx";
  
  // 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) {
      // Shorthand to the document of the Iframe
      var doc = iFrame.contentWindow.document;
      
      // Without a null src, switching tabs in the form reloads the src
      iFrame.src = 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.style.backgroundColor = "#eaf3ff";
    }
  }
  
  // Set it, and forget it!
  httpObj.send(params);
}
3. Next, establish values for all of the parameters, call the function, and you're done!
It's a pretty simple process, actually. It makes use of "AJAX" related functionality in IE by way of the "XMLHTTP" object. This object represents a "POST" call to the Advanced Find form given the parameters you establish. When the request is complete, the results are written directly into the Iframe.
For me, this code works beautifully. Compare it to Adi Katz' solution at http://mscrm4ever.blogspot.com/2008/09/display-fetch-in-iframe.html. The main differences between his solution and mine are as follows:
1. He provides a handler a viewer object which provides, among other things, a handy "Refresh" function to update the view when related information changes. For mine, you have to call the function again. Personally, I wrap it in an Update() function for those forms which require it.
2. He uses a hard-coded HTML Form in the Iframe to submit information. Mine uses an ActiveX XMLHTTP object.
3. His displays a handy, "loading" image, while the server interaction is taking place. I'm probably going to duplicate this functionality in mine at some point later.
4. He plugs into the form's event handlers to determine when to load the Advanced Find. If the page is on a different tab, it's not loaded until you view that tab. He does this, I believe, because the form's behavior naturally causes the Iframe to load when the tab containing the Iframe is navigated to--but managed in CRM's internal code. Mine initializes immediately with the record, and since it's asynchronous it shouldn't impact the performance of navigating around the form until the XMLHTTP object returns the results. I did encounter the form's behavior of loading (or, in this case, reloading) Iframes on different tabs, and found it was simply easy to subvert this behavior by nullifying the "src" attribute of the Iframe. Then, the form simply has nothing to load.
I would like to thank Adi for his code. There is hardly a better resource out there for understanding how to interact with the Advanced Find form, outside of Michael Höhne's posting at http://www.stunnware.com/crm2/topic.aspx?id=AdvancedFind1. I credit them both for discovering the protocol for interfacing with Advanced Find. Though, I did only find Adi's code after completing development on mine. (Had I found his first, I probably would have copied it outright, and not made a parallel development.)
So, for an example that uses my code:
// Embed an AF window
fetchXml = ""
  + "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>"
  + " <entity name='account'>"
  + " <attribute name='name'/>"
  + " <attribute name='address1_city'/>"
  + " <attribute name='primarycontactid'/>"
  + " <attribute name='telephone1'/>"
  + " <attribute name='accountid'/>"
  + " <order attribute='name' descending='false'/>"
  + " <filter type='and'>"
  + " <condition attribute='statecode' operator='eq' value='0'/>"
  + " </filter>"
  + " </entity>"
  + "</fetch>";

layoutXml = ""
  + "<grid name='resultset' object='1' jump='name' select='1' icon='1' preview='1'>"
  + " <row name='result' id='accountid'>"
  + " <cell name='name' width='300' />"
  + " <cell name='primarycontactid' width='150' />"
  + " <cell name='telephone1' width='100' />"
  + " <cell name='address1_city' width='100' />"
  + " </row>"
  + "</grid>";

EmbedAdvancedFindView("IFRAME_Accounts", "account", fetchXml, layoutXml, "name", "false", "{00000000-0000-0000-00AA-000000666000}");
In this example, I have an Iframe on a form with the name/id "IFRAME_Accounts". No part of the FetchXml is dynamic, so I really don't need to reload the Advanced Find contextually. If I did, then I would simply wrap the above code in an Update() function of some form, and call it whenever relevant fields on the form were changed.
My next task is finding out how to change the New button on the embedded AF view to make a new target record related to the record holding the form. I think top.locAddObjTo() will do the trick, if I can programmatically force that into the action attribute of the menu button.