Saturday, February 28, 2009

CRM 4.0 Public View Manager Wizard (Hiding Views)




The Public View Manager Wizard is now available on GI’s company website.
Click here if you wish to acquire the source code

I believe that beside FLS, hiding system views is one of the most requested features that are missing from CRM. The concept behind the CRM public views did not fit our product architecture. We needed to come up with a way to deliver pre defined public views with pre defined filtering and column display for different business units and roles in order to create a complete user experience and target specific business logic. Obviously, the idea of having a single default view set for the entire organization and the inability to hide specific views marks the current architecture as unacceptable.

Following are the PVS Wizard features which I’m sure you’ll find very appealing:

  1. Ability to set different public views for specific business units, roles and users.

  2. Ability to set the public views order within the target’s (BU, ROLE, and USER) list of available views.

  3. Ability to set a default public view at the business unit, role and user level.

  4. Inheritable settings. Settings defined for a business unit affects all roles within that BU. Settings for a Role affects all users within that Role.

  5. Ability to create exceptions at the user level by allowing users to request a specific view as their favorite default view.

  6. Complements our security model (FLS and SMW) and enables us to completely secure CRM from all angles.

  7. An intuitive User interface that enables you to define a complete PVS settings for an entire organization in just a few minutes.





If you are a Microsoft dynamics partner our sharing initiative enables you to save time and money by acquiring the source code once and distributing it to all your current and future clients. Find a client that will appreciate the PVS wizard or any of our other wizards and acquire the source code while you’re at it.

PVS Global features:

  1. Available for all languages

  2. Supports multi tenancy

  3. Supports RTL (right to left) and LTR (left to right) displays

  4. Supports IFD

  5. Supports IE8



In order to rewind the PVS video, right click on the flash movie, select rewind and then select play.


Saturday, February 14, 2009

CRM 4.0 Concatenating Fields Plug-In


A few weeks back I carried out a boot-camp training course to a team of dot net developers. I have to say it’s quite amusing to see the frustration on experienced developers faces when they realize the amount of energy one needs to invest in order to implement a requirement as simple as the one covered in this post. No doubt dynamics requires getting use to, but the benefits of using such a vast platform makes it all worthwhile.

Many CRM implementations require you to construct a field’s value by concatenating other fields. There are hands full of reasons for implementing this type of functionality and Most have to do with ways data is displayed or what I call data usability.

There are three common approaches which one can utilize. If you find yourself in that crossroad consider the pros and cons of each approach before you start developing.


The first solution is attaching a JavaScript event handler to the CRM form OnSave event. This allows you to set the target field before the form is saved and data is actually sent to the server. There is no arguing about the easiness of this approach since it only requires a few lines to get this done.

Here is an example which you can follow:




function OnCrmPageLoad()
{
crmForm.attachEvent(“onsave”,OnCrmPageSave);
}

function OnCrmPageSave()
{
var attribute1Value = (crmForm.new_attribute1.DataValue)? crmForm.new_attribute1.DataValue:””;
var attribute2Value = (crmForm.new_attribute2.DataValue)? crmForm.new_attribute2.DataValue:””;
crmForm.new_targetattribute.DataValue = attribute1Value + “ ” + attribute2Value;
}



This method has obvious drawbacks which must be considered. One which is especially important is when the application needs to utilize clients other then Internet explorer such as excel when importing data or a web service when constructing data from code. Of course this is more of a general statement since writing client side code is never about a client but about all available ones. This is something many dynamics developers tend to oversee.


The second approach is creating a workflow using the workflow designer. Although this provides a remedy to the dilemma presented above, the method is not bullet proof and has its own drawbacks. The first one has to do with the way the workflows operate. Since the workflow process is asynchronous the user won’t see the result concatenation until he reopens the record. A second disadvantage worth mentioning has to do with designer limitations. Consider a scenario where you need to concatenate the values of a lookup field and a text field. The workflow designer does not allow you to concatenate dynamic values which are not of the same type. This might convince you choose the third and final approach which is using a plug-in

Although this might take longer to implement, planning and coding it once will no doubt save you plenty of time down the road. And since the following plug-in offers a generic solution you’ll get there even faster. The plug-in also offers a remedy to the workflow drawbacks mentioned above.

Basically, in order to provide full coverage, the plug-in needs to run on both post create and post update events and also require post event images from which the values are read and concatenated. This approach seems unnecessary at first but since lookup and customer types don’t carry their names (just guids) to the plug-in the only way to retrieve the actual names is to use an image.

