Pages

Wednesday, March 25, 2009

Microsoft CRM: Embedding Advanced Find Views in Entity Forms (Version 4)

[UPDATE: See Version 5 of the code in this post at http://crmentropy.blogspot.com/2009/04/microsoft-crm-embedding-advanced-find.html]

Thank you Adi Katz! I'll tell you what, it feels good to be thanked by a CRM master for discovering something they overlooked. But that's not the only reason I'm thanking him. He's solved the problem with the automatic refresh of the embedded Advanced Find View grids!

After thorough examination of his new code, I couldn't help but be overcome with a mote of jealousy at its elegance. I've redesigned my code to incorporate the best elements of Adi's solution: namely the automatic refresh hooks, the "Loading View" presentation, and his style adjustments (mine were leaving a space for a scroll bar on the right-hand side of the IFrame--only I didn't know that that's why the space was there until I implemented that part of his code over mine).

I also ran into unexpected "Access Denied" problems regarding the direct interaction that my XMLHTTP object was making to the domain-less IFrame window (and yes, it does not have a domain if it does not have a src/location). Apparently this problem would only rear its ugly head on some machines and not others. On top of that, the problem fixed itself on one machine for no apparent reason, while still manifesting on others.

This prompted me to rewrite the code that pushes content into the IFrame. It should be noted that I suspect "Access Denied" problems people have reported with Adi's code similar to this may fall into the same boat. But his code differs from mine in that he constructs a form into the IFrame, which changes the IFrame's src/location and consequently its domain. This domain should be identical to the domain of the parent window, so XSS limitations shouldn't apply. But, not actually having used his code or produced the error, I can't say for certain.

Here's Version 4:

/// 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 
/// 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

function EmbedAdvancedFindView (iFrameId, entityName, fetchXml, layoutXml, sortCol, sortDescend, defaultAdvFindViewId, entityTypeId) { 
  // Initialize our important variables 
  var httpObj = new ActiveXObject("Msxml2.XMLHTTP"); 
  var url = SERVER_URL + "/AdvancedFind/fetchData.aspx"; 
  var iFrame = document.getElementById(iFrameId); 
  var win = iFrame.contentWindow; 
  var doc = iFrame.contentWindow.document; 
  
  // 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); 
      
      eval("newButton.action = 'locAddRelatedToNonForm(" + entityTypeId + ", " + crmForm.ObjectTypeCode + ", \"" + crmForm.ObjectId + "\",\"\");'");
    } 
    
    // 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) { 
      m_iFrameShowModalDialogFunc(sUrl, vArguments, sFeatures); 
      doc.all.crmGrid.Refresh(); 
    } 
    
    function OnWindowAuto(otc) { 
      doc.all.crmGrid.Refresh(); 
      m_windowAutoFunc(otc); 
    } 
  } 
  
  // Without a null src, switching tabs in the form reloads the src 
  iFrame.src = 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); 
}

Once again, I'm really grateful to Adi for the enlightenment and code that his solution has provided me with. As I said previously, though I developed my solution primarily in accordance to the recommendations of Michael Höhne's post on the subject without the benefit of exposure to Adi's code, his solution has helped me identify where I could improve my own development since Version 1.

We do things differently within our respective solutions, so I would welcome anybody to weigh our solutions against each other and flesh out the pros and cons to either. As far as I'm concerned, I'm happy with what I have, and I have it for the job I need right now. If anybody else can make use of it, that's a bonus. I'm not trying to steal Adi's thunder, or (much) of his implementation. But I'm naturally biased when it comes to the distinctions between our methods, and find my own efforts more desirable, and I hope I've given him an acceptable amount of credit for the things I've taken from his code.