Friday, July 11, 2008

Statera's New Blog

Well, what started as an "unauthorized" effort to create a new blog has now turned into an "authorized" phenomenon at Statera, the consulting company we enthusiastically work for. So I have moved all the CRM Junkie posts to our new Blog (http://stateracrm.blogspot.com) and this will be the last post to this blog.

Thursday, July 10, 2008

CRM Webservices and .Net 3.5

Now, I haven't done a lot of experimenting on this but I ran into a bit of a pain today.

I typically stick to doing development against the provided .dlls and not by adding web services. It poses it's own problems but also has it's advantages (for example, you don't actually have to have a CRM instance going to do CRM development. Nice!).

I find that the MetadataService in the SDK dll's is sorely lacking (and in many ways completely non-functional) so I had to add it in as a webservice. However, with VS 2008 and .Net 3.5, the services weren't adding in correctly. For example, I didn't get a MetadataService proxy class. Kinda important, you'd think...

Thank God for the "Advanced" button and being able to add a .Net 2.0 style web service in. Problem solved!

Wednesday, July 9, 2008

CRM Properties and Values: More About "Equals"

As you can see from a previous post, I'm not real happy with the DynamicEntity class and what I consider an improper implementations of the Equals method on it. I attempted to fix this.

If you take a close look at the code I provided there was a second little trick in there I didn't touch on. I'm touching it now.

Assume this:

CrmBoolean leftSide = new CrmBoolean(true);
CrmBoolean rightSide = new CrmBoolean(true);

Assert.IsTrue(leftSide == rightSide);
Assert.IsTrue(leftSide.Equals(rightSide));
Can you guess what will happen with those assertions?! Yup, they fail.

GRRR! Why?!

Now, admittedly this is a little grayer of a subject. Conceptually speaking there is no way to tell that, from a business logic standpoint, that those should equal. Why do I say that?

Well, what if leftSide is actually a CrmBoolean from the account object and rightSide is a CrmBoolean from the contact record. Now, even though they are both true they don't carry the same meaning.

Ok, ok, I'll admit, that's pretty flimsy. I'm just trying to give MS the benefit of the doubt here on why they didn't do a data comparison here.

In the previous post the code has to compare these types of objects to see if they're equal. It does it by using reflection on all public properties. This code was originally part of a general CRM Utility class I'm working on but I refactored it into the object itself. But that's neither here nor there.

Let's assume for a moment (I love assumptions) that we actually want these properties to reflect a more realistic "Equals" operation.

We'll start by creating a new object that inherits from CrmBoolean:

public class CRMBoolean : Microsoft.Crm.Sdk.CrmBoolean { }
Just like before, we'll override the Equals method (and some related methods) so that we do a data comparison and not just a pointer comparison:

public CRMBoolean(bool value) : base(value)
{
}

public CRMBoolean()
{
}

public override bool Equals(object obj)
{
if (obj == null)
return false;

if (obj.GetType() != typeof(CrmBoolean) && obj.GetType() != typeof(CRMBoolean))
return false;
if (GetType() == typeof (string))
return this == obj;

PropertyInfo[] properties = GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo property in properties)
{
object leftValue = property.GetValue(this, null);
object rightValue = property.GetValue(obj, null);
if (leftValue == null && rightValue != null)
return false;
if (leftValue != null && !leftValue.Equals(rightValue))
return false;
}
return true;
}

public bool Equals(CRMBoolean obj)
{
return base.Equals(obj);
}


public override int GetHashCode()
{
return base.GetHashCode();
}

public static bool operator ==(CRMBoolean left,
CRMBoolean right)
{
return Equals(left, right);
}

public static bool operator ==(CRMBoolean left,
CrmBoolean right)
{
return Equals(left, right);
}

public static bool operator !=(CRMBoolean left,
CrmBoolean right)
{
return !(left == right);
}

public static bool operator !=(CRMBoolean left,
CRMBoolean right)
{
return !Equals(left, right);
}
However, here we have a slightly unfun task ahead of us. Namely, we have to override EVERY "CrmSomething" class in the library.

I'm not going to cover how to do this, but I'm intending to include these types of objects in the future library I'm working on (wow, two posts now mentioning this, I sure better produce something!). I'll likely generate them.

Once all the various "value" type objects in the CRM libraries are overridden with this "better" Equals method, then I'll no longer need to do that heavy-lifting comparison in the DynamicEntity's Equals method, which is always a bonus.

=-}

P.S. Still not caring too much about the formatting of the code. Sorry. Complain and I might fix it.

CRM DynamicEntity Limitations - Part 1

