Pages

Monday, December 21, 2009

Hooking into the CRM Grid Refresh

For those of us who heavily extend CRM with javascript, there is an event handler I've just found and used for a project that I'm working on that allows you to hook into the refresh of a crmGrid object.  This is an example of how to use the event:

function GridRefresh () {
  alert("The grid has refreshed.");
}

document.all.crmGrid.attachEvent("onrefresh", GridRefresh);

UPDATE: I discovered recently that this event will not be triggered in the event of a view change.  As near as I can tell, there is no event that is triggered when the view is changed.

Monday, December 14, 2009

Fire an event on any CRM field change

The following code shows how you can attach any function to all of the fields on a CRM form for execution when any of them change.  It's based on Michael Höhne's code to find all fields on a form:

FieldChange = function () {
  alert("Change!");
}

for (var index in crmForm.all) {
  var control = crmForm.all[index];
  if (control.req && (control.Disabled != null)) {
    control.attachEvent("onchange", FieldChange);
  }
}

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.

Tuesday, October 13, 2009

Default Multi-column Sort (Proof of Concept)

Being able to sort by multiple columns in CRM is quite handy, but unfortunately there are no mechanisms for configuring a default multi-column sort. Until now. The following code will illustrate how to accomplish it:
var stageFrame = document.getElementsByName("stage")[0]; 
var stageFrameDoc = stageFrame.contentWindow.document; 
crmGrid = stageFrameDoc.all.crmGrid; 
crmGrid.all.divGridProps.children["sortColumns"].value = "statuscode:0;casetypecode:1"; 
crmGrid.Refresh()
I spent some time trying to answer this question: http://social.microsoft.com/Forums/en-US/crmdevelopment/thread/bd082f9d-1288-49ea-a8b4-e2b0beefd1d4. And came up with the solution above.
Basically, it's implemented as this:
  1. First, get the document that holds the crmGrid;
  2. Then, access the divGridProps collection within it;
  3. Change the value for the "sortColumns" property to "<field name 1>:<sort type>[; <field name x>:<sort type>]" where: <field name> is the name of the sorted column <sort type> is 1 (descending) or 0 (ascending)
  4. Repeat the bracketed block as necessary, being sure to place a semicolon (;) between each field/sort pair.
There must be NO SPACES. Finally, call a Refresh() on the grid. The downside to this code, is that it does not update the sorting icon on the columns. There's a call being made somewhere, and I think it has to do with analyzing the keypress+click event, that updates the image... whereas my code does not follow that code path.

UPDATE:  You may have noticed that the code above references a mechanism for changing the sorting of the currently viewed CRM Grid from the main CRM window, but makes no mention of where to put the code.  This is because I have not researched, or located, a way to load custom Javascript into the main CRM window.  This code is proof-of-concept.

Wednesday, September 30, 2009

The Contract Calendar (aka "Effectivity Calendar")

My company has a report template I created that generates an executable agreement document based on Contract records in CRM. It does a lot of pretty formatting, but one thing I never had with it is a proper rendering of the contract's calendar (you know, that button on top of the contract).

This is mostly because the data for that calendar is saved in a single column called "effectivitycalendar" as a 168-character string of minus and plus signs. Not very helpful, by itself.

So, I initially took a shortcut, and output dates/times to the report based on the contract's service level. This worked fine, until recently I received a request to add another range of effective times to the contract (and by extension, the report). Without going into too much detail, it would have been ugly to rewrite portions of my contract customizations and report template. So, I banged out the following VBA code and placed it into the report:

