Detect metadata changes in MSCRM 4.0

The

Google Tags:
Message retrieves a time stamp for the metadata can be executed before you retrieve metadata. You can repeat this process later to see if the metadata has changed in the intervening time. If the value is the same, the metadata has not changed. If the value is different, you retrieve the metadata again to get the updated values.

This sample shows how to detect metadata changes in MSCRM 4.0 for the purposes of building a metadata cache.


using System;
using System.Collections;
using MetadataServiceSdk;
using CrmSdk;

namespace Microsoft.Crm.Sdk.Reference.MetadataServiceApi
{
///
/// This sample shows how to retrieve a timestamp.
///

public class RetrieveTimestamp
{
public RetrieveTimestamp()
{
}

public static bool Run(string crmServerUrl, string orgName)
{
bool success = true;

try
{
// Set up the CRM Services.
MetadataService metadataService = Microsoft.Crm.Sdk.Utility.CrmServiceUtility.GetMetadataService(crmServerUrl, orgName);
metadataService.PreAuthenticate = true;

// Create a metadata timestamp request
RetrieveTimestampRequest initialTimestampRequest = new RetrieveTimestampRequest();

// Execute the request
RetrieveTimestampResponse initialTimestampResponse = (RetrieveTimestampResponse)metadataService.Execute(initialTimestampRequest);

// Store the retrieved metadata timestamp
string initialTimestamp = initialTimestampResponse.Timestamp;

// Create a metadata timestamp request
RetrieveTimestampRequest timestampRequest = new RetrieveTimestampRequest();

// Execute the request
RetrieveTimestampResponse timestampResponse = (RetrieveTimestampResponse)metadataService.Execute(timestampRequest);

// Access the retrieved timestamp
string currentTimestamp = timestampResponse.Timestamp;

// Compare the timestamp with a previously retrieved timestamp
if (currentTimestamp == initialTimestamp)
{
// The metadata has not been modified since our initial timestamp was retrieved.
}
else
{
// The metadata has been modified since our initial timestamp was retrieved.
}

#region check success

// Verify that the timestamp has data
if (currentTimestamp.Length == 0)
{
success = false;
}

#endregion

#region Remove Data Required for this Sample

#endregion

}
catch (System.Web.Services.Protocols.SoapException)
{
// Perform error handling here.
throw;
}
catch (Exception)
{
throw;
}

return success;
}
}
}

Custom Workflow Activities for Microsoft Dynamics CRM 4.0

Developing custom workflow activities enables us to merge our own requirements with the Microsoft CRM 4.0 workflow features. This custom workflow activity can be used as a workflow step within a CRM workflow for opportunities. To keep it simple we will only export the product names of all products related to an opportunity (opportunityproduct) to a text file and we will not define the complete workflow to cover the scenario described above.

The custom workflow activity will call a Web service method of Microsoft Dynamics CRM and use an input parameter that can be entered by the user as a workflow property within the CRM workflow form.

 

Prerequisites

These are the tools and components I used to build my own Microsoft Dynamics CRM custom workflow activity:

  • Microsoft Visual Studio 2005
  • Windows Workflow Foundation
  • Visual Studio 2005 extensions for .NET Framework 3.0 (Windows Workflow Foundation)
  • Microsoft Dynamics CRM 4.0 SDK: the following assemblies must be added to your project. They can be found in the SDK\Bin folder:
    • Microsoft.Crm.Sdk.dll
    • Microsoft.Crm.SdkTypeProxy.dll
  • The Plug-in Developer Tool to register the workflow as a plug-in: see Microsoft Dynamics CRM SDK Documentation for more information how to build and use this tool

 

Building a Workflow Activity Library

If the Visual Studio 2005 extensions for .NET Framework 3.0 are installed, some additional project templates for workflows are provided:

  • Choose the Workflow Activity Library template.
  • Add a reference to Microsoft.Crm.Sdk.dll and Microsoft.Crm.SdkTypeProxy.dll located in the SDK\Bin folder to the project created.

You may want to rename the generated Activity1 class. Please do not use the same name as the namespace of your project because you might get problems when publishing this workflow in CRM. In this example I use ExportProductInformationActivity as the class name andExportProductInformation as the namespace.

Change to the code view of the generated class and you see that the generated class derives from SequenceActivity. This is an activity of the Windows Workflow Foundation base activity library which may run a set of child activities in an ordered manner. This is ok for our purpose even though we will not run any child activities.

To access the custom workflow activity from the Microsoft CRM 4.0 workflow interface you have to annotate the class with the .NET attribute CrmWorkflowActivity and override the Execute method which is called by the workflow runtime to execute an activity. Basically, that is all you have to do to implement a custom workflow activity for Microsoft CRM 4.0.

In our scenario we want to access the CRM Web service methods to get information about the opportunityproducts related to the opportunity for which the workflow is started.