In order to make the process friendlier I added a configuration xml which enables you to configure multiple target fields for each entity and set the desired format for each target field.


Bellow is a sample configuration xml. You must add it to the post-create and post-update unsecure configuration boxes. You might wonder why this is required for both events. The reason is that the plug-in needs to address both the creation of a new record and the update of existing records when it loads / cached and re-cached by CRM.


This sample uses fields from the incident entity as a showcase. This is also the case for the plug-in solution xml file which is displayed at the bottom of the post.




<Entity>
<Target ID="title" Format="{0} - {1}">
<Field ID="subjectid" Type="Lookup"/>
<Field ID="customerid" Type="Customer"/>
</Target>
</Entity>


The Target node describes the field whose value is constructed from the inner Field nodes. The Target ID attribute must specify and existing attribute name. The plug-in code makes use of the String.Format method which receives a format string such as “{0} – {1}” and a list of optional parameters. Each Field Node must specify an existing entity attribute and correct Type. The Type attribute is obviously used to differentiate between CRM types.


The Generic concatenation Plug-in is build on top of 3 classes. A Handler class which implements the IPlugIn Interface. A TargetField class which represent a single Target. And the ConcatPlugin proxy class which implements the concatenation process. I’ve also added remarks and regions to make the code more readable.



using System;
using System.Xml;
using System.Collections.Generic;
using System.Text;

using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;

namespace GenericConcatPlugIn
{
///
/// Plug-In Handler
///

public class Handler : IPlugin
{
#region IPlugin Members
///
/// Holds all fields that require concatenation
///

private Dictionary Targets;
///
/// plug-in constructor
///

///
///
public Handler(String config, String secureConfig)
{
XmlDocument TargetsDocument = TryLoadTargetsXml(config);

#region Build Targets Settings

XmlNodeList TargetsNodeList = TargetsDocument.SelectNodes("//Target");
this.Targets = new Dictionary(TargetsNodeList.Count);

foreach (XmlElement ndField in TargetsNodeList)
{
TargetField Target = new TargetField(ndField);
if (this.Targets.ContainsKey(Target.ID))
{
this.Targets[Target.ID] = Target;
}
else
{
this.Targets.Add(Target.ID, Target);
}

}

#endregion
}
///
/// Attempt to load entity configuration xml
///

///
///
private XmlDocument TryLoadTargetsXml(String configXml)
{
#region Validate Plugin Configuration

if (configXml == null || configXml.Length == 0)
{
throw new InvalidPluginExecutionException("Initialize: Configuration Xml cannot be null");
}

#endregion

#region Return Configuratoin Document

try
{
XmlDocument document = new XmlDocument();
document.LoadXml(configXml);
return document;
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException("Initialize: Error loading Configuration Xml Document", ex);
}

#endregion
}
///
/// Plug-in Execute implementation
///

///
public void Execute(IPluginExecutionContext context)
{

#region Execute Concatenation Plugin

ConcatPlugin contactPlugIn = new ConcatPlugin();
contactPlugIn.Context = context;
contactPlugIn.Execute(Targets);

#endregion
}

#endregion
}
///
/// A Field whose value is constructed (concatenated) from other fields
///

public class TargetField
{
#region Target Field Members

public String ID = String.Empty;
public String Format = String.Empty;
public Dictionary Related;

#endregion

public TargetField(XmlElement ndTarget)
{
#region Set Field Members

this.Related = new Dictionary();
this.ID = ndTarget.Attributes["ID"].Value;
this.Format = ndTarget.Attributes["Format"].Value;

#endregion

#region Set Field Related (Concatenated) Fields

XmlNodeList RelatedFieldList = ndTarget.SelectNodes("Field");

foreach (XmlElement related in RelatedFieldList)
{
String relatedId = related.Attributes["ID"].Value;
String relatedType = related.Attributes["Type"].Value;

if (!Related.ContainsKey(relatedId))
{
Related.Add(relatedId, relatedType);
}
}

#endregion

#region Sample Target Xml Configuration
/*





*/
#endregion
}
}
///
/// Plug-in proxy
///

public class ConcatPlugin
{
///
/// Plug-in Original Context
///

public IPluginExecutionContext Context;
///
/// Plug-in Main method
///

///
internal void Execute(Dictionary Targets)
{
#region Validate Target Entity

if (!this.Context.InputParameters.Contains(ParameterName.Target))
{
return;
}

#endregion

#region Concatenate Target Fields

DynamicEntity Current = null;
foreach (KeyValuePair Target in Targets)
{
Current = ((DynamicEntity)this.Context.InputParameters[ParameterName.Target]);
Current.Properties.Add(ConcatProperty(Target.Value));
}

if (Current == null)
{
return;
}

#region Update the target

if (this.Context.MessageName == "Create")
{
if( !this.Context.OutputParameters.Contains("id") )
{
return;
}

ICrmService Service = this.Context.CreateCrmService(true);
Key entityId = new Key( new Guid( this.Context.OutputParameters["id"].ToString() ) );
KeyProperty entityKey = new KeyProperty(this.Context.PrimaryEntityName + "id",entityId);
Current.Properties.Add(entityKey);
Service.Update(Current);
}
else if (this.Context.MessageName == "Update" && this.Context.Depth == 1)
{
ICrmService Service = this.Context.CreateCrmService(true);
Service.Update(Current);
}
#endregion

#endregion
}
///
/// Reference to Target Dynamic Entity being created or updated
///

private DynamicEntity Target
{
get { return this.Context.InputParameters[ParameterName.Target] as DynamicEntity; }
}
///
/// Reference to the Target Property Collection
///

private PropertyCollection PostImageProperties
{
get
{
return ((DynamicEntity)this.Context.PostEntityImages[ParameterName.Target]).Properties;
}
}
///
/// Concatenates the target fields and returns the a StringProperty
///

///
///
private StringProperty ConcatProperty(TargetField Target)
{
List TargetValues = new List();
StringProperty TargetProperty = new StringProperty();
TargetProperty.Name = Target.ID;

#region Retrieve Each Concatenated Field Value

foreach (KeyValuePair related in Target.Related)
{
String fieldId = related.Key;
String fieldType = related.Value;

#region Get Field Value or Name by Type

PropertyCollection Properties = PostImageProperties;

switch (fieldType)
{
case "String":
TargetValues.Add(Properties[fieldId] + "");
break;
case "CrmNumber":
TargetValues.Add(((CrmNumber)Properties[fieldId]).Value + "");
break;
case "CrmFloat":
TargetValues.Add(((CrmFloat)Properties[fieldId]).Value + "");
break;
case "CrmDecimal":
TargetValues.Add(((CrmDecimal)Properties[fieldId]).Value + "");
break;
case "CrmMoney":
TargetValues.Add(((CrmMoney)Properties[fieldId]).Value + "");
break;
case "Lookup":
TargetValues.Add(((Lookup)Properties[fieldId]).name + "");
break;
case "Owner":
TargetValues.Add(((Owner)Properties[fieldId]).name + "");
break;
case "Customer":
TargetValues.Add(((Customer)Properties[fieldId]).name + "");
break;
}
#endregion
}

#endregion

TargetProperty.Value = String.Format(Target.Format, TargetValues.ToArray());
return TargetProperty;
}
}
}



