The CRM Grid – A Microsoft CRM Blog

Greg Owens’ rose-tinted MS-CRM mumblings

Archive for Plug-In

Disabled fields incorrectly saving to the CRM database

Earlier this week, I encountered what seems to be a bug in MS CRM 4.0 which I had not noticed before. This occurred with Update Rollup 13 and Update Rollup 14 (and possibly others…?) and has been replicated on at least one other unrelated environment. (Edit: but not replicable on all environments…)

The problem manifests itself with fields marked on the CRM Form Designer as “readonly” being incorrectly saved back to the database, during the create stage. Strangely on affected machines I was unable to replicate this problem when updating records (which behaved as expected).

According to the CRM 4.0 SDK, readonly/disabled fields are not submitted to the server during the save process. After all, that’s what the ForceSubmit method is for (my emphasis):

All enabled fields will be submitted after they have been modified either through script or by user input.

Contrary to this, on creation of a record, data in read-only fields is currently being submitted and saved to the database. I haven’t identified why this occurs on some deployments and not on others. Details of the main environment that I have encountered this on are listed at the end of this posting. To test your environment, try following these steps:

  • Create a new entity, “new_test”
  • Create a new string attribute, “”new_editablestring”
  • Create a new string attribute, “”new_readonlystring”
  • Place both fields onto the form
  • Mark “new_readonlystring” as Read-Only
  • Enable the “onchange” event for “new_editablestring”
  • Add the following script to the onchange event handler
    crmForm.all.new_readonlystring.DataValue = crmForm.all.new_editablestring.DataValue;
  • Save & publish the form
  • Create a new_test record
  • Enter a value in the Editable String field, e.g. “Creation String”
  • Note that the ReadOnly string value is correctly changed on screen
  • Hit save & close
  • Open the record again, note that the “new_readonlystring” now contains a value “Creation String”
  • Enter a new value in the Editable String field, e.g. “Edited String”
  • Note that the ReadOnly string value is correctly changed on screen to “Edited String”
  • Hit save & close
  • Open the record again, note that the “new_readonlystring” still contains a value “Creation String” as it was correctly NOT updated

After some testing, it became clear that no permutation of programatically setting the field to Disabled, disabled, readOnly, ReadOnly or readonly made any difference. The only answer, it seems, is to prevent read-only attributes from hitting the database via a plugin. This is simple enough – the Create message accepts a Target input parameter which is a DynamicEntity. We can simply remove the unwanted properties from the DynamicEntity before the Target gets commited. This is difficult to manage though – every time you make a field read only on your form, you would need to recompile your code, or update your plug-in configuration strings (depending on how you chose to handle this). There is however a better solution – retrieve the form definition from the CRM platform, identify which fields should be read-only and omit them dynamically from the input target. And here is how this is done:

using System;
using System.Xml;
using Vitalogy.XRM.Common;
using Vitalogy.XRM.Plugins.BaseClass;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;

namespace Vitalogy.XRM.Plugins
{
public class AllEntitiesCreate : VitalogyBasePluginClass, IPlugin
{
///
/// Identifies read-only fields from the form and ensures that they
/// are not saved to the database (this shouldn't happen anyway
/// but there appears to be a bug in the CRM platform)
///
/// <param name="context" />
public void Execute(IPluginExecutionContext context)
{
if (!PluginShouldRun(context))
{
DebugLog.LogMessage(string.Format("Plug-In \"{0}\" is not designed to run in this scenario", GetType().Name));
return;
}

DebugLog.LogStart();
try
{
var entity = (DynamicEntity)context.InputParameters.Properties[ParameterName.Target];

// Filter out read-only attributes from the create message
FilterReadOnlyAttributes(context, entity);

}
catch (System.Web.Services.Protocols.SoapException ex)
{
DebugLog.LogError(string.Format("A SOAP error occurred in the {0} plug-in.\n\n{1}", GetType(), ex.Detail), ex);
throw new InvalidPluginExecutionException(
String.Format("A SOAP error occurred in the {0} plug-in.", GetType()), ex);
}
catch (Exception ex)
{
DebugLog.LogError(string.Format("A general exception occurred in the {0} plug-in.", GetType()), ex);
throw new InvalidPluginExecutionException(
String.Format("A general exception occurred in the {0} plug-in.", GetType()), ex);
}
finally
{
DebugLog.LogFinish();
}
}

private static void FilterReadOnlyAttributes(IPluginExecutionContext context, DynamicEntity entity)
{
using (var crmService = context.CreateCrmService(true))
{
DebugLog.LogMessage("Plug-in to strip out read-only fields during record creation");
// get formxml
DebugLog.LogMessage(string.Format("Getting formXml for entity {0}", entity.Name));
// RetrieveFormXmlRequest is unsupported since it is for "internal use
// only" but the alternative involves getting the entity type code
// from metadataservice then issuing a request for OrganisationUI
// for the given entity. This too is deprecated due to not being
// supported in CRM 2011 - but what are we to do, eh?
var formXmlRequest = new RetrieveFormXmlRequest {EntityName = entity.Name};
var formXmlResponse = (RetrieveFormXmlResponse) crmService.Execute(formXmlRequest);
var formXml = formXmlResponse.FormXml;
DebugLog.LogMessage(string.Format("Retrieved formXml: {0}", formXml));

// xpath to identify read-only fields
var formXmlDocument = new XmlDocument();
formXmlDocument.LoadXml(formXml);
var disabledAttributes = formXmlDocument.SelectNodes("//control[@disabled='true']/@id");

// pluck them out of the create message
if (disabledAttributes == null)
{
DebugLog.LogMessage("No read-only attributes found");
return;
}

DebugLog.LogMessage(string.Format("Retrieved {0} read-only attributes", disabledAttributes.Count));
foreach (XmlNode disabledAttribute in disabledAttributes)
{
DebugLog.LogMessage(string.Format("{0} is read-only", disabledAttribute.Value));
if (entity.Properties.Contains(disabledAttribute.Value))
{
DebugLog.LogMessage("and has been removed from the Target input parameter");
entity.Properties.Remove(disabledAttribute.Value);
}
}
context.InputParameters[ParameterName.Target] = entity;
}
}

private static bool PluginShouldRun(IPluginExecutionContext context)
{
// only run if in the pre-Create stage
if (context.Stage == MessageProcessingStage.BeforeMainOperationOutsideTransaction)
{
if (context.InputParameters.Properties.Contains(ParameterName.Target) &&
(context.InputParameters.Properties[ParameterName.Target] is DynamicEntity))
{
return context.MessageName == MessageName.Create;
}
return false;
}
return false;
}

}
}

A few things to note:

  • Vitalogy.XRM.Common and Vitalogy.XRM.Plugins.BaseClass are simply used to implement the log4net logging framework (and hence define DebugLog).
  • This implementation is very simplistic so isn’t exactly efficient – the next step would be to cache the list of read-only fields so that it is not being read at every Create
  • This code can only deal with fields defined as “ReadOnly” in the form definition. If you have client-side onLoad script which disables fields onLoad this code will not identify those fields (though if you’ve coded consistently, you might be able to examine your client-side code within the formXml to find these cases…).

My environment

I encountered the aforementioned issue in the following conditions:

  • Combined Application and Database server
  • Microsoft Dynamics CRM 4.0 – Update Rollup 14
  • Microsoft Windows Server 2008 R2 Standard
  • Microsoft SQL Server 2008 SP1 (64 Bit)
  • Various clients, including:
    • Internet Explorer 7 (7.0.5730.13) from client
    • Internet Explorer 8 (8.0.7600.16385) on server
  • When signed in as a CRM Administrator or non-administrative user