Use the ActivityExecutionContext passed to the Execute method to get the workflow context, which contains information about the instance of the workflow.
Basically you will get information about the primary entity (name, id, pre- and post-image) using the workflow context, but it also contains a method CreateCrmService which returns a reference to the CrmService. Please note that this is a reference to the CrmService of the SDK assembly, we added to the project, so you do not have access to any custom fields or entities. Use CRM's MetadataService Web service to access them. According to the CrmService there is a CreateMetadataService method to get a reference to the MetadataService.

Here is the code of ExportProductInformationActivity.cs:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Workflow;
using System.IO;

namespace ExportProductInformation
{
/// <summary>
/// Custom workflow activity class derived from SequenceActivity.
/// The CrmWorkflowActivity provides the information needed by the CRM workflow form
/// </summary>
[CrmWorkflowActivity("Export Product Information", "My Custom Workflow Activities")]
public partial class ExportProductInformationActivity : SequenceActivity
{

/// <summary>
/// The dependency property is used to define an input parameter
/// It is annotated with attribute CrmInput to access the parameter within the CRM workflow form
/// The parameter of Attribute CrmDefault is used as the default content of the property.
/// </summary>
public static DependencyProperty filenameProperty = DependencyProperty.Register("filename", typeof(string), typeof(ExportProductInformationActivity));

[CrmInput("Filename")]
[CrmDefault(@"C:\Test\OrderForecast.txt")]
public string filename
{
get
{
return (string)base.GetValue(filenameProperty);
}
set
{
base.SetValue(filenameProperty, value);
}
}

public ExportProductInformationActivity()
{
InitializeComponent();
}

/// <summary>
/// The Execute method is called by the workflow runtime to execute an activity.
/// </summary>
/// <param name="executionContext"> The context for the activity</param>
/// <returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{

// Get the context service.
IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;

// Use the context service to create an instance of CrmService.
ICrmService crmService = context.CreateCrmService(true);
BusinessEntityCollection opportunityProducts = RetrieveOpportunityProducts(crmService, context.PrimaryEntityId);

// Use the given filename to create a new StreamWriter
TextWriter w = new StreamWriter(this.filename);
string productname;

foreach (DynamicEntity de in opportunityProducts.BusinessEntities)
{
productname = de.Properties.Contains("productid") ? ((Lookup)de.Properties["productid"]).name : null;
w.WriteLine(productname);
}

w.Close();

return ActivityExecutionStatus.Closed;
}

private BusinessEntityCollection RetrieveOpportunityProducts(ICrmService service, Guid oppId)
{
Microsoft.Crm.Sdk.Query.QueryByAttribute query = new Microsoft.Crm.Sdk.Query.QueryByAttribute();

query.ColumnSet = new Microsoft.Crm.Sdk.Query.AllColumns();
query.EntityName = EntityName.opportunityproduct.ToString();
query.Attributes = new string[] { "opportunityid" };
query.Values = new string[] { oppId.ToString() };

RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest();
retrieve.Query = query;
retrieve.ReturnDynamicEntities = true;

RetrieveMultipleResponse retrieved = (RetrieveMultipleResponse)service.Execute(retrieve);
return retrieved.BusinessEntityCollection;
}
}
}



You can noticed that the code contains the definition of a DependencyProperty named filenameProperty. In Windows Workflow Foundation Properties provided on activities are used to make the activity configurable by the user respectively the consumer of the activity. In our case the CRM user should be able to decide the location where the generated export file will be saved. The property is annotated with the CRMInput attribute to be able to access it as a workflow property on the CRM workflow form as an input parameter. The CrmDefaultattribute is used to set a default value.



All plug-in assemblies used with Microsoft CRM 4.0 have to be signed. So we have to sign the workflow assembly, too, as it will be registered as a plug-in. To sign the assembly with a strong name, go to the project properties of the Visual Studio project, tab 'Signing', check 'Sign the assembly'. Under 'Choose a strong name key file' choose <New> and provide a name for the key file.


 


Registering the Workflow Activity as a Plug-in


After your custom workflow activity has been compiled, you have to register this assembly as a plug-in. Your activity will then appear in the workflow form in the Microsoft Dynamics CRM Web application.

I used the SDK's Plug-in Developer Tool to register the workflow activity using the following register.xml:

<?xml version="1.0" encoding="utf-8" ?>
<Register
LogFile = "Plug-in Registration Log.txt"
Server = "http://[your CRM server:port]"
Org = "[your organization name]"
Domain = "[your domain]"
UserName= "[your user name]" >

<Solution SourceType="0" Assembly="[location of the assembly dll]">
<WorkflowTypes>
<WorkflowType
TypeName="ExportProductInformation.ExportProductInformationActivity"
FriendlyName="Export Product Information"/>
</WorkflowTypes>
</Solution>
</Register>


If you run the Plug-in Developer Tool multiple times to register the assemblies after you changed something you might have to restart IIS.



Testing the Custom Workflow Activity


The Custom Workflow Activity is now registered and if you open the Workflows and create a new one for opportunities, you can add your activity as a workflow step:

