- If a contact is deleted in Outlook and the contact is not owned by you, the contact is deleted in Microsoft Dynamics CRM and Microsoft Dynamics CRM for Outlook.
- If a contact is deleted in Outlook and is owned by you, the contact is not deleted in Microsoft Dynamics CRM or Microsoft Dynamics CRM for Outlook, but the link is broken and the record is not updated or recreated in Outlook.
- If a contact is deleted in Microsoft Dynamics CRM or Microsoft Dynamics CRM for Outlook and the contact is owned by you, it is not deleted in Outlook, but the link is broken and the record is not updated.
- When you delete an e-mail message, contact, completed task, past appointment, or service activity in Outlook, it is not deleted in Microsoft Dynamics CRM, but the link is broken. However, if you delete a pending (incomplete) task or future appointment in Outlook, it is also deleted in Microsoft Dynamics CRM.
Wednesday, November 26, 2008
Understanding How Outlook Synchronization Handles Deletions
Customizing the Report Wizard Template
The report wizard functionality of Microsoft Dynamics CRM 4.0 allows end users to quickly and easily create basic Reporting Services reports. After the user completes the wizard, CRM creates an RDL file that can then be rendered within the CRM Reporting Services viewer. A common question we get is how to change the template used by the report wizard. Well, my colleague, Brian, found a solution to this request. In this post, we discuss how to add your company logo to the report template used by the wizard.
Note: This approach is definitely unsupported, so use at your own risk!
We will go through the following steps to update the template:
- Backup the existing template
- Create a simple tool to extract the template from the database
- Customize the template by adding our logo to the header
- Import the template back to the SQL database
Step 1 - Backup existing template
The report wizard template is located in the body field of the ApplicationBaseFile table within the _MSCRM database. We recommend that you back up this data prior to any alterations. You can do this simply by executing the following SQL statement in the _MSCRM database which creates a backup table in your _MSCRM database to store the template data:
select body into dbo.ApplicationFileBase_Backup from ApplicationFileBase
Creating your own tables within the _MSCRM database is typically frowned upon, so you could instead copy this to a backup table in another database.
Step 2 - Extract template from SQL
Unfortunately, since SQL Management Studio limits its output to 64KB, retrieving the template is not as simple as just selecting the body text and copy and pasting into your favorite XML editor. While there are a number of ways to accomplish this, we decided to write a very simple .NET application to extract the template. Create a console application in Visual Studio, and paste in the following code. Be sure to update the sqlServerName and databaseName variables with your information. Run the application and your template will be saved on the c: drive in a file called report_template.xml.
using System;using System.Collections.Generic;using System.Text;using System.Data.SqlClient;using System.Data;using System.IO;namespace CrmExtractTemplate{ class Program { static void Main(string[] args) { //update the sql server name string sqlServerName = "sqlserver"; string databaseName = "organization_mscrm"; string connectionString = String.Format("Data Source={0};Initial Catalog={1};Integrated Security=SSPI", sqlServerName, databaseName); SqlConnection conn = new SqlConnection(connectionString); conn.Open(); string sqlText = "select body from applicationfilebase"; SqlCommand cmd = new SqlCommand(sqlText); cmd.Connection = conn; cmd.CommandType = CommandType.Text; SqlDataReader rdr = cmd.ExecuteReader(); while (rdr.Read()) { TextWriter log = TextWriter.Synchronized(File.AppendText(@"c:\report_template.xml")); log.Write(rdr["body"].ToString()); log.Close(); } } }}
Step 3 - Add your logo to the template file
The report wizard template is a custom XSL stylesheet that CRM uses to transform into a valid RDL file. To update it, you need to find the actual RDL code, which is located within the <Report> node. This area contains all of the RDL XML. The default template doesn't include a <PageHeader> node, so you need to add one with your image information.
Now that you know where in the template file to add the image, you need to determine what XML to add. The easiest way to do that is to actually create a new report in a tool like Visual Studio .NET or Business Intelligence Design Studio. Also, by creating it first in a tool, you can be sure of the placement and sizing settings.
You can add an image as an external link or embed it in the report. The code for an external image would look similar to:
<PageHeader> <PrintOnFirstPage>true</PrintOnFirstPage> <ReportItems> <Image Name="image1"> <Sizing>AutoSize</Sizing> <Width>4.01042in</Width> <MIMEType /> <Source>External</Source> <Style /> <Value>http://www.sonomapartners.com/images/logo.jpg</Value> </Image> </ReportItems> <Height>0.79167in</Height> <PrintOnLastPage>true</PrintOnLastPage> </PageHeader>
However, if the image is small enough, you could also choose to embed it in the report. To do this, you would not only add the <PageHeader> node as shown before, but also add an <EmbeddedImages> node with the image. The code for the embedded image approach would look similar to:
<PageHeader> <PrintOnFirstPage>true</PrintOnFirstPage> <ReportItems> <Image Name="Image15"> <Source>Embedded</Source> <Value>sonomalogosmall</Value> <Sizing>AutoSize</Sizing> <Width>4.01042in</Width> </Image> </ReportItems> <Height>0.79167in</Height> <PrintOnLastPage>true</PrintOnLastPage> </PageHeader> <EmbeddedImages> <EmbeddedImage Name="sonomalogosmall"> <MIMEType>image/jpeg</MIMEType> <ImageData> /9j/4AAQSkZJRgABAQEAYABgAAD... remaining encoded image removed for brevity
</ImageData> </EmbeddedImage> </EmbeddedImages>
This code can be placed anywhere within the parent <Report></Report> node in the template file.
Step 4 - Import the template file back to SQL Server
Luckily, you don't need to rely on .NET to get the file back into SQL Server. Copy your template file to the c:\drive of your SQL Server, and then from SQL Management Studio, execute the following SQL:
create table dbo.ApplicationFileBase_tempLoad( Body xml)insert ApplicationFileBase_tempLoad (body) select BulkColumn from openrowset( bulk 'C:\Report_Template.xml', Single_Blob) as Bodyupdate ApplicationFileBaseset Body = (select top 1 convert(nvarchar(max), Body) from ApplicationFileBase_tempLoad)drop table dbo.ApplicationFileBase_tempLoad
Now when you create a new report wizard, your new logo appears in the top left corner of the report!
Posted by Jim Steger on November 21, 2008 | Permalink
Custom Workflow Activity - Add to Marketing List
You need to use Dependency Properties to pass parameters in (and out). The worflow activity form assistant will allow you to set some dynamically. You can use the context to get a handle to the crmService. CrmWorkflowActivity defines the menu labelling in the workflow activity.
Use the Developer PluginRegistration tool to register it, or there is sample installer code in the SDK workflow walkthrough.
Hope this helps someone.
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;
namespace Vizola.VEMWorkflow
{
[CrmWorkflowActivity("Add to Marketing List", "VEM Workflow")]
public partial class Add2ML: SequenceActivity
{
public static DependencyProperty listIdProperty = DependencyProperty.Register("listId", typeof(Lookup), typeof(Add2ML));
[CrmInput("Marketing List")]
[CrmReferenceTarget("list")]
public Lookup listId
{
get
{
return (Lookup)base.GetValue(listIdProperty);
}
set
{
base.SetValue(listIdProperty, value);
}
}
public static DependencyProperty contactIdProperty = DependencyProperty.Register("contactId", typeof(Lookup), typeof(Add2ML));
[CrmInput("Contact")]
[CrmReferenceTarget("contact")]
public Lookup contactId
{
get
{
return (Lookup)base.GetValue(contactIdProperty);
}
set
{
base.SetValue(contactIdProperty, value);
}
}
public Add2ML()
{
InitializeComponent();
}
///
/// The Execute method is called by the workflow runtime to execute an activity.
///
/// The context for the activity
///
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);
AddMemberListRequest amReq = new AddMemberListRequest();
amReq.EntityId = contactId.Value;
amReq.ListId = listId.Value;
AddMemberListResponse amRes = (AddMemberListResponse)crmService.Execute(amReq);
return ActivityExecutionStatus.Closed;
}
}
}
Using a Custom Database for reporting queries
But what about security and other settings - what do you need on your custom database?
If you are basing your custom reports on the stock ones (or otherwise), you may also find it very useful to keep using the common shared datasource. You are then easily able to return tenant-specific formats, culture info etc.
This all seems a bit of a nightmare to wade through. How can you do this? Well, it is possible, and this article shows you how, based on my own experience...
First of all, consider the collation of your custom database and make it the same as your tenant databases (CI_AI). If you don't you will later have collation compatibility issues that you don't want or need.
Next, users, roles and security. You need to make users and give roles to both network service and CRM reporting group. You can use the following script to do this:
USE [CUSTOMDB]
GO
CREATE USER [NT AUTHORITY\NETWORK SERVICE] FOR LOGIN [NT AUTHORITY\NETWORK SERVICE] WITH DEFAULT_SCHEMA=[dbo]
EXEC sp_addrolemember 'db_owner',[NT AUTHORITY\NETWORK SERVICE]
GO
DECLARE @SQLAccessGroup VARCHAR(255)SELECT TOP 1 @SQLAccessGroup=name FROM sys.server_principals WHERE CHARINDEX('SQLAccessGroup',name)<>0
EXEC ( 'CREATE USER [' + @SQLAccessGroup + '] FOR LOGIN [' + @SQLAccessGroup + ']' )EXEC ( 'sp_addrolemember ''db_owner'', [' + @SQLAccessGroup + ']' )
GO
So now you have your custom database setup and ready to populate with views, stored procedures, functions etc. If you are working in a multi-tenant environment (or even otherwise), and want to keep the standard shared datasource, you should aim to pass in the organization name from your report, apart from any other parameters. Consider using a query similar the following in each report.
<DataSourceName>CRM</DataSourceName>
<CommandText>
DECLARE @orgname Varchar(100)
SELECT @orgname = Name FROM OrganizationBase
EXEC [CUSTOMDB].dbo.usp_customsp @orgname, @filter
</CommandText>
<QueryParameters>
<QueryParameter Name="@filter">
<Value>=Parameters!CRM_xxxxxxxxxxxxxxxxx.Value</Value>
</QueryParameter>
</QueryParameters>
</Query>
The final piece of the puzzle is creating a function in your custom database to return the specific tenant's database name. You can use this in your dynamic SQL to retrieve your query information from the right database.
CREATE FUNCTION [dbo].[GetDBName] ( @orgname varchar (100))RETURNS varchar (100)AS BEGIN
DECLARE @dbname varchar (100)
SELECT @dbname = DatabaseName FROM MSCRM_CONFIG.dbo.Organization WHERE (UniqueName = @orgname) OR (FriendlyName = @orgname)
RETURN @dbname
END
GO
For anyone who is starting to write custom reports, this information would have saved me a lot of time. I hope it helps someone?
Forrester ranks Microsoft Dynamics CRM ahead of Salesforce.com
Recently, Forrester announced the results of its rankings for its CRM Sales and Customer Service Wave reports. Forrester ranked Microsoft Dynamics CRM as the #1 solution in the Direct Sales Management category, beating out Salesforce.com, Oracle Siebel, SAP CRM, and all others in SFA. Also, Dynamics CRM was ranked as #1 in the “Record Customer-Centric” category over Salesforce, Oracle, and SAP. This category focuses on complex B2B relationships where customer service is driven from within the CRM solution.
Earlier this year, Forrester published two other wave reports: Midmarket CRM Suites and Enterprise CRM Suites. These reports evaluate leading CRM products from an all-up perspective in terms of overall value and long-term viability for companies in the midmarket and enterprise segments. In both of these reports, Forrester ranked Microsoft Dynamics CRM higher than Salesforce.Com. In the midmarket category Dynamics CRM beat Salesforce.com to finish #2 behind Oracle Siebel. In the enterprise category, Dynamics CRM beat Salesforce.com to finish #3 behind Oracle Siebel and SAP-CRM.
You may access the full reports below from our corporate site (microsoft.com).
· The Forrester WaveTM: Sales Force Management, Q4 2008: click here.
· The Forrester WaveTM: Midmarket CRM Suites, Q3 2008: click here.
· The Forrester WaveTM: Enterprise CRM Suites, Q3 2008: click here.
Sanjay Jain
Microsoft Dynamics ISV Architect Evangelist
Blogs: Team’s Blog + Sanjay’s Blog
Storing Configuration Data for Microsoft Dynamics CRM Plug-ins
CRM MVP Mitch Milam returns as a guest blogger. You can read more from Mitch at his blog.
One of the benefits to the plug-in architecture of CRM 4.0 is the ability to store plug-ins in the CRM database so they may be used by multiple CRM servers. This introduces a slight complication regarding the storage of configuration information. Because the plug-in assembly doesn’t reside on the disk the normal method of using a .config file located with the assembly no longer works.
Luckily, the plug-in architecture solves this issue by allowing the developer to supply configuration information for each step executed by the plug-in.
Plug-in Configuration Architecture
As noted in the CRM SDK article, Writing the Plug-in Constructor, when creating your plug-in, you may define a constructor that passes two parameters to your plug-in: unsecure configuration and secure configuration:
1: public class SamplePlugin : IPlugin
2: {
3: public SamplePlugin(string unsecureConfig, string secureConfig)
4: {
5: }
6: }
Both parameters are strings and may contain any configuration data, in any format, that you wish. For the purposes of this discussion, we will only be concerned with the unsecure configuration parameter.
Creating a Configuration Structure
Since most of us are familiar with the XML configuration provided by the standard Properties.Settings structure, I thought it would be a great idea to retain as much of that experience as possible so we can move code from a stand-alone test application to a plug-in with little difficulty.
Using an XML fragment that closely resembles the Settings section found in the .config file of a .Net assembly, we can create a similarly functional system for storing configuration data. Consider the following XML:
1: <Settings>
2: <setting name="RetryCount">
3: <value>5</value>
4: </setting>
5: <setting name="TaskPrefix">
6: <value>This task was created on {0}.</value>
7: </setting>
8: <setting name="FirstRun">
9: <value>false</value>
10: </setting>
11: </Settings>
As you can see, we have three settings which contain values that we would normally find in our .config file and which are used to configure our assembly. Using the Plug-in Registration Tool, we can add this information to the Unsecure Configuration field when registering a new step, as show by the figure below:
Plug-in Configuration Class
I created a simple class to extract values from an XML document for simple data types such as Guids, strings, Booleans, and integers, given the structure we discussed above:
1: class PluginConfiguration
2: {
3: private static string GetValueNode(XmlDocument doc, string key)
4: {
5: XmlNode node = doc.SelectSingleNode(String.Format("Settings/setting[@name='{0}']", key));
6:
7: if (node != null)
8: {
9: return node.SelectSingleNode("value").InnerText;
10: }
11: return string.Empty;
12: }
13:
14: public static Guid GetConfigDataGuid(XmlDocument doc, string label)
15: {
16: string tempString = GetValueNode(doc, label);
17:
18: if (tempString != string.Empty)
19: {
20: return new Guid(tempString);
21: }
22: return Guid.Empty;
23: }
24:
25: public static bool GetConfigDataBool(XmlDocument doc, string label)
26: {
27: bool retVar;
28:
29: if (bool.TryParse(GetValueNode(doc, label), out retVar))
30: {
31: return retVar;
32: }
33: else
34: {
35: return false;
36: }
37: }
38:
39: public static int GetConfigDataInt(XmlDocument doc, string label)
40: {
41: int retVar;
42:
43: if (int.TryParse(GetValueNode(doc, label), out retVar))
44: {
45: return retVar;
46: }
47: else
48: {
49: return -1;
50: }
51: }
52:
53: public static string GetConfigDataString(XmlDocument doc, string label)
54: {
55: return GetValueNode(doc, label);
56: }
57: }
Putting PluginConfiguration to Work
Once we have our PluginConfiguration class added to our project, we need to modify the plug-in constructor to extract the values from our configuration string:
1: public SamplePlugin(string unsecureConfig, string secureConfig)
2: {
3: XmlDocument doc = new XmlDocument();
4: doc.LoadXml(unsecureConfig);
5:
6: string TaskPrefix = PluginConfiguration.GetConfigDataString(doc, "TaskPrefix");
7: bool FirstRun = PluginConfiguration.GetConfigDataBool(doc, "FirstRun");
8: int RetryCount = PluginConfiguration.GetConfigDataInt(doc, "RetryCount");
9: }
There is no automatic determination of data types so you will need to know which method to use to extract a specific value from the configuration data.
Conclusion
Today we’ve covered how to store configuration information used by a CRM plug-in within the CRM database itself. One of the items you may wish to remember is that each step executed by a plug-in has its own configuration information. If you are using the same configuration data for multiple steps, you will need to set the configuration values to be the same between steps or just insert configuration data that is necessary for a particular step to complete successfully.
Microsoft Dynamics CRM Plug-in Template for Visual Studio
The Microsoft Dynamics CRM User Experience team is always looking to improve our customers’ experience when using the SDK. Past examples of our work in this area include publishing the Plug-in Developer tool and the Plug-in Registration tool for registering plug-ins and custom workflow activities. This blog is about another useful tool for your development toolbox, a C# and VB plug-in template for Microsoft Visual Studio 2008.
Since I have to write plug-ins now and then, I wanted to come up with a standard Visual Studio code template for writing a plug-in. In the code template provided in this blog I have included the following features:
- A plug-in constructor that supports passing secure and non-secure information to the plug-in
- A standard Execute method that retrieves and verifies information from the passed context parameter
- Web service proxy instantiation
- Basic handling of SOAP exceptions
- Custom methods that instantiate the Web service proxies for those plug-ins that execute in a child pipeline
The complete plug-in template can be downloaded here. To install the template into Visual Studio, copy the downloaded zip file into your Visual Studio 2008\Templates\ProjectTemplates\Visual C# (or Visual Basic) folder.
To use the template, simply create a new project in Visual Studio, select a C# or Visual Basic project type in the New Project dialog box, and then select the MSCRM Plug-in template.
After creating the new project, you may need to remove and then add the project references to the Microsoft.Crm.Sdk and Microsoft.Crm.SdkTypeProxy assemblies in Solution Explorer if Visual Studio cannot find the assemblies on your system when you build the project. In the project’s property page on the Signing tab, remember to check Sign the assembly and create a new key file.
Below is the C# version of the plug-in code if you just want the code and don’t need the Visual Studio template.
1: using System;
2: using System.Collections.Generic;
3: using Microsoft.Win32;
4:
5: // Microsoft Dynamics CRM namespaces
6: using Microsoft.Crm.Sdk;
7: using Microsoft.Crm.SdkTypeProxy;
8: using Microsoft.Crm.SdkTypeProxy.Metadata;
9:
10: namespace Crm.Plugins
11: {
12: public class MyPlugin : IPlugin
13: {
14: // Provide configuration information that can be passed to a plug-in at run-time.
15: private string _secureInformation;
16: private string _unsecureInformation;
17:
18: // Note: Due to caching, Microsoft Dynamics CRM does not invoke the plug-in
19: // contructor every time the plug-in is executed.
20:
21: // Related SDK topic: Writing the Plug-in Constructor
22: public MyPlugin(string unsecureInfo, string secureInfo)
23: {
24: _secureInformation = secureInfo;
25: _unsecureInformation = unsecureInfo;
26: }
27:
28: // Related SDK topic: Writing a Plug-in
29: public void Execute(IPluginExecutionContext context)
30: {
31: DynamicEntity entity = null;
32:
33: // Check if the InputParameters property bag contains a target
34: // of the current operation and that target is of type DynamicEntity.
35: if (context.InputParameters.Properties.Contains(ParameterName.Target) &&
36: context.InputParameters.Properties[ParameterName.Target] is DynamicEntity)
37: {
38: // Obtain the target business entity from the input parmameters.
39: entity = (DynamicEntity)context.InputParameters.Properties[ParameterName.Target];
40:
41: // TODO Test for an entity type and message supported by your plug-in.
42: // if (entity.Name != EntityName.account.ToString()) { return; }
43: // if (context.MessageName != MessageName.Create.ToString()) { return; }
44: }
45: else
46: {
47: return;
48: }
49:
50: try
51: {
52: // Create a Microsoft Dynamics CRM Web service proxy.
53: // TODO Uncomment or comment out the appropriate statement.
54:
55: // For a plug-in running in the child pipeline, use this statement.
56: // CrmService crmService = CreateCrmService(context, true);
57:
58: // For a plug-in running in the parent pipeline, use this statement.
59: ICrmService crmService = context.CreateCrmService(true);
60:
61: // TODO Plug-in business logic goes here.
62:
63: }
64: catch (System.Web.Services.Protocols.SoapException ex)
65: {
66: throw new InvalidPluginExecutionException(
67: String.Format("An error occurred in the {0} plug-in.",
68: this.GetType().ToString()),
69: ex);
70: }
71: }
72:
73: #region Private methods
74: /// <summary>
75: /// Creates a CrmService proxy for plug-ins that execute in the child pipeline.
76: /// </summary>
77: /// <param name="context">The execution context that was passed to the plug-ins Execute method.</param>
78: /// <param name="flag">Set to True to use impersonation.</param>
79: /// <returns>A CrmServce instance.</returns>
80: private CrmService CreateCrmService(IPluginExecutionContext context, Boolean flag)
81: {
82: CrmAuthenticationToken authToken = new CrmAuthenticationToken();
83: authToken.AuthenticationType = 0;
84: authToken.OrganizationName = context.OrganizationName;
85:
86: // Include support for impersonation.
87: if (flag)
88: authToken.CallerId = context.UserId;
89: else
90: authToken.CallerId = context.InitiatingUserId;
91:
92: CrmService service = new CrmService();
93: service.CrmAuthenticationTokenValue = authToken;
94: service.UseDefaultCredentials = true;
95:
96: // Include support for infinite loop detection.
97: CorrelationToken corToken = new CorrelationToken();
98: corToken.CorrelationId = context.CorrelationId;
99: corToken.CorrelationUpdatedTime = context.CorrelationUpdatedTime;
100: corToken.Depth = context.Depth;
101:
102: RegistryKey regkey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\MSCRM");
103:
104: service.Url = String.Concat(regkey.GetValue("ServerUrl").ToString(), "/2007/crmservice.asmx");
105: service.CorrelationTokenValue = corToken;
106:
107: return service;
108: }
109:
110: /// <summary>
111: /// Creates a MetadataService proxy for plug-ins that execute in the child pipeline.
112: /// </summary>
113: /// <param name="context">The execution context that was passed to the plug-ins Execute method.</param>
114: /// <param name="flag">Set to True to use impersonation.</param>
115: /// <returns>A MetadataServce instance.</returns>
116: private MetadataService CreateMetadataService(IPluginExecutionContext context, Boolean flag)
117: {
118: CrmAuthenticationToken authToken = new CrmAuthenticationToken();
119: authToken.AuthenticationType = 0;
120: authToken.OrganizationName = context.OrganizationName;
121:
122: // Include support for impersonation.
123: if (flag)
124: authToken.CallerId = context.UserId;
125: else
126: authToken.CallerId = context.InitiatingUserId;
127:
128: MetadataService service = new MetadataService();
129: service.CrmAuthenticationTokenValue = authToken;
130: service.UseDefaultCredentials = true;
131:
132: RegistryKey regkey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\MSCRM");
133:
134: service.Url = String.Concat(regkey.GetValue("ServerUrl").ToString(), "/2007/metadataservice.asmx");
135:
136: return service;
137: }
138: #endregion Private Methods
139: }
140: }
Cheers,