Following is the plug-in solutionxml. After you construct the plug-in project put the file inside the project debug folder where the plug-in dll resides and import the solution using the registration tool. The solution xml contains step information for the Incident post create and update events as described above.



<Register LogFile="Plug-in Registration Log.txt" Server="http://moss:5555" Org="MicrosoftCRM" Domain="" UserName="administrator">
<Solution SourceType="1" Assembly="GenericConcatPlugIn.dll" Id="fc137b56-936f-4711-852c-d5d4ca508f73">
<PluginTypes>
<Plugin TypeName="GenericConcatPlugIn.Handler" FriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" Id="2545c234-dee8-4a4c-979f-749748358295">
<Steps>
<Step PluginTypeName="GenericConcatPlugIn.Handler" PluginTypeFriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" CustomConfiguration="<Entity> <Target ID="title" Format="{0} - {1}"> <Field ID="subjectid" Type="Lookup"/> <Field ID="customerid" Type="Customer"/> </Target> </Entity> " SecureConfiguration="" Description="Create of incident in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Create" Mode="0" PrimaryEntityName="incident" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="c0fef31b-08fe-dd11-91f6-0003ff2d0264">
<Images>
<Image EntityAlias="Target" ImageType="1" MessagePropertyName="Id" Attributes="" Id="f0f9d623-0bfe-dd11-91f6-0003ff2d0264" />
</Images>
</Step>
<Step PluginTypeName="GenericConcatPlugIn.Handler" PluginTypeFriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" CustomConfiguration="<Entity> <Target ID="title" Format="{0} - {1}"> <Field ID="subjectid" Type="Lookup"/> <Field ID="customerid" Type="Customer"/> </Target> </Entity> " SecureConfiguration="" Description="Update of incident in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Update" Mode="0" PrimaryEntityName="incident" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="c03e4a2e-08fe-dd11-91f6-0003ff2d0264">
<Images>
<Image EntityAlias="Target" ImageType="0" MessagePropertyName="Target" Attributes="" Id="8065c335-08fe-dd11-91f6-0003ff2d0264" />
</Images>
</Step>
</Steps>
</Plugin>
</PluginTypes>
</Solution>
</Register>