Function ConvertEffectivityCalendar(ByVal effectivityString As String) As String 
  Dim dailyCodeArray() As String = New String() {Mid(effectivityString, 1, 24), Mid(effectivityString, 25, 24), Mid(effectivityString, 49, 24), Mid(effectivityString, 73, 24), Mid(effectivityString, 97, 24), Mid(effectivityString, 121, 24), Mid(effectivityString, 145, 24)}
  Dim dailyCode As String 
  Dim currentDay As Integer 
  Dim results As String 
  Dim startTime As Integer 
  Dim endTime As Integer 
  Dim nextStartTime As Integer 
  Dim nextEndTime As Integer 
  Dim lastStartTime As Integer 
  Dim lastEndTime As Integer 
  Dim today As String 
  
  For currentDay = 0 To 6 
    dailyCode = dailyCodeArray(currentDay)
    startTime = InStr(dailyCode, "+")
    endTime = InStrRev(dailyCode, "+")
    nextStartTime = 0 
    nextEndTime = 0 
    lastStartTime = 0 
    lastEndTime = 0 
    
    If currentDay < 6 Then 
      nextStartTime = InStr(dailyCodeArray(currentDay + 1), "+")
      nextEndTime = InStrRev(dailyCodeArray(currentDay + 1), "+")
    End If 
    
    If currentDay > 0 Then 
      lastStartTime = InStr(dailyCodeArray(currentDay - 1), "+")
      lastEndTime = InStrRev(dailyCodeArray(currentDay - 1), "+")
    End If 
    
    Select Case currentDay 
      Case 0 
        today = "Sunday" 
      Case 1 
        today = "Monday"
      Case 2 
        today = "Tuesday" 
      Case 3
        today = "Wednesday"
      Case 4 
        today = "Thursday" 
      Case 5 
        today = "Friday" 
      Case 6 today = "Saturday" 
    End Select
    
    If startTime <> 0 And endTime <> 0 Then 
      If startTime <> lastStartTime Or endTime <> lastEndTime Then 
        If Len(results) > 1 Then 
          results = results + Chr(10) 
        End If 
        
        results = results + today 
      Else If startTime <> nextStartTime Or endTime <> nextEndTime Then
        results = results + " - " + today
      End If 
      
      If startTime <> nextStartTime Or endTime <> nextEndTime Then 
        results = results + Chr(10) 
        startTime = startTime - 1 
        
        If startTime <> 0 And endTime <> 23 Then 
          If startTime > 12 Then 
            results = results + CStr(startTime - 12) + ":00 pm" 
          Else If startTime = 0 Then 
            results = results + "12:00 am" 
          Else 
            results = results + CStr(startTime) + ":00 am" 
          End If 
        
          results = results + " to "
          endTime = endTime - 1 
          
          If endTime > 12 Then 
            results = results + CStr(endTime - 12) + ":00 pm"
          Else If endTime = 0 Then 
            results = results + "12:00 am" 
          Else 
            results = results + CStr(endTime) + ":00 am" 
          End If 
        Else 
          results = results + "All Day"
        End If 
      End If 
    End If 
  Next 
  
  Return results 
End Function

What it returns is a string, specially formatted for the particulars of my report. The nice thing about it, is that it summarizes the data. If the times blocked out on the calendar are identical for consecutive days, it prints the range of dates in the format "[Start Date] - [End Date]" and places the effective times underneath it in the format "[Start Time] to [End Time]". If there is variance in the effective times on following dates, it prints the unique start and end times for each date.

This may not be the most efficient, or portable method of converting the "effectivitycalendar" data into readable form, but it works nicely for my implementation and allows for a great deal more flexibility than I had. It should be noted that this code assumes all time points between the start and the end for any given day are filled in. This code would need heavy modification to accommodate several time-ranges for a single day.

Wednesday, June 3, 2009

The Poor Man's Advanced Find To Exclude Matching Related Records

So, I had a novel approach to a problem regarding the limitations of the Advanced Find function in CRM. When you are searching for a record that does not have a relationship to a some other record, there is hardly a workable set of query parameters you can use to accomplish the search.

For example:

Record Type A is the parent of Record Type B. Data: Record Type A instance Alpha is related to Record Type B instances Beta and Gamma. Record Beta has the name Q. Record Gamma has the name Y.

Exercise: Find all records of Type A that do not have a child Type B with the name Q.

The best that Advanced Find can do is find all records of Type A that have children of Type B without the name Q. This will always return Record Alpha, since Alpha does have another Type B record without the name Q.

To solve this limitation of the Advanced Find, I used a Workflow to augment it. Here's my solution to the problem above:

Workflow: "Find Type B with Q"

If (Related Entity)Type B's name field equals Q, then:

Stop Workflow with status of "Succeeded"

Otherwise:

Stop Workflow with status of "Canceled"

Then, run the Workflow "Find Type B with Q" on every Type A record you care to test against. This is probably the most cumbersome part--especially with a significant amount of Type A records to work with.

Advanced Find: Look for: Type A

(Related) System Job conditions:

System Job Name: "Find Type B with Q"

Status Reason: Canceled

Voila! I now have an Advanced Find that will exclude any Type A record with a related Type B record that has the name Q. It isn't very pretty, and if I had to perform this kind of search on a regular basis, I would have to limit the Advanced Find to making sure the System Job had only been recently run--making sure to run the Workflow on every Type A record first. I'd love to see a more native solution to this problem, but what I like about this solution is how fast it can be put together, and that I do not have to perform the query outside of CRM. It was this mechanism that allowed me to run yet another Workflow on all the Type A records to establish the missing Type B record with the name Q.

Tuesday, April 21, 2009

Making a manual Workflow understand who ran it.

So, I ran into a situation that I've seen a few people butt up against: Workflows have no register that connects to the executing user. By and large, all automatic workflows execute within the context of the User that established the Workflow. However, for those "on demand" workflows, the context changes to the user who has selected to run the workflow. This is awesome because there is an accounting of who has performed a certain function. What's unawesome about this, is that the Workflow's internal workings can't access this data natively. Huge bummer.

