Sample Code


The Microsoft CRM SDK includes great code samples and examples that you can reference when you develop your own solution. You should definitely use them as a resource, but we wanted to include additional code samples in this book that address common requests. We will demonstrate how to:

  • Retrieve a user's assigned roles

  • Create an auto number field

  • Validate a field when converting an Opportunity

  • Add data auditing

  • Create a Project record from converted Opportunities

Retrieving a User's Assigned Roles

The Microsoft CRM security model allows you to assign one or more security roles to each user. The native classes help you retrieve a user's actual privileges associated with the combination of the user's roles, but you might find that you need to retrieve just the roles for custom security or business logic. For example, your code might take some action if the user has a Salesperson role, but not if he or she has a Customer Service role.

Listing 9-4 shows the code for the QueryExpression class to retrieve the assigned roles of the calling user. Note that this query uses an extra join to retrieve the roles for the user.

Listing 9-4: Retrieving Assigned Roles

image from book
 // Standard CRM Service Setup CrmService service = new CrmService(); service.Credentials = System.Net.CredentialCache.DefaultCredentials; service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx"; // Retrieve user information. WhoAmIRequest userRequest = new WhoAmIRequest(); WhoAmIResponse user = (WhoAmIResponse) service.Execute(userRequest); // We will create two link entities. // One will be between role and systemuserroles entities. LinkEntity roleXuserroles = new LinkEntity(); roleXuserroles.LinkFromEntityName = EntityName.role.ToString(); roleXuserroles.LinkFromAttributeName = "roleid"; roleXuserroles.LinkToEntityName = "systemuserroles"; roleXuserroles.LinkToAttributeName = "roleid"; // The second will be between the systemuser and systemuserroles entities. LinkEntity userXuserroles = new LinkEntity(); userXuserroles.LinkFromEntityName = "systemuserroles"; userXuserroles.LinkFromAttributeName = "systemuserid"; userXuserroles.LinkToEntityName = EntityName.systemuser.ToString(); userXuserroles.LinkToAttributeName = "systemuserid"; // Create a condition filtering the systemuserid from the systemuserroles entity. ConditionExpression condition = new ConditionExpression(); condition.AttributeName = "systemuserid"; condition.Operator = ConditionOperator.Equal; condition.Values = new object[]{user.UserId}; // Add the filter. userXuserroles.LinkCriteria = new FilterExpression(); userXuserroles.LinkCriteria.Conditions = new ConditionExpression[] {condition}; // Connect the user link to the role link. roleXuserroles.LinkEntities = new LinkEntity[] {userXuserroles}; // Build a query expression. QueryExpression query = new QueryExpression(); query.EntityName = EntityName.role.ToString(); query.ColumnSet = new AllColumns(); // Add the join back to the role query. query.LinkEntities = new LinkEntity[] {roleXuserroles}; try {  // Retrieve the values from Microsoft CRM.  BusinessEntityCollection retrieved = service.RetrieveMultiple(query); } catch (System.Web.Services.Protocols.SoapException ex) {  // Handle error. } 
image from book

Creating an Auto Number Field

Microsoft CRM uses a GUID to uniquely identify each record in the database, and Microsoft CRM also gives you the ability to add custom attributes. However, Microsoft CRM does not provide a way to create an automatically incrementing field (typically referred to as an Identity field in SQL Server). Although some objects (such as Cases, Invoices, and Quotes) include a numbering scheme that neatly identifies a unique record to the user, entities such as Lead, Account, and Contact do not include a numbering method. If your users want to reference records by a simple integer number instead of the unfriendly looking GUID, you can create custom code that manages a numbering scheme for the entities that you want to uniquely identify with a number.

This example code will simulate the SQL Server Identity concept to give our end users a Lead entity numbering scheme using the pre-callout. We will first create a new integer attribute on the Lead form. Then we will develop a query to get the maximum Lead number and use that method within a pre-callout routine to set the value of the Lead number before saving a new Lead record to the database.

Real World 

This example demonstrates the power and usefulness of impersonation. Since we want the lead number to be unique across all Leads, it's critical that we execute our QueryExpression method under the context of a user who has read access to all lead records.

Configure the Lead Form
  1. Add a new integer attribute to the Lead form called new_leadnumber.

  2. Add this field to the form, and make sure that it is disabled to the user because we will be populating the value automatically.