DynamicEntities in CRM are either a love or hate relationship. Either you love the flexibility of using them within code (no need to do WebService proxy updates as configuration changes are made in CRM) or you hate them for their lack of usability.

What do I mean by their lack of usability? Namely this:

Microsoft.Crm.Sdk.DynamicEntity leftSide = new Microsoft.Crm.Sdk.DynamicEntity("account");
Microsoft.Crm.Sdk.DynamicEntity rightSide = new Microsoft.Crm.Sdk.DynamicEntity("account");

leftSide.Properties.Add(new StringProperty("name", "Fabrikam"));
rightSide.Properties.Add(new StringProperty("name", "Fabrikam"));

Assert.IsTrue(leftSide == rightSide); // this will fail every time
Assert.IsTrue(leftSide.Equals(rightSide)); // this will fail every time too
I find that incredibly annoying. I love the TDD style to coding and not having these two objects do a data style comparison kills me. I have rarely run into a need to do a reference comparison and if so, I'll explicitly call a ReferenceEquals. To me. "Equals" is always a data-comparison. I mean, if you do "test" == "test" you'll get True, right?

Now, I could always create routines that will compare DynamicEntity classes (data wise) during my unit testing, but then every time I have some comparison of objects I have to create a new matcher. I tend to use NMock and NUnit which means I've got two different libraries to work with and about 3-4 different types of matching interfaces I have to implement. Why would I want to do that!?

Now, I could go off on MS for this sorely lacking detail but I'll refrain. That is better left for a well thought-out and tested post going over ALL the pain points! =-}

Here's my solution (which will hopefully make it up on CodePlex at some point as part of a general "Better CRM Library" project.

I first take and create my own DynamicEntity class that inherits from MS's DynamicEntity:

public class DynamicEntity : Microsoft.Crm.Sdk.DynamicEntity {}

Now let's get to some overriding! Now, I wouldn't say this code has been thoroughly unit tested or integration tested, so use at your own risk. It is only meant to point you in the right direction for your own implementations:

public class DynamicEntity : Microsoft.Crm.Sdk.DynamicEntity
{
public DynamicEntity(string name) : base(name)
{

}

public DynamicEntity()
{

}

public override bool Equals(object obj)
{
if (!(obj is Microsoft.Crm.Sdk.DynamicEntity || obj is DynamicEntity))
{
return false;
}
var rightSide = (DynamicEntity) obj;
if (Name != rightSide.Name)
return false;
foreach (Property property in Properties)
{
if (!rightSide.Properties.Contains(property.Name))
return false;
if (!PropertiesEqual(Properties[property.Name], rightSide[property.Name]))
{
return false;
}
}
return true;
}

public static bool operator ==(DynamicEntity left,
DynamicEntity right)
{
if (Equals(left, null) && Equals(right, null))
{
return true;
}
if (Equals(left, null))
{
return false;
}
return left.Equals(right);
}

public static bool operator !=(DynamicEntity left,
DynamicEntity right)
{
return !(left == right);
}

public bool Equals(DynamicEntity obj)
{
return Equals(this, obj);
}

public override int GetHashCode()
{
int runningTotal = 0;
runningTotal += Name.GetHashCode();
foreach (Property p in Properties)
runningTotal += p.GetHashCode();
return runningTotal;
}

public static bool PropertiesEqual(object leftSide,
object rightSide)
{
if (leftSide == null && rightSide == null)
return true;
if (leftSide == null)
return false;

if (leftSide.GetType() != rightSide.GetType())
return false;
if (leftSide.GetType() == typeof (string))
return leftSide == rightSide;

PropertyInfo[] properties = leftSide.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo property in properties)
{
object leftValue = property.GetValue(leftSide, null);
object rightValue = property.GetValue(rightSide, null);
if (leftValue == null && rightValue != null)
return false;
if (leftValue != null && !leftValue.Equals(rightValue))
return false;
}
return true;
}
}
I haven't included the unit tests around this and I'm sure they don't have full code coverage anyway. But I want to get this out there.

This will hopefully be only one part of a whole series of these types of posts where I continue to expand and show how to handle some of the frustrating limitations of the CRM API. Let's just hope I've got the bench-time to get all them out.

=-}

P.S. Forgive me for the poor formatting of the code.

Thursday, July 3, 2008

CRM Email Router Event Log

One of our IT Administrators found out that when the CRM 4.0 Email Router is running it will log an event in the server Application Event log each time an email is filtered. There is a fix for this that can be found at the following URL. I have also copied the specific section text below:

http://support.microsoft.com/kb/907490