So, I short-circuited it with a very simple custom Workflow activity that returns a systemuser reference of whomever executed the workflow:

using System; 
using System.Collections; 
using System.Workflow.Activities; 
using System.Workflow.ComponentModel; 
using Microsoft.Crm.Sdk; 
using Microsoft.Crm.Sdk.Query; 
using Microsoft.Crm.SdkTypeProxy; 
using Microsoft.Crm.Workflow; 

namespace CrmWorkflows
{ 
  /// <summary> 
  /// Defines the workflow action, and places it in a container directory 
  /// </summary> 
  [CrmWorkflowActivity("Who Am I", "General Utilities")] 
  
  public class WhoAmI : Activity 
  {   
    #region Define Output systemUserLookup 
    
    /// <summary> 
    /// Workflow dependency property for systemUserLookup 
    /// </summary> 
    public static DependencyProperty systemUserLookupProperty = DependencyProperty.Register("systemUserLookup", typeof(Lookup), typeof(WhoAmI)); 
    
    /// <summary> 
    /// CRM Output definition for systemUserLookup 
    /// </summary> 
    [CrmOutput("User")] [CrmReferenceTarget("systemuser")] 
    public Lookup systemUserLookup 
    { 
      get 
      { 
        return (Lookup)base.GetValue(systemUserLookupProperty); 
      } 
    
      set 
      { 
        base.SetValue(systemUserLookupProperty, value); 
      } 
    } 
    
    #endregion 
    
    #region Activity Members 
    
    /// <summary> 
    /// Overridden Execute() function to provide functionality to the workflow. 
    /// </summary> 
    /// <param name="executionContext">Execution context of the Workflow</param> 
    /// <returns></returns> 
    protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) 
    { 
      IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService)); 
      IWorkflowContext context = contextService.Context; 
      
      systemUserLookup = new Lookup(EntityName.systemuser.ToString(), context.UserId); 
      
      return ActivityExecutionStatus.Closed; 
    } 
    
    #endregion 
  } 
} 

Tuesday, April 7, 2009

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

[UPDATE: See version 6 of the code in this post at http://crmentropy.blogspot.com/2009/11/embedding-advanced-find-views-in-entity.html]

Well, it's been interesting seeing my code deployed on the CRM system here at work. I've run into the following bug that has been corrected in this newest version:

  1. The Run Workflow button on the embedded view would not function properly, because of the modal dialog function hack; corrected by adjusting the modal dialog override to return a value, and refrain from refreshing the grid.

Version 5:

/// 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); 
      
      if (newButton != null) { 
       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) { 
      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); 
    } 
  } 
  
  // 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); 
}

Wednesday, April 1, 2009

Using activityparty in a partylist for a DynamicEntity