Building a Callout Assembly Project
  1. Create a new C# Class Library project in Visual Studio .NET 2003 called Working-WithCrm.Callout.

  2. Be sure check for the System.Web.Services reference. If it doesn't exist, add a reference to the System. Web.Service namespace.

  3. Add a Web Reference to the CrmService Web service, calling it CrmSdk.

  4. Add a reference to the Microsoft.Crm.Platform.Callout.Base.dll.

  5. Add a new Class file called LeadCallout.

  6. Enter the code shown in Listing 9-5.

    Listing 9-5: Creating an Auto Number Field

    image from book
     using System; using System.Diagnostics; using System.IO; using System.Xml; using WorkingWithCrm.Callout.CrmSdk; using Microsoft.Crm.Callout; namespace WorkingWithCrm.Callout {  /// <summary>  /// This Sample shows how to log various events with callouts  /// </summary>  public class LeadCallout: CrmCalloutBase  {   public LeadCallout()   {   }   // This Sample shows how to log an object creation using a precallout   public override PreCalloutReturnValue PreCreate(CalloutUserContext userContext, CalloutEnt ityContext entityContext, ref string entityXml, ref string errorMessage)   {    // Call the helper function NextLeadNumber() to return the next highest lead number value    string nextLeadNumber = NextLeadNumber();    // Create an xml document in order to work with the xml stream    XmlDocument entityDoc = new XmlDocument();    entityDoc.LoadXml(entityXml);    // Create the appropriate xml node    XmlNodeList propertyList = entityDoc.GetElementsByTagName("Property");    XmlElement leadNumberValue = null;    XmlElement properties = (XmlElement) entityDoc.GetElementsByTagName("Properties")[0];    XmlElement leadNumberElement = entityDoc.CreateElement("Property");    XmlAttribute typeAttrib = entityDoc.CreateAttribute("type");    // Set the values for our new_leadnumber field    leadNumberElement.SetAttribute("type", "http://www.w3.org/2001/XMLSchema- instance", "StringProperty");    leadNumberElement.SetAttribute("Name", "new_leadnumber");    leadNumberValue = entityDoc.CreateElement("Value");    leadNumberValue.InnerText = NextLeadNumber();    leadNumberElement.AppendChild(leadNumberValue);    properties.AppendChild(leadNumberElement);    // Add back to the entityXml    StringWriter output = new StringWriter();    entityDoc.Save(output);    entityXml = output.ToString();    // Remove the extra XML that will confuse CRM    entityXml = entityXml.Replace("xmlns=\"\"", "");    entityXml = entityXml.Replace("<?xml version=\"1.0\" encoding=\"utf-16\"?>", "");    return PreCalloutReturnValue.Continue;   }   #region Helpers   public bool IsTextIncluded(string InputString, string ValueToCheck)   {    return (InputString.IndexOf(ValueToCheck) > -1);   }   public string NextLeadNumber()   {    // Standard CRM Service Setup    CrmService service = new CrmService();    service.Credentials = System.Net.CredentialCache.DefaultCredentials; service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx";    // We need a user that has global read access to the lead record so that we    // have the absolute maximum lead number. If all roles have global read privileges to    // the read value of the Lead, then this wouldn't be necessary.    // For production, access this guid in a config file.    Guid callerid = new Guid("");    // Impersonate our global read user    service.CallerIdValue = new CallerId();    service.CallerIdValue.CallerGuid = callerid;    // Create a set of columns to return    ColumnSet cols = new ColumnSet();    cols.Attributes = new string [] {"leadid", "new_leadnumber"};    // Set the order of the bring back the highest new_leadnumber value at the top    OrderExpression order = new OrderExpression();    order.AttributeName = "new_leadnumber";    order.OrderType = OrderType.Descending;    // To improve performance, we will only pass back the top record    // This will return only 1 page with 1 record per page    PagingInfo pages = new PagingInfo();    pages.PageNumber = 1;    pages.Count = 1;    // Create a query expression and set the query parameters    QueryExpression query = new QueryExpression();    query.EntityName = EntityName.lead.ToString();    query.ColumnSet = cols;    query.Orders = new OrderExpression[] {order};    query.PageInfo = pages;    // Retrieve the values from CRM    BusinessEntityCollection retrieved = service.RetrieveMultiple(query);    string nextNumber = "1";    // Check to see if we have any records    if (retrieved.BusinessEntities.Length > 0)    {     // Cast to results lead object and only retrieve first record     lead results = (lead)retrieved.BusinessEntities[0];     // Return the next value lead number. If there are records, but none have a number (the result will be null), so just pass back 1     nextNumber = (results.new_leadnumber != null) ? (results.new_leadnumber.Value + 1).ToStr ing() : "1";    }    return nextNumber;   }   #endregion  } } 
    image from book

After you build your assembly, you must update the Callout.config.xml file to include your new method. Replace the assembly and class names with the ones from your project. Note that we do not need any values from Microsoft CRM for this routine to function, so you don't need to specify a <prevalue>. The Callout.config.xml file code is shown below:

 <?xml version="1.0" encoding="utf-8" ?> <callout.config version="2.0">  <callout entity="lead" event="PreCreate">   <subscription assembly="WorkingWithCrm.Callout.dll" ></subscription>  </callout> </callout.config> 

Next you will deploy the Callout.config.xml file and your .dll file to <crm install drive>\Program Files\Microsoft CRM\Server\bin\assembly. And don't forget to do an iisreset on the server after deploying the files.

Validating a Field When Converting an Opportunity Record

Let's imagine that as part of your sales process, you want every sales person to enter the start date of a project (a custom Opportunity attribute) before the sales person can close an Opportunity as won. However, you don't want to make the project state date field required on the Opportunity form because that would force the sales person to enter a value before he or she could save a record. In this example the user won't know the project start date until the customer agrees to the purchase, so it doesn't make sense to configure this as a required field. However, your business requirement dictates they must enter this project start date field before they can close the Opportunity as won. This example will walk through all the steps necessary to develop a customization that will perform this type of check.

At first, you might think that the pre-callout would be the obvious choice for implementing this solution because we do not recommend using the pre-callout for data validation when users create or modify records. Although the pre-callout allows you to pass back an error message to the client, the client will lose any data that the user entered. So, in this example, if the user updated 10 fields on the Opportunity and then saved it, the pre-callout could check to make sure the user completed it according to your business rules. However, if there is a problem, the user will see an error and lose all of the data from the 10 fields that he or she just updated. Obviously, this would make for some unhappy users, so we won't go down this path. For regular form validation, we recommend using client-side methods, as demonstrated in Chapter 10, instead of the pre-callout.

However, this particular example is a unique case in which we need to use the pre-callout for validation. In our example, we want to perform a validation when the user interacts with a Web dialog page. As you will learn in Chapter 10, Microsoft CRM does not include client-side events such as Converting Leads or Closing Opportunities on Web dialog pages. Fortunately, the pre-callout provides the appropriate hook for us to perform this type of Web dialog page validation. If the user tries to close an Opportunity without entering a project start date, we will return a simple error message (shown in Figure 9-15) that instructs him or her to correct it.

image from book
Figure 9-15: Custom callout error returned to the user

Configure the Opportunity Form
  1. Add a new datetime attribute to the Opportunity form called new_projectstartdate

  2. Leave the default value as Unassigned.

  3. Add this field to the form and select the Lock field on the form check box.

  4. Publish the Opportunity entity.

Caution 

When you deploy the pre-callout assembly in this example, the user must enter a value in this field. If someone accidentally removes this field from the form, no one will be able to close an Opportunity. We recommend that you lock any fields that you reference in a custom event, callout, or workflow assembly so that other users who might edit the form know to leave that field on the form.

Next we must create the pre-callout code. The pre-callout offers us six events to choose from (PreCreate, PreUpdate, PreDelete, PreAssign, PreSetState, and PreMerge), but we need to know which one is appropriate for our solution. We know that we need to check the value of the new_projectstartdate field. We also need to have access to the statecode because we don't want to validate unless the user is closing the Opportunity record. Let's brainstorm some options.

Microsoft CRM handles closing an Opportunity record differently than most other entities. When an Opportunity record is closed (marked as inactive), a special activity record is created and logged with the Opportunity record. Due to this, we could attempt to configure a pre-callout method against the OpportunityClose activity. Upon inspection of the sdkreadme.htm, we find a known issue detailing that callout events are not fired for the OpportunityClose activity records when closed through the application.

Since the OpportunityClose activity callout event does not work in this instance, we have to accomplish our task with one of the Opportunity events. Your next inclination might be to use the PreSetState event for the opportunity, checking for the statecode and new_projectstartdate field. Unfortunately, the entity XML is not passed in to the PreSetState event; see the method signature in the following code.

 public override PreCalloutReturnValue PreSetState(CalloutUserContext userContext,  CalloutEntityContext entityContext, ref int newStateCode, ref int newStatusCode,  ref string errorMessage) 

What about using the opportunity PreUpdate event and doing the statecode and new_projectstartdate field checks? We did a quick test to output a sample entity XML using the PreUpdate event after we closed an Opportunity so that we could review the information available to us.

 PreUpdate - 12/9/2005 12:05:11 PM ObjectType: 3 ObjectId:  CreatorId:  entityXml: <BusinessEntity xsi:type="DynamicEntity" Name="opportunity" xmlns:xsi=   "http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/crm/2006/   WebServices"><Properties><Property xsi:type="CrmDateTimeProperty"   Name="new_projectstartdate"> <Value>2005-12-09T00:00:00</Value></Property></Properties></BusinessEntity> 

As you can see, the entityXml line doesn't include the state and status reasons as properties, which prevents us from easily checking the statecode value.

Obviously, the remaining pre-callout events, PreCreate, PreDelete, PreAssign, and PreMerge, don't apply to our scenario.

Because it doesn't appear that any of the native options will get us exactly what we want in one step, we will have to make an extra SDK call to get the information that we need. We decided to go back and use the PreSetState method and add an additional call to retrieve the value of the project start date field. The PreSetState method fires only when a statecode change is made, which occurs when you close an Opportunity.

We will look at the statecode and determine whether it is being marked as Won. If it is being changed to any other state, we will not validate that the Project Start Date field contains data. After we know that the user is closing the Opportunity and marking it as Won, we will make a separate call to retrieve the saved value of the project start date.

Using our WorkingWithCrm.Workflow callout project, add a new class file called Validate-Opportunity. Add the pre-callout code shown in Listing 9-6.

Listing 9-6: Using a Pre-Callout to Validate a Form Field

image from book
 using System; using System.Xml; using WorkingWithCrm.Callout.CrmSdk; using Microsoft.Crm.Callout; namespace WorkingWithCrm.Callout {  /// <summary>  /// This callout class will log all data changes for the entities specified.  /// </summary>  public class ValidateOpportunity : CrmCalloutBase  {   public ValidateOpportunity()   {   }   public override PreCalloutReturnValue PreSetState(CalloutUserContext userContext,  CalloutEntityContext entityContext, ref int newStateCode, ref int newStatusCode,  ref string errorMessage)   {    // Standard CRM Service Setup    CrmService service = new CrmService();    service.Credentials = System.Net.CredentialCache.DefaultCredentials;    service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx";    // Create the Column Set Object indicating the fields to be retrieved.    ColumnSet cols = new ColumnSet();    cols.Attributes = new string [] {"new_projectstartdate"};    // Retrieve the Opportunity record.    opportunity opp = (opportunity)service.Retrieve(EntityName.opportunity.ToString(),  entityContext.InstanceId, cols);    bool isValid = true;    // Only check this if the new state code is won (1).    // Otherwise, we would be validating when a user reopened an Opportunity, and there might be existing data that would always force this error.    if (newStateCode == 1)    {     // If the new_projectstartdate field is null, then no data exists for it.     if (opp.new_projectstartdate == null)      isValid = false;    }    if (isValid)    {     return PreCalloutReturnValue.Continue;    }    else    {     // Set the error message and abort the transaction.     errorMessage = "Please select a Project Start Date before closing this opportunity.";     return PreCalloutReturnValue.Abort;    }   }  } } 
image from book

Next we update the Callout.config.xml file to register this new routine and deploy our assemblies. After an iisreset, you now have custom validation when closing an Opportunity record!

 <?xml version="1.0" encoding="utf-8" ?> <callout.config version="2.0">  <callout entity="opportunity" event="PreSetState">   <subscription assembly="WorkingWithCrm.Callout.dll" >   </subscription>  </callout> </callout.config> 

Note 

Are you having problems getting your callout to work? Your first troubleshooting step should be to double-check the configuration file. Callout errors are typically caused by incorrectly setting up the configuration file or forgetting to include the entity that you want to register. Be sure to check the Microsoft CRM Web server Event Viewer for any errors that Microsoft CRM might log. You can also add your own trace logging for further analysis.

Data Auditing

Microsoft CRM automatically records the date, the time, and the user who last modified a record. However, it does not record the specific values that the user changed in the record. This lack of detailed data auditing might cause some concern for your management and executives in today's intense Sarbanes-Oxley world. Fortunately, you can save the day for management by using the SDK and this code sample to add a data auditing feature that uses the post-update callout method and custom entities.

We mentioned earlier that you must explicitly tell Microsoft CRM which fields you want passed in to your routine in the Callout.config.xml file. Therefore, you can choose to audit a specific set of fields or you can audit changes to all fields by using the @all keyword. Because Microsoft CRM passes the before and after data on any changes to the post-update callout method, you can leverage this to add your own custom data logging.

Before we begin coding, we must first create a custom entity called Data Audit that will store our auditing information. We will use this custom entity to record the field level changes. For this example we will audit changes to the Contact entity, so we'll add a one-to-many relationship from the Contact entity to the Data Audit entity. Our final result will resemble Figure 9-16.

image from book
Figure 9-16: Data Audit grid

If a user double-clicked one of the Data Audit records in this grid, Microsoft CRM will launch the form for the Data Audit entity. We decided to just remove all the fields from that form and display a message to the user by labeling a section with the following text: "This information is generated by the system."

Finally, as with all new custom entities, you must update your security roles to allow the appropriate access to our Data Audit entity. At a minimum, you will have to allow for Create and Read privileges for all users so that all of their changes are properly logged in our Data Audit entity. To maintain the integrity of the data audit records, you should not grant Delete privileges on the Data Audit entity to any role except the System Administrator role, which has access to all entities and privileges by default.

To summarize, we will do the following:

  1. Create a new entity called Data Audit, which includes a relationship to the Contact entity.

  2. Customize the Form, Preview, and Associated views of our new entity.

  3. Use the WorkingWithCrm.Callout project created in the "Creating an Auto Number Field" example and add a reference to the Microsoft.VisualBasic namespace.

  4. Add a new class file called DataAudit and enter the code in Listing 9-7.

    Listing 9-7: Updating the Audit Entity

    image from book
     using System; using System.Collections.Specialized; using System.IO; using System.Xml; using System.Xml.Xsl; using System.Xml.Serialization; using WorkingWithCrm.Callout.CrmSdk; using Microsoft.Crm.Callout; namespace WorkingWithCrm.Callout {   /// <summary>   /// This callout class will log all data changes for the entities specified.   /// </summary>   public class DataAudit : CrmCalloutBase   {     public DataAudit()     {     }     public override void PostUpdate(CalloutUserContext userContext, CalloutEntityContext  entityContext, string preImageEntityXml, string postImageEntityXml)    {       // Standard CRM Service Setup       CrmService service = new CrmService();       service.Credentials = System.Net.CredentialCache.DefaultCredentials;       service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx";       service.CallerIdValue = new CallerId();       service.CallerIdValue.CallerGuid = userContext.UserId;       // Only execute if the contact record is updated       if (entityContext.EntityTypeCode == 2)       {         // Deserialize entityxml looking for the values we need         if ((preImageEntityXml != null && preImageEntityXml.Length > 0) && (postImageEntity Xml != null && postImageEntityXml.Length > 0))         {         // Deserialize the pre and post data         DynamicEntity dePre = DeserializeXmltoDynamicEntity(preImageEntityXml);         DynamicEntity dePost = DeserializeXmltoDynamicEntity(postImageEntityXml);         NameValueCollection prop = new NameValueCollection();         AddPropertiesToCollection(prop,dePre,false);         AddPropertiesToCollection(prop,dePost,true);         foreach (string key in prop)         {         // Using the Visual Basic split method in order to split on a string, instead of a         character               string[] arr = Microsoft.VisualBasic.Strings.Split(prop[key],"|,|",-1,                 Microsoft.VisualBasic.CompareMethod.Text);         if (arr.Length > 1)         {               // If the values do not match, then insert a record into our audit table               if (arr[0] != arr[1]) InsertAuditRecord(service,entityContext.InstanceId,key,                 arr[0],arr[1]);         }         else         {               // If the array length is 1, then we have a case where a text value was changed to or from a blank value.               // Need to split on just the resulting string               string[] arrBlankText = prop[key].Split('|');               if (arrBlankText.Length > 1) InsertAuditRecord(service,entityContext.InstanceId,key,arrBlankText[0],arrBlankText[1]);               }            }       }     }   }     /// <summary>     /// This helper method will loop through the properties and add them to the passed in name/value collection.     /// If the DynamicEntity is pre data, it will append a pipe (|) character. If it is post data, then it will prepend     /// a pipe (|) character. This will allow us to easily split the resulting the collection when checking for any changed values.     /// </summary>     /// <param name="prop">Name/value collection to store the resulting values.</param>     /// <param name="EntityData">DynamicEntity of properties to loop through.</param>     /// <param name="PostData">Boolean to determine if the DynamicEntity contains pre or post Xml data.</param>     public void AddPropertiesToCollection(NameValueCollection prop, DynamicEntity EntityDat a, bool PostData)     {       Property currentProperty;       string attributeName;       string attributeValue;       string attributeModValue;       // Loop through entities       for (int i=0; i<EntityData.Properties.Length; i++)       {      // Reset the variables      currentProperty = EntityData.Properties[i];      attributeValue = string.Empty;      attributeModValue = string.Empty;      // Get the attribute name      attributeName = currentProperty.Name;      attributeValue = GetValueFromProperty(currentProperty);      // Add a pipe character after the value if we are looping through the pre data or      // before the value if we are looping through the post data.      attributeModValue = (PostData) ? "|" + attributeValue : attributeValue + "|";      // Skip these values, since they change on every update      if ((attributeName != "modifiedon") && (attributeName != "modifiedby"))      {       prop.Add(attributeName,attributeModValue);      }     }    }    /// <summary>    /// This function will return the value from a property based on its property type.    /// </summary>    /// <param name="InputProperty"></param>    /// <returns></returns>    public string GetValueFromProperty(Property InputProperty)    {      Type propType = InputProperty.GetType();      string propValue = string.Empty;      // Returning the values will depend on the attribute's property type      if (propType == typeof(CustomerProperty))      {        // Return the name, not the Guid        propValue = ((CustomerProperty)InputProperty).Value.name.ToString();      }      else if (propType == typeof(CrmBooleanProperty))      {       propValue = ((CrmBooleanProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(CrmDateTimeProperty))      {       propValue = ((CrmDateTimeProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(CrmDecimalProperty))      {       propValue = ((CrmDecimalProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(CrmFloatProperty))      {        propValue = ((CrmFloatProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(CrmMoneyProperty))      {        propValue = ((CrmMoneyProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(CrmNumberProperty))      {       propValue = ((CrmNumberProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(LookupProperty))      {        // Return the name, not the Guid if one exists        // Note that the owningbusinessunit lookup does not provide a name, so will skip it.        if (InputProperty.Name.ToString() != "owningbusinessunit")          propValue = ((LookupProperty)InputProperty).Value.name.ToString();      }      else if (propType == typeof(OwnerProperty))      {        // Return the name, not the Guid        propValue = ((OwnerProperty)InputProperty).Value.name.ToString();      }      else if (propType == typeof(PicklistProperty))      {        propValue = ((PicklistProperty)InputProperty).Value.Value.ToString();      }      else if (propType == typeof(StringProperty))      {        propValue = ((StringProperty)InputProperty).Value.ToString();      }      else if (propType == typeof(StatusProperty))      {        // Return the name, not the Guid        propValue = ((StatusProperty)InputProperty).Value.ToString();      }     return propValue;    }    /// <summary>    /// Inserts a new record in the audit table.    /// </summary>    /// <param name="service"></param>    /// <param name="ContactId"></param>    /// <param name="AttributeName"></param>    /// <param name="PreviousValue"></param>    /// <param name="NewValue"></param>    private void InsertAuditRecord( CrmService service, Guid ContactId, string AttributeName, string PreviousValue, string NewValue )    {      Lookup contactId = new Lookup();      contactId.Value = ContactId;      contactId.type = EntityName.contact.ToString();      new_dataaudit audit = new new_dataaudit();      audit.new_attribute = AttributeName;      audit.new_contactid = contactId;      audit.new_previousvalue = PreviousValue;      audit.new_newvalue = NewValue;      service.Create(audit);    }     /// <summary>     /// Translates a xml string from the callout into a DynamicEntity object.     /// </summary>     /// <param name="XmlString"></param>     /// <returns></returns>     public DynamicEntity DeserializeXmltoDynamicEntity (string XmlString)     {       TextReader sr = new StringReader(XmlString);       XmlRootAttribute root = new XmlRootAttribute("BusinessEntity");       root.Namespace = "http://schemas.microsoft.com/crm/2006/WebServices";       XmlSerializer xmlSerializer = new XmlSerializer(typeof(BusinessEntity), root);       BusinessEntity entity = (BusinessEntity)xmlSerializer.Deserialize(sr);       DynamicEntity myDE = entity as DynamicEntity;       return myDE;     }   } } 
    image from book

  5. Add an entry to the Callout.config.xml file to run the callout each time a user updates a Contact record.

  6. Deploy the callout assembly and configuration file to the Microsoft CRM Web server.

Creating and Customizing a Custom Data Audit Entity
  1. Create a new entity called Data Audit, as shown in the following graphic. Be sure to change the Ownership to Organization. Also, change the primary attribute's maximum length to 25 characters, change the requirement level to No Constraint, and clear the Notes and Activities check boxes.

    image from book

  2. Click Attributes, and add the custom attributes shown in Table 9-7. You do not need to add an attribute for the dates or for the person who made the change because you will be using the fields (createdby and createdon) that are automatically created with the entity.

  3. Click Relationships and add a many-to-one, referential relationship to the Contact entity.

    image from book

  4. Click Forms and Views, click Form, and then remove all of the fields. Click Add a Section, and then type This information is generated by the system. in the Name box. Select the check box to show the name on the form. Click Save and Close in the toolbar.

    image from book

  5. Double-click Preview, and then add the fields shown here.

    image from book

  6. Double-click Data Audit Associated View, and then change the columns and default sort to the following.

    image from book

  7. Publish this entity.

    Important 

    Don't forget to add the appropriate security settings for this new entity in your system. By default, only the System Administrator role has access to newly created custom entities.

Table 9-7: Audit Entity Attributes

Display name

Schema name

Type

Previous Value

New_PreviousValue

ntext (2,000)

New Value

New_NewValue

ntext (2,000)

Attribute

New_Attribute

Nvarchar (150)

We will use the PostUpdate callout method and pass in all the columns to handle our data auditing. The incoming data will be passed in as XML snippets, so we could then use XPath or even string techniques to analyze each property to determine whether an attribute was changed. However, we chose to deserialize the resulting input XML strings into two separate DynamicEntity classes to demonstrate that technique.

Note 

You will find a performance impact when using this the deserialize code. In high volume environments, you may find that an alternate another approach may provide performance.

This example brings to light an interesting situation in which Microsoft CRM passes data to the callout. We mentioned earlier that null values are not passed to the callout, even if you specify that a specific field should be passed in the callout configuration file. To help clarify what this means, consider this example: You enter "James" for a contact record that previously had no middle name, and you leave the gender field unchanged. When you save, the Post-Update callout fires. The following are two snippets of pre-and post-XML data passed to the PostUpdate callout method.

 PreImageXml Snippet: <BusinessEntity xsi:type="DynamicEntity" Name="contact" xmlns:xsi="http://www.w3.org/2001/ XMLSchema-instance" xmlns="http://schemas.microsoft.com/crm/2006/WebServices">  <Properties>    <Property xsi:type="PicklistProperty" Name="gendercode">     <Value>1</Value>    </Property>  </Properties> </BusinessEntity> PostImageXml Snippet: <BusinessEntity xsi:type="DynamicEntity" Name="contact" xmlns:xsi="http://www.w3.org/2001/ XMLSchema-instance" xmlns="http://schemas.microsoft.com/crm/2006/WebServices">  <Properties>   <Property xsi:type="StringProperty" Name="middlename">    <Value>James</Value>   </Property>   <Property xsi:type="PicklistProperty" Name="gendercode"> <Value>1</Value>   </Property>  </Properties> </BusinessEntity> 

As you can see, the PreImageXml data does not contain a node for middlename, because it was originally blank (null). So the number of properties for the pre-and post-DynamicEntity objects will vary depending on the passed-in data.

Therefore, we can't simply try to perform one loop through the submitted properties and compare the differences, because we could have nodes. We will instead loop through the pre- and post-DynamicEntity objects individually and add the attribute name and values to a NameValueCollection. We chose to use the NameValueCollection because it automatically concatenates the values for keys with the same name. However, on null fields, only one value will be added, so we will need to know whether it was pre- or post-data. For simplicity, we will append a pipe character (|) on the pre-data and prepend a pipe character on the post-data to delineate our data. We will then loop through the resulting NameValueCollection, split the value string, and compare the values from there. When we find that the new value doesn't match the previous one, we will call an insert method to update the audit entity for the contact record.

The code for our DataAudit class file is shown in Listing 9-7. Be sure to add the Microsoft.VisualBasic reference to your callout project file.

Finally, we update the Callout.config.xml file to register our DataAudit routine, passing in all (non-null) values.

 <?xml version="1.0" encoding="utf-8" ?> <callout.config version="2.0">  <callout entity="contact" event="PostUpdate">   <subscription assembly="WorkingWithCrm.Callout.dll" >    <prevalue>@all</prevalue>    <postvalue>@all</postvalue>  </subscription>  </callout> </callout.config> 

This data audit example shows how to pull together the various Microsoft CRM customization options to solve a real business need. Our example works for Contact records only, but of course you could modify and extend this example to perform a similar data audit on other entities such as Lead, Opportunity, Account, and Case.

Creating a Project Record from Converted Opportunities

When you convert a Lead, you can create new Account, Contact, and Opportunity records with values mapping from the originating lead. Likewise, you can convert an Opportunity by marking it as Won or Lost, but are not able to natively create new entity records. We've had several customers who need to automatically create new records triggered from these convert actions. For this example, we will create a new "Project" record (a custom entity) upon each successfully converted Opportunity. To accomplish this, we will do the following:

  1. Create a new entity called Project.

  2. Customize the Opportunity entity to include a field called Project Name.

  3. Create a custom workflow assembly that will create a new Project entity based on the information from the Opportunity closing it.

  4. Create a new workflow rule that uses this custom assembly.

Creating the Project Entity
  1. Create a new entity called Project, and leave the default values for the primary attribute.

  2. Create a many-to-one, referential relationship to the Account entity as shown.

    image from book

  3. Create another many-to-one, referential relationship to the Opportunity entity. This will allow us to track which Opportunity created the Project record.

  4. Add the Account and Opportunity lookup fields to the Project form.

    image from book

  5. Save and publish the Project entity.

  6. Update the security roles to provide access to this new entity.

Next we will develop a routine that creates an account based on some passed-in parameters. For this example, we want to have all of our projects relate to Account records. However, the Opportunity allows for either an Account or a Contact for its customer reference. We decided to handle this by creating two methods. The main method, CreateProjectRecord, will take in the project name, an account ID, and an opportunity ID. This method will be used when the Opportunity customer is an account. We then create a second method called CreateProjectRecordFromContact, which will take in a project name, a contact ID, and an opportunity ID when our customer is a contact. Then we will retrieve the parent account for the Contact record and pass that as our account ID. If no parent account is found, we will simply leave the account ID blank for the project.

Use the WorkingWithCrm.Workflow assembly you created back in the "Creating a Workflow Assembly" section of this chapter. Add a new class file named CustomProject to the Visual Studio .NET project with the code displayed in Listing 9-8.

Listing 9-8: Creating the Project Record

image from book
 using System; using System.Reflection; using System.Runtime.CompilerServices; using WorkingWithCrm.Workflow.CrmSdk; namespace WorkingWithCrm.Workflow {   public class CustomProject   {     public void CreateProjectRecordFromContact( string ProjectName, Guid ContactId, Guid Opp ortunityId )     {       // If the customer is a contact record, we will try to retrieve the parent account to use for the project       CrmService service = new CrmService();       service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx";       service.Credentials = System.Net.CredentialCache.DefaultCredentials;       WhoAmIRequest userRequest = new WhoAmIRequest();       WhoAmIResponse userResponse = (WhoAmIResponse) service.Execute(userRequest);       service.CallerIdValue = new CallerId();       service.CallerIdValue.CallerGuid = userResponse.UserId;       // Retrieve the record, casting it as the correct entity       ColumnSet cols = new ColumnSet();       cols.Attributes = new string [] {"parentcustomerid"};       contact oContact = (contact)service.Retrieve(EntityName.contact.ToString(), ContactId,  cols);       // If we find a parent account for this contact, use that as our project account       Guid accountId = Guid.Empty;       if (oContact.parentcustomerid != null)         accountId = new Guid(oContact.parentcustomerid.Value.ToString());       // Call the CreateProjectRecord method passing in the accountid (if one was found)       CreateProjectRecord(ProjectName, accountId, OpportunityId);     }     public void CreateProjectRecord(string ProjectName, Guid AccountId, Guid OpportunityId)     {       // Standard CRM Service Setup       CrmService service = new CrmService();       service.Url = "http://<crmserver>/mscrmservices/2006/crmservice.asmx";       service.Credentials = System.Net.CredentialCache.DefaultCredentials;       WhoAmIRequest userRequest = new WhoAmIRequest();       WhoAmIResponse userResponse = (WhoAmIResponse) service.Execute(userRequest);       service.CallerIdValue = new CallerId();       service.CallerIdValue.CallerGuid = userResponse.UserId;       // Create the new_project object       new_project project = new new_project();       // Set properties       project.new_name = ProjectName;       // Check to see if there is a valid account       if ( (AccountId.ToString() != string.Empty) && (AccountId != Guid.Empty))       {         Lookup accountId = new Lookup();         accountId.Value = AccountId;         accountId.type = EntityName.account.ToString();         project.new_accountid = accountId;       }       // Check to see if there is a valid opportunity       if ( (OpportunityId.ToString() != string.Empty) && (OpportunityId != Guid.Empty))       {         Lookup oppId = new Lookup();         oppId.Value = OpportunityId;         oppId.type = EntityName.opportunity.ToString();         project.new_opportunityid = oppId;       }       // Set the project owner       project.ownerid = new Owner();       project.ownerid.Value = userResponse.UserId;       project.ownerid.type = EntityName.systemuser.ToString();       // Creates the record in crm       Guid entityGuid = service.Create(project);     }   } } 
image from book

You must update the Workflow.config file to register your new assembly with the following code.

 <method name="Create Project Record From Contact"   assembly="Sonoma.Crm.Book.Workflow.dll"   typename="Sonoma.Crm.Book.Workflow.CustomProject"   methodname="CreateProjectRecordFromContact"   group="Custom Assemblies"   isvisible="1"   timeout="7200">   <parameter name="ProjectName" datatype="string"/>   <parameter name="ContactId" datatype="lookup" entityname="contact" />   <parameter name="OpportunityId" datatype="lookup" entityname="opportunity" /> </method> <method name="Create Project Record From Account"   assembly="Sonoma.Crm.Book.Workflow.dll"   typename="Sonoma.Crm.Book.Workflow.CustomProject"   methodname="CreateProjectRecord"   group="Custom Assemblies"   isvisible="1"   timeout="7200">   <parameter name="ProjectName" datatype="string"/>   <parameter name="AccountId" datatype="lookup" entityname="account" />   <parameter name="OpportunityId" datatype="lookup" entityname="opportunity" /> </method> 

Note the use of lookup parameters. This tells the Workflow Manager that you want to pass a GUID identifier for that identity, as you will see shortly. You will now deploy the assembly to the Web server, using the same process you have used before. Then you must create a workflow rule to create a new account when the Opportunity status is closed and marked as Won.

  1. To open the Workflow Manager, click Start, point to All Programs, point to Microsoft CRM, and then click Workflow Manager.

  2. Select Opportunity as the entity type, and then create a new rule for the Change Status event, calling it Create Project.

  3. Using the techniques you learned in Chapter 8, create a workflow rule as shown here.

    image from book

  4. When you create the custom assembly actions, you must set the parameters to the following:

    1. Create Project Record from Account Project Name: Use the dynamic opportunity Topic field. Use the Dynamic setting for both Account and Opportunity.

    2. Create Project Record from Contact Project Name: Use the dynamic opportunity Topic field. Use the Dynamic setting for both Account and Opportunity.

  5. Activate your new workflow rule.

After you activate the workflow rule, go back to your Microsoft CRM application and create a new Opportunity, selecting an Account as your potential customer. After you have saved it, click Close Opportunity on the Actions menu, and then mark the Opportunity as Won. You should see a Projects subarea in the navigation pane, as shown in Figure 9-17. Click it to see your new project.

image from book
Figure 9-17: Project list

Remember that the workflow rule will run asynchronously, so it's possible that you could click the Projects tab and not see anything because the rule hasn't completed processing yet. In that case, click the Refresh icon to check for new data. If it still doesn't appear, you should log back on to the server and use the Workflow Monitor tool to make sure that the rule processed correctly.

Note 

We could have also implemented this feature by using a post-callout. We decided to use a workflow assembly because we don't need the project to be created synchronously. This also allows for tweaks to the workflow rule without requiring development changes (such as changing the values passed in to the routine to default the account fields, or adding conditions for when you want to create a new record).




Working with Microsoft Dynamics CRM 3.0
Working with Microsoft Dynamics(TM) CRM 3.0
ISBN: 0735622590
EAN: 2147483647
Year: 2006
Pages: 120

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net