Microsoft Dynamics CRM 4.0 E-mail Router
You can update the Microsoft Dynamics CRM E-mail Router to enable tracing. To do this, you must modify the registry and XML service configurations.To enable verbose operations log level, you must manually update the configuration by updating the registry. To do this, follow these steps:
1. Log on to the server where the Microsoft CRM E-mail Router is installed with local administrator privilege.
2. Click Start, click Run, type regedit, and then click OK.
3. Locate and then click the following registry subkey: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MSCRMEmail
4. Right-click the LogLevel registry entry, click Modify, and then change the value to 3. By default, this value is set to 1.
5. Update the Microsoft.Crm.Tools.EmailAgent.xml configuration file for the E-mail Router Service. To do this, follow these steps:
a. In Windows Explorer, locate the Microsoft.Crm.Tools.EmailAgent.xml file. By default, this file is located in the following folder:
SystemDrive:\Program Files\Microsoft CRM Email\Service
b. Open the file by using Notepad or another text-editing program.
c. In the file, examine the <systemconfiguration> node, and then scroll down the text toward the end of the file to find the following statement:
<loglevel>1</loglevel>
By default, this value is set to 1 (one). Modify this statement so that it reads as follows:
<loglevel>3</loglevel>
d. Save the file.
e. Restart the E-mail Router Service. You will find a new event viewer log view that is named MSCRMEmailLog. It is in this event viewer log view that the events appear.


Tuesday, June 17, 2008

CRM 4.0 Gotchas

After implementing a bunch of CRM 4.0 projects I have compiled a short list of "gotchas" that I always share with people as I am gathering requirements for the project. Some of these were around in v3.0 but were not rectified in v.4.0. If anyone has additional ones that they would like to add in the comments I will update this posting.
  • Service Activities have a max length of 3 days (cannot be changed)
  • Bug in WF/Customization: If you customize an entity, all WFs associated are "soft" unpublished. If you change a WF and re-publish, JS on that entity is disabled.
  • If you restrict Write access to an Account, Opportunities, Contacts, Cases associated to that Account cannot be created. One possible solution is to use Teams.
  • The KB and Queue views cannot be modified (use c360 My Workplace instead)
  • You cannot access related data from the Recipient of a Phone Call activity in workflow.
  • Contacts and Contract Lines cannot be removed from the Case form. They must be hidden with JavaScript.
  • It takes 15mins for a published KB article to be indexed and searchable in the Knowledge Base.
  • You cannot delete a KB template unless all articles using it are deleted.
  • In order for an Opportunity to show in the Pipeline report it must have a revenue value entered.

Monday, June 16, 2008

Field Level Security Function

Because of a security limitation with CRM v.4.0 we had to implement an onLoad function that locked form fields based on a user's security role. To detail the issue, CRM does not allow you to restrict write access to the Account while allowing write access to related entities, mainly Opportunities and Contacts. So if you want a specific user role to be able to edit Contacts and not Accounts you have to lock the Account form using JavaScript. I found this code below on forum (sadly I didn't save the original location so if it is yours - give yourself a shout-out).

//PUT THIS CODE IN THE ONLOAD
if(UserHasRole("System Administrator"))
{
var oField1 = crmForm.all.long_taxscheduleid;
oField1.Disabled = !oField1.Disabled;
}


//THIS CODE CHECKS FOR A SPECIFIC SECURITY ROLE AND ALLOW FOR FORM EDITING
//alert(UserHasRole("System Administrator"));

function UserHasRole(roleName)
{
//get Current User Roles, oXml is an object
var oXml = GetCurrentUserRoles();

if(oXml != null)
{
//select the node text
var roles = oXml.selectNodes("//BusinessEntity/q1:name");

if(roles != null)
{

for( i = 0; i < text ="="" xml = "" version="\" encoding="\">" +
"
" +
GenerateAuthenticationHeader() +
"
" +
" " +
" " +
" role" +
"
" +
"
" +
" name" +
"
" +
"
" +
" false" +
"
" +
"
" +
" roleid" +
" role" +
" systemuserroles" +
" roleid" +
" Inner" +
"
" +
"
" +
" systemuserid" +
" systemuserroles" +
" systemuser" +
" systemuserid" +
" Inner" +
"
" +
" And" +
"
" +
"
" +
" systemuserid" +
" EqualUserId" +
"
" +
"
" +
"
" +
"
" +
"
" +
"
" +
"
" +
" " +
" " +
"
" +
"
" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction"," http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");

xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);

var resultXml = xmlHttpRequest.responseXML;
return(resultXml);
}