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);
    }
  }
}

12 comments:

  1. Very nice code, how how would a beginner do the following as mentioned in a previous post?

    3. Next, establish values for all of the parameters, call the function, and you're done!

    ReplyDelete
  2. Thanks for catching that I've been leaving examples out of my posts on this topic. I'll update this post later today with some example usage code.

    ReplyDelete
  3. Hi Dave,

    I think this post is what I'm looking for to help me embed the associated view into a tab for me. I've taken out the part of the code specific to advanced find (so it basically will load any page I want) but I'm a bit lost on what (if anything) needs to be put into the iframe's src attribute. I am putting this onto a custom page with an iframe in it, not a crm form.


    So the structure is like this
    Contact Form Nav Link -> custom asp page -> iframe

    ReplyDelete
  4. For this code, you do not need a to place a value in the src. Personally, I set it to the blank.aspx page that comes with CRM (about:blank tends to have its own security zone and cross-site scripting preventions can cause heartache). This code should work just fine on any Iframe on any website.

    ReplyDelete
  5. Sorry for slacking with the code examples, too. I've been working a lot on a more exciting project that really makes use of this code and takes it places you never thought CRM could go. If I can get the permission to make it public, I anticipate a more thorough presentation of these code pieces.

    ReplyDelete
  6. Great work, David!

    This is one of two external JavaScript snippets that I use in my project. The other one is Adi's form validator (which I made significant enhancements and bug fixes).

    David, you might want to change the following line:

    var url = SERVER_URL + "/AdvancedFind/fetchData.aspx";

    to:

    var url = prependOrgName("/AdvancedFind/fetchData.aspx");

    I was caught by this line because I was using IP address to access my CRM application, but SERVER_URL is actually rendered as http://mycrmserver/, which introduced a cross-domain issue.

    For people who are looking for sample, you can refer to David's another blog post. David, you should know your own blog better than me. :-)

    Thanks David, excellent work!

    ReplyDelete
  7. Daniel,

    I thank you for your suggestion, and I'll try to implement it when I move the project to a managed code-base.

    ReplyDelete
  8. David I am still wondering about how to set it up "3. Next, establish values for all of the parameters, call the function, and you're done!
    "? Thanks!

    ReplyDelete
  9. Daniel linked back to the original post for this project, and it has example code--but only for the simple options. I plan to implement a project page for this solution soon to vastly improve both the code and the documentation. Your patience is appreciated.

    ReplyDelete
  10. I am using this code on a hosted server requiring SSL. It is working on a development server from both server and client machines, and is working from the live Hosted server, but when accessing the hosted server from any other machine, getting an error "The webpage cannot be displayed"

    I have tried changing the URL to include https, but this requests authentication, and does not accept the correct username/password when supplied.

    Any help with this issue is much appreciated.

    ReplyDelete
  11. I am trying to work through the Service Activity (entity serviceappointment) example, and was wondering if the function, IdentifyValidEntityState, is restricting edit of Service Activity records with a 'scheduled' (statecode 3).

    Before I tweak this code, I wanted to confirm that I should go ahead and adjust this line...


    if (stateOption.Value == "0") {
    return stateOption;
    }


    Cheers!

    ReplyDelete
  12. You can adjust that, if you wish. That is the correct place to adjust the value check for the code. I wanted to make that part of the configuration portion of the script, eventually. But you're on the right track.

    ReplyDelete

Unrelated comments to posts may be summarily disposed at the author's discretion.