Pages

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.