Incident Post Create And Post Entity Image




Incident Post Update And Post Entity Image





Good Luck

Tuesday, February 10, 2009

Displaying an Image in an IFRAME


This seems like a very simple requirement. But when it comes to implementation, you might find that fitting this requirement into an already existing functionality such as the CRM the annotations (notes) facility, is not as strait forward as it sounds.

Wanting to display a user avatar, a company logo or even a product image is a very acceptable requirement. And since CRM does not allow such facility out of box, I figured it would be nice to show you how this could be done in just a few minutes.

The simplest solution would be to place all images under the ISV folder then create an IFRAME, where desirable, and set its SRC attribute to a specific URL through script. This also requires the user or application to adhere to a strict naming convention such as a contact FirstName + LastName.gif or an account accountid.jpg or accountnumber.bmp and so forth.

For the sake of discussion the rest of my code bits would refer to the account entity and presenting an account logo inside a dedicated iframe e.g. IFRAME_accountlogo.

Your code might look like the following lines:


var accountLogo = "/isv/images/accounts/missing.gif";
If (crmForm.accountnumber.DataValue != null)
{
accountLogo = "/isv/images/accounts/" + crmForm.accountnumber.DataValue + ".gif";
}
document.all.IFRAME_accountlogo.src = accountLogo;


This process is quite limiting since it requires the involvement and communication with a power user or an administrator who need to put the images in their respective folders. They also need to manage the changes and deletions of images from that folder for obvious reasons. By now this looks like a bad strategy and we need to come up with a better and more manageable solution.

Can I upgrade the above solution? The answer is of course you can! You can automate this type of process by allowing the user to upload the image to a specified location and saving the image name to a new dedicated attribute you set on the entity e.g new_accountimagename.

The client side script might look like the following lines:


var accountLogo = "/isv/images/accounts/missing.gif";
if (crmForm.new_accountimagename.DataValue != null)
{
accountLogo = "/isv/images/accounts/" + crmForm.new_accountimagename.DataValue;
}
document.all.IFRAME_accountlogo.src = accountLogo;


You might argue, and rightly so, that this type of solution does not completely resolve the need for our administrator to manage these folders, and CRM already has an uploading facility (for annotations), so why not use that?!
Obviously this type of solution only answers a partial requirement and we need to come up with a better one yet again.

So how do you take advantage of CRM’s annotation facility? And how can you bind the upload process to an IFRAME?

If you ask a developer, with no relevant knowledge of dynamics, how to do that with asp.net he would probably tell you that you need to read the binaries from the database and render them back to the calling image on the client.

e.g.




Wait a minute, this looks very familiar. And indeed it is! as CRM already uses this type of functionality inside the email entity. When you track an email with an inline image from outlook client CRM saves the inline image as attachment and present it inside the email body.

e.g.


image001.png


So I asked my self why would ms send the attachment entity type if this type of functionality is only for attachments? And as it appears if you change the AttachmentType parameter from 1001 to 5 (annotation object type code) and set the attachmentid to an annotationid you receive the desired results.

e.g.





And your onload script should look like:


var annotationid = getAccountLogoAnnotationId();
var iframeDoc = document.all.IFRAME_accountlogo.contentWindow.document;
var image = iframeDoc.createElement('img');
image.src = prependOrgName("/Activities/Attachment/download.aspx?AttachmentType=5&AttachmentId=") + annotationid;
iframeDoc.body.appendChild(image);


The getAccountLogoAnnotationId function should use a fetchxml to retrieve the annotationid. The best way of doing that is telling the user to upload the image under a well known name and creating a constant fetchxml request as follows.














Is this type of solution consider supported? My first gut feeling is actualy yes. I don’t see why ms would downgrade this type of solution to only support email attachment. The only thing that might change is the actual url. This seems a good enough solution for a 5 minute work.

Eventually you might mimic the entire functionality your self by creating a download.aspx page, reading the image binaries from the filtered annotation view and render the result your self.

For a complete example of how to use an Ajax fetch call to CRM follow this post.