[Full Disclosure: The inspiration for the following code comes from YuvaKumar's post on the matter.]

So, I added custom attributes to my Service Activity entity in CRM 4.0 recently, and then found myself tasked with establishing a Workflow that used them. Since I have a very strong habit of writing my Plug-ins and Workflows for CRM using DynamicEntity for any reference to my custom fields--hell, it's practically the only reference I use--I was a little confused about how to approach the partylist attributes.

That's when I found the post linked above. Now I have a solution to my problem, and I'd like to share how you can use it for yourself in different coding situations:

Long story short, the target record is setup as a Lookup reference of the activityparty entity--established as a DynamicEntity object--which itself is nested in a DynamicEntityArrayProperty of the activity entity.

That's a mouthful. Here's a practical application: Let's say I have the GUID of an Account record in the variable customerId, and I want to establish this record into the customers property of a Service Activity I'm building in a DynamicEntity variable called serviceActivity.

First, I establish a DynamicEntity of the activityparty type:

DynamicEntity activityParty = new DynamicEntity("activityparty"); 

Then, I create a new LookupProperty for the partyid attribute of our new activityparty entity, and set it to our customerId variable:

LookupProperty partyProperty = new LookupProperty("partyid", new Lookup(EntityName.account.ToString(), customerId));
activityParty.Properties.Add(partyProperty);

Finally, I create a new DynamicEntityArrayProperty for the customers attribute of my Service Activity, and load our activityParty into it as a DynamicEntity array:

DynamicEntityArrayProperty customersProperty = new DynamicEntityArrayProperty("customers", new DynamicEntity[] { activityParty }); 
serviceActivity.Properties.Add(customersProperty); 

That's it! But that's just the basics. Obviously, the implications here are that for establishing many activityparty references, you need to load them as an array of DynamicEntity objects into a DynamicEntityArrayProperty for your Activity record. And, these DynamicEntity objects aren't typed with the record you're referencing, but actually typed as an activityparty instance with Lookups to those records.

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.

Friday, March 13, 2009

Microsoft CRM: Look Up Site Address button

One of the simpler hacks I've ever accomplished in CRM, is the ability to provide a button on an Entity form that allows you to populate address fields on that same Entity with the addresses from Sites. Most of this functionality comes from the Look Up Customer Address code by Michael Höhne over at Stunnware--which, if you haven't guessed by now, is like my CRM idol.

Here's the code:

var url = '/' + ORG_UNIQUE_NAME + '/_controls/lookup/lookupsingle.aspx?class=Site&objecttypes=4009&browse=0&bindingcolumns=address1_line1%2caddress1_line2%2caddress1_line3%2caddress1_city%2caddress1_stateorprovince%2caddress1_postalcode%2caddress1_country&ShowNewButton=0&ShowPropButton=1';

var selectedAddress = window.showModalDialog(url, null, 'dialogWidth:400px;dialogHeight:400px;resizable:yes'); 

if (selectedAddress != null) { 
  var addressFields = selectedAddress.items[0].values; 
  
  for (var index in addressFields) { 
    if (addressFields[index] && addressFields[index].name) { 
      var control = document.getElementById('<Prefix Value>' + addressFields[index].name.replace(/address1_/,'')); 
      
      if (control) { 
        control.DataValue = addressFields[index].value; 
      } 
    } 
  } 
}

This code works best when all of the address fields on your form use a prefix on the name of the fields originating in the Site entity. For example, the target custom_addressline1 field from a custom entity matches the field address1_line1 on the Site entity, making "custom_address" the value for <Prefix Value>. Otherwise, you'll end up mapping each of the fields in the script.

I take the liberty of stripping the "address1_" portion of each source field name from the Site. As you can see, there's an interesting feature of lookupsingle.aspx: the variable bindingcolumns can be used to set a series of invisible fields to return in the values array. I take the above code, and drop it into the ISV.config XML file under a button element for the entity's menu, and voila! The button now generates a nice, little lookup window populated with all my Sites. Selecting one automatically populates address fields on my form. It's possible that you could extend this functionality to System User records as well, and many other record types. I just haven't done so myself.

Thursday, March 12, 2009

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

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

So, I ran into a little problem using the New button override on an embedded AF view on a Contract Line entity. Apparently top.locAddObjTo() does not function properly on this particular record type. So, I modified the code to use the more appropriate locAddRelatedToNonForm() function. This function takes one more parameter, which I haven't identified, but apparently it's not required or is often set to a blank string. Best of all, this function works from the Iframe, and is the function used by the ordinary Associated View for custom entities. As of yet, I've only tested it with embedded AFs that display custom entities, but it appears to work from any "parent" entity form.

Version 3:

/// 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 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"; 
      
      // 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 + "\",\"\");'");
      } 
    } 
  } 
  
  // Set it, and forget it! 
  httpObj.send(params); 
}

Tuesday, March 10, 2009

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

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

So, I discovered a way to hack the functionality of the New button on the Advanced Find grid to establish new records that are related to the record housing the view. So without further ado, I give to you, the updated version of my EmbedAdvancedFindView() function.

Version 2:

/// 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 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"; 
      
      // 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 = 'top.locAddObjTo(" + entityTypeId + ", " + crmForm.ObjectTypeCode + ", \"" + crmForm.ObjectId + "\");'"); 
      } 
    } 
  } 
  
  // Set it, and forget it! 
  httpObj.send(params); 
}

You'll notice that there's an additional parameter over the last version. entityTypeId should set as a string representation of the entity type code assigned to the record described by entityName. The code then overwrites the functionality of the New button with a call to top.locAddObjTo(). This function, I've found, takes 3 parameters in the following order: the entity type of the target record, the entity type of the related record, and the GUID of the related record. The caveat to this hacking is that upon completion of the new record, the grid does not automatically refresh to display the new record.

This may be a limitation to using the code for your purposes, so take it with a grain of salt. As for correcting this particular annoyance, there seems to be little option. As hard as I try, I cannot seem to reference the parent window, or grid, from the new record. All the normal javascript pointers (parent, top, opener) don't reference it. This doesn't mean a reference doesn't exist--I just haven't found it. The reverse seems also true: I can't grab a handler to the spawned window from any object/variable within the parent.

In light of this, I had toyed around with the idea of establishing a dual functionality for the New button, so that it would open a new record window and also start an invisible polling process to check the number of records for the view by using SOAP calls to the WSDL API. If the total number of records changed during a particular poll, then this invisible process would call crmGrid.Refresh().

I'm all for hacking, but to me that seems a bit overzealous and ugly. Although it may work, I'm not convinced it's the best approach. So, for the moment I'm leaving the annoyance in and training people to manually refresh the grid after they've established a new, related record. If anyone reading this blog has some insight on how I might better accommodate the automatic grid refresh, please leave a comment.

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.