Pages

Sunday, October 31, 2010

Tito-Z’s Iframe-embedded Image Code

[Full Disclosure: The following, true story happened within the CRM Development forums.]

With blessings of the author, Tito-Z, whose real name I don’t know, I’d like to present his code for taking an image file attachment from an Annotation record and displaying it within an Iframe dynamically.  Since I helped him locate the solutions that he used in his code, Tito-Z graciously granted me permission to reproduce his work here.

Originally, Tito-Z started with Adi Katz’ code.  However, he soon discovered that Update Rollup 7 (and later) thwarted its use.  Unfortunately, the work-around left him with a file-stream and not a valid URL to place in a src attribute for an image element.  When I did a little searching, I found some resources that showed how to assemble an inline-data URI.

I then left Tito-Z to do the dirty work—not my usual style, but since he had demonstrated proficiency in CRM-hacking, I figured the project was in good hands.  He did not disappoint.  The following, unaltered code is his example for reproducing an image within a form’s Iframe, after retrieving the image data from an Annotation (file attachment) record:

[Caveat:  The following code works only for IE8, as earlier versions of IE do not support the necessary inline-data URIs employed by the code.]

// *********************************************************
// To fetch the picture file from the Notes Entity 
// *********************************************************

var xml = "" +
"<?xml version='1.0' encoding='utf-8'?>" +
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'>" +
GenerateAuthenticationHeader() +
"<soap:Body>" +
"<Fetch xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>" +
"<fetchXml>" +
"&lt;fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'&gt;"+
"&lt;entity name='annotation'&gt;"+
"&lt;attribute name='filename'/&gt;"+
"&lt;attribute name='documentbody'/&gt;"+
"&lt;attribute name='annotationid'/&gt;"+
"&lt;order attribute='subject' descending='false'/&gt;"+
"&lt;filter type='and'&gt;"+
"&lt;condition attribute='isdocument' operator='eq' value='1'/&gt;"+
"&lt;condition attribute='filename' operator='eq' value='mex9.jpg'/&gt;"+
"&lt;/filter&gt;"+
"&lt;/entity&gt;"+
"&lt;/fetch&gt;"+
" </fetchXml>" +
" </Fetch>" +
" </soap:Body>" +
"</soap:Envelope>";

// Prepare the xmlHttpObject and send the request.
var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xHReq.setRequestHeader("Content-Length", xml.length);
xHReq.send(xml);

// Capture the result.
var resultXml = xHReq.responseXML;

// Check for errors.
var errorCount = resultXml.selectNodes('//error').length;
if (errorCount != 0)
{
 var msg = resultXml.selectSingleNode('//description').nodeTypedValue;
 alert(msg);
}

// Process and display the results.
else
{

// Capture the result and UnEncode it.
var resultSet = new String();
resultSet = resultXml.text;
resultSet.replace('&lt;','<');
resultSet.replace('&gt;','>');

 


// Create an XML document that you can parse.
   var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
   oXmlDoc.async = false;
// Load the XML document that has the UnEncoded results.
   oXmlDoc.loadXML(resultSet);

 

// Display the results.
var results = oXmlDoc.getElementsByTagName('result');

for (i=0;i < results.length;i++)
    {var docBody= results[i].selectSingleNode('./documentbody').nodeTypedValue;}


crmForm.all.IFRAME_Picture.src="";

// BE CAREFULL WITH THE QUOTES

var image = "<img alt='', src= 'data:image/jpeg;base64," + docBody + "'> </img>";

ff = document.getElementById("IFRAME_Picture");

ff.contentWindow.document.write(image);

}

Thank you, Tito-Z, for your success in this endeavor and for allowing me to host it here, on my blog.

Thursday, October 28, 2010

Dealing with Automatic Draft Contract Expiration

I’m not sure how much of this situation Microsoft is aware, but I have long known about a behavior of the “Update Contract States” job that only recently became a problem for me and my employer:

Contracts and Contract Lines in Draft state will automatically expire when the “End Date” occurs in the past.

You read that right:  Draft state.  I could speculate as to why the system behaves this way, but for now I’m just going to assume it’s a bug and soon write-up a report back to Microsoft about it.  However, I cannot wait for them to address it, and must stop our Draft Contracts from entering into an Expired state.  Here’s a few business-cases in which I find this behavior obstructive:

Entering historical data:

Obviously, I ultimately want historical data regarding Contracts to ultimately progress to the Expired state.  Unfortunately, because the job runs once a day, at 3pm, it’s become a race to finalize the record and make sure it is perfect before the arbitrary deadline smacks the record with the Expired-stick.  Sure, I could alter the frequency, or probably even the execution time of the job—but those efforts neither eliminate the deadline, nor treat the desirable state transitions acceptably.

Using Drafts to produce a history for “lost” service opportunities:

Call me crazy, but I find that the Contract deals with certain things better than a Quote does, so my first task in providing a comprehensive Sales quoting utility was to establish a custom relationship between the Opportunity and the Contract entities.  It was either that, or copying the Contract’s mechanisms within the Quote and Quote Product (and I am loathe to duplicate functionality or reinvent wheels).

We determined at the time that when an Opportunity was lost, we would retain the Draft Contracts as a historical accounting of the various service options that had been quoted to the customer.  The problem is that now the “End Dates” on some of these early records are starting to fall into the past, and the records appear to be “Expired” along with any other legitimately historical record.  Ideally, I’d love to have an alternate state to use for these records, but I haven’t had the time to bother with approaching that situation—instead preferring to leave them in a “Draft” state indefinitely.

Conclusion:

To preserve the accuracy of the representations made by the state of a Contract record, I need a way to prevent “Drafts” from progressing automatically into the “Expired” state.  The obvious, and easy choice, is to go with a Plug-in that attaches to the SetState and SetStateDynamicEntity messages for both the Contract and Contract Line entities.

I use two code files to define state-change handlers for each entity, and a register.xml file to deploy the Plug-in with the CRM Plug-in Developer Tool.  This is helpful because there are Pre-stage entity images that get used by the code to identify the previous state of the record (I don’t want to accidentally block the progress of a traditionally-Invoiced Contract).

Here are the code files in use for my CrmFixes project:

ContractSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contract.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Not a Contract instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContract"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing PreSetStateContract image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContract = (DynamicEntity)context.PreEntityImages["PreSetStateContract"];

        if (!preSetStateContract.Properties.Contains("statecode"))
          throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Missing statecode in image.");

        String oldStateCode = (String)preSetStateContract.Properties["statecode"];

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && oldStateCode == "Draft")
          throw new InvalidPluginExecutionException("Draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
ContractDetailSetStateHandler.cs:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace CrmFixes
{
  /// <summary>
  /// Executes before the SetState or SetStateDynamicEntity of a Contract in CRM
  /// </summary>
  public class ContractDetailSetStateHandler : IPlugin
  {
    #region IPlugin Members

    /// <summary>
    /// Checks the advancement of state for the Contract record, and throws an exception to
    /// prevent the automatic advancement of a Contract in Draft status to Expired status.
    /// </summary>
    /// <param name="context">Context during execution of CRM Plugin</param>
    public void Execute(IPluginExecutionContext context)
    {
      // Check the InputParameters for an EntityMoniker instance to work with; validate type
      if (context.InputParameters.Contains("EntityMoniker") && context.InputParameters["EntityMoniker"] is Moniker)
      {
        Moniker contractInfo = (Moniker)context.InputParameters["EntityMoniker"];

        // Verify entity is a Contract instance
        if (contractInfo.Name != EntityName.contractdetail.ToString())
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Not a Contract Detail instance.");

        if (!context.InputParameters.Contains("state"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing state in context.");

        if (!context.PreEntityImages.Contains("PreSetStateContractDetail"))
          throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Missing PreSetStateContractDetail image.");

        String newStateCode = (String)context.InputParameters["state"];
        DynamicEntity preSetStateContractDetail = (DynamicEntity)context.PreEntityImages["PreSetStateContractDetail"];

        Lookup contractId = (Lookup)preSetStateContractDetail.Properties["contractid"];

        ICrmService crmService = context.CreateCrmService(false);

        contract contract = (contract)crmService.Retrieve(EntityName.contract.ToString(), contractId.Value, 
          new ColumnSet(new string[] { "contractid", "statecode" }));

        // Make sure we only care about a transition from Draft to Expired
        if (newStateCode == "Expired" && contract.statecode.Value == ContractState.Draft)
          throw new InvalidPluginExecutionException("Contract Details on draft Contracts are not allowed to automatically expire.");
      }
      else
        throw new InvalidPluginExecutionException("Failure in ContractDetailSetStateHandler: Expected EntityMoniker unavailable.");
    }

    #endregion
  }
}
register.xml:
<?xml version="1.0" encoding="utf-8" ?>
<!-- Input file for the PluginDeveloper tool that provides the configuration for
     registering a workflow activity or plug-in. -->

<Register
    LogFile = "Plug-in Registration Log.txt"
    Server  = "http://crm"
    Org     = "Org"
    Domain  = "domain"
    UserName= "user" >
  <Solution SourceType="0" Assembly="path-to\CrmFixes.dll">
    <Steps>
      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Expiration Handler"
        PluginTypeName = "CrmFixes.ContractSetStateHandler"
        PrimaryEntityName = "contract"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContract" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractid,statecode" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetState Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetState"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>

      <Step
        CustomConfiguration = ""
        Description = "Draft Contract Detail expiration denial : SetStateDynamicEntity Message"
        FilteringAttributes = ""
        ImpersonatingUserId = ""
        InvocationSource = "0"
        MessageName = "SetStateDynamicEntity"
        Mode = "0"
        PluginTypeFriendlyName = "Contract Detail Expiration Handler"
        PluginTypeName = "CrmFixes.ContractDetailSetStateHandler"
        PrimaryEntityName = "contractdetail"
        SecondaryEntityName = ""
        Stage = "10"
        SupportedDeployment = "0" >

        <Images>
          <Image EntityAlias="PreSetStateContractDetail" ImageType="0" MessagePropertyName="EntityMoniker"
                 Attributes="contractdetailid,contractid" />
        </Images>
      </Step>
    </Steps>
  </Solution>
</Register>

Wednesday, October 6, 2010

Disable the Links in a Lookup Field

[UPDATE: Thanks to Mad Computerist for his recommendations for script additions to remove hyperlink formatting!]

Sometimes simply making a Lookup field “read only” isn’t enough.  Sometimes, you may want to restrict the ability to even follow the links held inside the field.  Though it may be trivial to work around such a limitation, sometimes a dead-bolt keeps people out of your house by virtue of being a dead-bolt.  That said, here’s a handy function I whipped up to meet such a need:

function DisableLookupLinks(lookupFieldName) {
 var lookupParentNode = document.getElementById(lookupFieldName + "_d");
 var lookupSpanNodes = lookupParentNode.getElementsByTagName("SPAN");

 for (var spanIndex = 0; spanIndex < lookupSpanNodes.length; spanIndex ++) {
  var currentSpan = lookupSpanNodes[spanIndex];

  // Hide the hyperlink formatting
  currentSpan.style.textDecoration = "none";
  currentSpan.style.color = "#000000";

  // Revoke click functionality
  currentSpan.onclick = function() {};
 }
}

To disable a field's Lookup links, pass the field's schema name into the function.  For example, for the "Customer" field on a Case:

DisableLookupLinks("customerid");

The code also conveniently works for multi-record Lookup fields, such as party-lists on activity records.