Make sure you have created an appropriate directory at CRM Server to save the output file to as there is no error handling implemented. Saving the file to C:\ is not the best option.

To keep it simple we choose the workflow to be available to run On Demand. Please do not forget to uncheck the option Record is created under Start when.
After publishing the workflow you can start it by selecting an opportunity and using button Run Workflow.

 

Debugging the Custom Workflow Activity


To Debug the custom workflow activity you have to copy the pdb file of you assembly to %installdir%\server\bin\assembly on your CRM server, set a breakpoint in your Execute method and attach your Visual Studio project to Microsoft Dynamics CRM asynchronous service (processCrmAsyncService.exe) on your CRM Server.

If you have difficulties to update the file in the assembly directory on the server because there is already another copy of the file run iisreset in a command window and / or restart the Microsoft Dynamics CRM asynchronous service.

Play any tones in you CPU speakers:!




#include
#include
#include

void main()
{
#define sn sound;
clrscr();
const DELAY=500;
int c=0,A;;
char az[60],her,v=14;
// /int c=5;
cout<<"\n\tINPUT CHARACTERS IN SEQUENCE\n===>";
cin>>az;
// az[c]=az;
//c=1;
one:
//az[5]=az[c];

switch(az[c])
{
case 'a':
//cout<<"\n\tchar is"<<'a';
sound(100);
delay(DELAY);
nosound();
break;
case 'b':
//cout<sound(200);
delay(DELAY);
nosound();
break;
case 'c':
sound(300);
delay(DELAY);
nosound();
break;
case 'd':
sound(400);
delay(DELAY);
nosound();
break;
case 'e':
sound(500);
delay(DELAY);
nosound();
break;

. . . . .
. . . . .
. . . .
. . . .

break;
case '9':
sound(14400);
delay(DELAY);
nosound();
break;
case '0':
sound(15400);
delay(DELAY);
nosound();
break;


default:
goto end;
sound(100);
delay(DELAY);
nosound();

// cout<<"\n\t unknown charector"<break;
}
//A=A+1;
cout<
c=c+1;

if ( c<=53 )
goto one;
//clrscr();


end:


}

Filtered Views for Microsoft Dynamics CRM 4.0

I rarely post about product updates on this blog, but this time I make an exception. The reason is that the new features in the Filtered Views are great and I want to share it with you.

So far the Filtered Views allowed creating dynamic views based on parameters being supplied at runtime. With "at runtime" I mean that you pass parameters in your CRM script code. That was good enough to view associated data going well beyond the capabilities of a standard CRM view. However, good is never good enough, so here's the list of enhancements in the new release:

  1. UI Filters to define custom search fields in filtered views
  2. Multi-Language support
  3. An entity context to activate field mappings when creating a new entity from a filtered view
  4. Script support to perform typical tasks like hiding toolbar buttons or colorizing views

Probably the best explanation of what you can do with these new features is the following screenshot: 

There are three UI Filters (search fields). The label of each UI Filter can use HTML to create special effects, like done for the "Payment Method" label. Dropdown controls can be used to display options from the CRM metadata. You can also use them to display a list of records, which are identified by a dynamic query that can be parameterized as well. Of course you also have text fields for simple text searches. Additional controls will be added, especially lookup and date controls seems making sense.

You also get a view OnLoad script. Dynamics CRM doesn't have a view event model and it's a highly requested feature for a long time. In the Filtered Views you can attach your own script to do view manipulations very easily. The above screenshot, for instance, uses the following OnLoad code:




var createNewRecordButtonId = "_MBopenObj" + etcViewItems;
var button = viewBody.document.getElementById(createNewRecordButtonId);

if (button != null) {
button.style.display = "none";
}

var dataTable = viewBody.document.all.gridBodyTable;

for (var i = 0; i < dataTable.rows.length; i++) {
var row = dataTable.rows[i];

if (row.cells.length > 5) {
var cell = row.cells[5];

if (cell.innerText == "") {
cell.style.backgroundColor = "#FF8080";
}
else {
cell.style.backgroundColor = "#80FF80";
}
}
}

It hides the "New" button and then loops through the data rows to display cells in a different color.

When clicking the "New" button (which is hidden in this example but usually available) you most probably want to associate it to an existing record. When displaying the Filtered View on a CRM form, this most likely is the hosting form itself, but it can also be a different record if that makes sense. Consider an account form where you use a Filtered View to display the contacts of the parent account. When clicking "New" it makes sense to associate the new contact with the parent account, rather than the account you are currently viewing.

And finally the Filtered Views now fully support multi-language, so that users always get the UI in the language they use in Dynamics CRM.

I know that you can do all of the above with SQL reports, so you don't have to email me that it can be done differently. When it comes to presenting data then everything can be done with SQL reports. But that also is true for other development areas. Almost everything is doable with the CRM Services, but not everyone likes it or is a .NET developer. And not everyone is comfortable with SQL reports, so it's always a good option to have alternatives.