Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

Tuesday, November 13, 2018

How To Fix Email Always Being Dirty

Recently, some customers complained that the email form was always showing as dirty.  Normally this happens when something is triggered post save that makes the form dirty again, and tracking it down can be difficult.  The simplest thing (assuming you have some JS executing on the on save) is to put a break point in the on save, and see what attributes are dirty in the console window:
Xrm.Page.getAttribute().map(function (a) { return a.getIsDirty() + " - " + a.getName(); });
The other option is to look at the requests either in the F12 Developer Tools, or in Fiddler, and see what data is being sent to the server.  In this case though, the only field that was being marked as dirty was the description, but I couldn’t visually see any changes that were occurring.  The description in the email in this case contained some html that was inserted via a signature template, so I decided to see if there was anything in the html that was different, so I edited the JS to include storing the description in a class level variable that then could be used to compare with the current value in an on change event.  Sure enough, there were some differences in the html.  The spaces after semicolons and colons in tags were being removed, as well as quotes around numbers (ie. size=”3” –> size=3).  Finally I also noticed that blank characters were being encoded differently as well (“ ” vs “&nbps;”).  After a good deal of trial and error, I finally came up with this supported solution (Note, this type of solution can be applied to any situation where you want to ignore formatting differences.  Also, this is for an 8.2 CRM instance, so if this issue occurs in the new UI for CRM, you’ll need to access the context in the correct manner): 
var serverEmailDescription = "";
var ignoreDescriptionUpdates = true;
/**
 * When dealing with html in the body, the form will format it differently than the server, resulting in some changes happening post save.
 * This then shows up as the field being dirty, but saving it again, will update the format again, and cause it to still look dirty
 * Fix is to mark it as submitmode = never if it isn't really dirty.  This will prevent the form from looking like it needs to be saved.
 */
function handleDescriptionAlwaysBeingDirty() {
    var description = Xrm.Page.getAttribute("description");
    if (!description) {
        return;
    }
    serverEmailDescription = removeServerDifferentFormatting(description.getValue());
    description.setSubmitMode("never");
    description.addOnChange(submitIfActuallyDirty);
    Xrm.Page.getAttribute("modifiedon").addOnChange(function() {
        serverEmailDescription = removeServerDifferentFormatting(Xrm.Page.getAttribute("description").getValue());
        description.setSubmitMode("never");
        ignoreDescriptionUpdates = true;
        setTimeout(function() { ignoreDescriptionUpdates = false; }, 1);
    });
    setTimeout(function () { ignoreDescriptionUpdates = false; }, 1);
}
 
function removeServerDifferentFormatting(v) {
    // Some Html Tags get surrounded with \"
    // Some spaces are added for ";" and ":"
    // Blank spaces are encoded differently
    return v.replace(newRegExp(": ", "g"), ":")
        .replace(new RegExp("\\\"", "g"), "")
        .replace(new RegExp("; ", "g"), ";")
        .replace(new RegExp(" ", "g"), " ");
}
 
function submitIfActuallyDirty() {
    var att = Xrm.Page.getAttribute("description");
    var description = removeServerDifferentFormatting(att.getValue());
    if (ignoreDescriptionUpdates) {
        serverEmailDescription = description;
        return;
    }
    if ((description === serverEmailDescription) === (att.getSubmitMode() === "dirty")) {
        att.setSubmitMode(description === serverEmailDescription ? "never" : "dirty");
    }
}
The main function is the handleDescriptionAlwaysBeingDirty function, which is called onLoad of the form.  It ensures that the description field is on the form, and then adds an onChange function to the modifiedOn and description attributes.  It also caches the initial value of the description, as well as setting the submit mode to “never”.
The modifiedOn attribute will get updated by the server post save, so the function can be used to store what the value of the description field was just after saving.  Due to the issue of the formatting being different, I first attempt to replace anything that would be different between how the server stores the format, and how the client framework updates it post save, before caching the description. 
Whenever the description is updated, either via a user, or post save, the formatting is normalized to compare it with the normalized cached version to see if it truly has changed.  If it has, the submit mode is changed from “never” to “dirty”.  The submit mode is updated for two reasons:
  1. There is no supported method to update the IsDirty flag.
  2. When an attribute isn’t set to be submitted, the form doesn’t check to see if it’s dirty when showing the “Unsaved Changes” text in the lower right hand corner of the screen.  This solves our infinite loop issue with the description always being updated!
Please note, this is not a perfect solution.  If someone edits the description of the e-mail and adds a space after a colon or a semicolon, the change won’t be registered,  Also the function removeServerDifferentFormatting may not include all of the possible formatting changes.  But it works on my machine and resolves the current issue at hand, and hopefully it is helpful for you as well!

Saturday, May 20, 2017

Making CrmWebApi Entity References Suck Less For Creates and Updates

For those that “grew up” on the 2011 Rest endpoint for CRM, attempting to populate Entity Reference attributes for create or update calls feels rather painful in the new CrmWebApi.
account["primarycontactid@odata.bind"] = "/contacts(E15C03BA-10EC-E511-80E2-C4346BAD87C8)";
Was it “"@odata.bind” or “@bind.odata”? Was it a forward slash or backward slash?  Did the Guid have curly braces?

Yes it’s a small pain, but it is bigger if you normally use field accessors (“entity.field” rather than array accessors: “entity[‘field’]”) because "account.primarycontactid@odata.bind" isn't a valid field name.  It’s probably because of my C# background, but I prefer not to use the object array accessor method when possible.  So the question is, how to make this syntax better and help me remember it.

On my current project I use David Yack’s CRMWebAPI.  It’s simple, and uses standard Promises, so no need for a new library, just polyfill Promises (if you’re using IE 11) and you’re all set.  The calls are wrapped by a custom TypeScript library (CrmWebApiLib) to allow for some custom changes, of which, this implementation is one.  First, the library defines an Entity Reference class (*Note, this is TypeScript, get it, use it, love it)
export class EntityReference implements ODataFormattable {
    constructor(public collectionName: string, public id: string) { }
 
    toODataFormat = (): string => {

        return `/${this.collectionName}(${CrmWebApiLib.removeCurlyBraces(this.id)})`;
    }
 
    getODataPropertyName = (propertyName: string): string => {
        return `${propertyName}@odata.bind`;
    }
}
The class has two public properties, “collectionName” and “id”, and implements the two functions of the ODataFormattable interface, “toODataFromat” and “getODataPropertyName”.  The toODataFormat adds the forward slash and formats the guid correctly, and the getODataPropertyName appends the “@data.bind” to the property name parameter.

The ODataFormattable interface just defines the two functions.  Then there is also a User Defined Type Guard to determine if any given object implements the ODataFormattable interface:
export interface ODataFormattable {
    toODataFormat(): string;
    getODataPropertyName(propertyName: string): string;
}

export function isODataFormattable(arg: any): arg is ODataFormattable {
    const formattable = arg as ODataFormattable;
    return formattable && formattable.toODataFormat !== undefined && formattable.getODataPropertyName !== undefined;
}
This then is all used in the prepareForOData function:
/**
 * Loops through properties, searching for any ODataFormattable properties or arrays with ODataFormattable, and updates the format to be OData Friendly
 * @param data
 */
function prepareForOData(data: any): any {
    const oData = {};
    for (const propName in data) {
        if (!data.hasOwnProperty(propName)) {
            continue;
        }
 
        const value = data[propName];
        if (isODataFormattable(value)) {
            oData[value.getODataPropertyName(propName)] = value.toODataFormat();
        } else if (value instanceof Array) {
            oData[propName] = value.map(prepareForOData);
        } else {
            oData[propName] = value;
        }
    }
    return oData;
}
It creates a new object, and basically loops through all properties of the data object, copying it over to the new object.  If the value of the property is a ODataFormatable, it will update the value as well as the property name.  There is then a recursive map call to handle arrays as well (think party lists).  prepareForOData is then called from within the create and update methods:
export function create(entityCollection: string, data: any): Promise<any> {
    return instance().Create(entityCollection, prepareForOData(data));
}
 
export function update(entityCollection: string, key: string, data: any, upsert?: boolean): Promise<any> {
    if (key.indexOf("{") >= 0 || key.indexOf("}") >= 0) {
        key = CrmWebApiLib.removeCurlyBraces(key);
    }

    return instance().Update(entityCollection, key, prepareForOData(data), upsert);
}
And now, these two calls, will result in the same exact request made to the CrmWebApi:

No Bueno
const note = {};
note["notetext"] = CommonLib.getValue(fields.description);
note["objectid_allgnt_location@odata.bind"] = `/allgnt_locations(${CommonLib.getSelectedLookupId(fields.location)})`;
CrmWebApiLib.create("annotations", note);

Muy Bueno
const note = {
    notetext: CommonLib.getValue(fields.description),
    objectid_allgent_location: new CrmWebApiLib.EntityReference("allgnt_locations", CommonLib.getSelectedLookupId(fields.location))
};
CrmWebApiLib.create("annotations", note);

Monday, July 13, 2015

How To Display A Web Resource In CRM 2015 Without Loosing Column Width

I was recently adding JavaScript to display an iFrame when the fields required by the web resource had been populated on the form.  I noticed that the width of the iFrame was getting squished when it was being displayed:

Default:
Before


After calling setVisible():
After


It took some time stepping through the CRM libraries to figure out what was going on, but I believe it to be a bug within CRM (having the same issue?  Upvote this ticket!)  By default, CRM will attempt to update the CSS Table-Layout of the control when setting it’s visibility.  The fix is unsupported, but resolves the re-sizing issue.  After setting the visibility, check to see if the control is an iFrame, and the visibility is true.  If so, update the css of the parent table to be “fixed” *UPDATE 7/14/2015* Also need to set the height if there is a data-height attribute specified.

if (controlName.indexOf("IFRAME") >= 0 && visibility) {
    // https://connect.microsoft.com/dynamicssuggestions/feedback/details/1541195
    // THIS IS A FIX FOR A BUG WITH CRM WHEN SETTING IFRAMES AS VISIBLE.
    var ctrl = $("#" + controlName + "_d");
    ctrl.parents("table").last().css("table-layout", "fixed");
    var height = ctrl.attr("data-height");
    if (height) {
        // Set the height if defined
        ctrl.css("height", height + "px");
        ctrl.css("padding-bottom", "0");
    }
}

Tuesday, July 16, 2013

Snippet to Create “C# Like” Collapsible Regions in VS2012 Javascript Files

Ever wanted to create a collapsible region within Javascript? If you have the Web Essentials Extension for VS 2012 you already have that feature:
image
But, the one thing that they are missing in the Web Essentials Extension is a snippet to allow you to automatically insert the region syntax.  Just save this text to “C:\Users\$UserName$\Documents\Visual Studio 2012\Code Snippets\JavaScript\My Code Snippets\regions.snippet” on your local computer, and viola!
<CodeSnippet Format="1.1.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>region</Title>
    <Author>Daryl LaBar</Author>
    <Shortcut>region</Shortcut>
    <Description>Code snippet for region in javascript Using Web Essentials 2012</Description>
    <SnippetTypes>
      <SnippetType>Expansion</SnippetType>
      <SnippetType>SurroundsWith</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Declarations>
      <Literal>
        <ID>name</ID>
        <ToolTip>Region name</ToolTip>
        <Default>MyRegion</Default>
      </Literal>
    </Declarations>
    <Code Language="JavaScript">
      <![CDATA[//#region $name$
 
 $selected$$end$
 
//#endregion $name$]]>
    </Code>
  </Snippet>
</CodeSnippet>

Wednesday, July 3, 2013

Making FetchXml in Javascript Less SOAPy and XMLy


Whenever I have to request data from CRM within Javascript, my first attempt is always to use an OData call.  The request urls aren’t too difficult to create (On a side note, there are multiple custom tools for this purpose. I prefer LinqPad.) and the data transferred in the request and the response is extremely tiny.  But… it has some limitations, especially within CRM.  So what do you do when you’ve hit one of the limitations?
  1. Search the internet for “CRM FetchXML from JavaScript
  2. Copy and paste some hard coded SOAP XML from a random post somewhere
  3. Shoehorn your FetchXML into it.
  4. Spend an hour parsing the returned XML with the XML Parser, trying to figure out how many .childNode[i] and .firstChild properties you need to string together to get your actual data.
  5. Feel sorry for the next developer that has to tweak your “ParseResults” code.
Need to do it again 3 months later?  Repeat the process form the top.  It’s one of those processes that makes you amazed there isn’t an easier way.  Well… now there is.

CRM FetchXML Soap Library

In the spirit of “All problems in computer science being solvable by another level of abstraction,” I’ve create a small JavaScript library to abstract away all of the SOAPy, XMLy, uglyness.  Here is an example of it’s use:
// Fetch Xml Call to get all opportunities for TX, with their estimated values
var fetchXml = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">' +
'  <entity name="opportunity">' +
'    <attribute name="estimatedvalue" />' +
'    <order attribute="name" descending="false" />' +
'    <link-entity name="account" from="accountid" to="customerid" alias="ae">' +
'      <filter type="and">' +
'        <condition attribute="address1_stateorprovince" operator="eq" value="TX" />' +
'      </filter>' +
'    </link-entity>' +
'    <link-entity name="account" from="accountid" to="parentaccountid" visible="false" link-type="outer" alias="Account">' +
'      <attribute name="address1_stateorprovince" />' +
'    </link-entity>' +
'  </entity>' +
'</fetch>';

var response = Allegient.Core.SoapLib.executeFetchXmlRequest(fetchXml);


Just give the Library FetchXml, and it’ll give you back a collection of javascript objects that are parsed from the result. Here is what the result looks like in the IE debugger:

IE Debugger Window


Here are a couple things to take note of:
  1. The response returned is just an array of entities.
  2. Each entity has a property that maps to a property of the entity returned.  This allows working with the entities in a way that is much easier than XML parsing.  ie to get the first entity’s name: “response[0].name” or to get the the 2nd entity’s currency’s name: “response[1].transactioncurrencyid.Name”.

The Implementation

There is a lot going on under the covers, so we’ll start at the top with executeFetchXmlRequest and work our way down:

executeFetchXmlRequest: function (fetchXml, onSuccess, onError, async) {

     if (async == null) {
         async = false;
     } 
    var request = '<request i:type="b:RetrieveMultipleRequest" xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
         + '    <b:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">'
         + '        <b:KeyValuePairOfstringanyType>'
         + '            <c:key>Query</c:key>'
         + '            <c:value i:type="b:FetchExpression">'
         + '                <b:Query>' + CrmEncodeDecode.CrmXmlEncode(fetchXml)
         + '                </b:Query>'
         + '            </c:value>'
         + '        </b:KeyValuePairOfstringanyType>'
         + '    </b:Parameters>'
         + '    <b:RequestId i:nil="true"/>'
         + '    <b:RequestName>RetrieveMultiple</b:RequestName>'
         + '</request>';
     var response = null;
     var internalOnSuccess;
     if (onSuccess == null) {
         internalOnSuccess = function (r) { response = Allegient.Core.SoapLib._onFetchXmlSuccess(r); };
     } else {
         internalOnSuccess = function (r) {
             var temp = Allegient.Core.SoapLib._onFetchXmlSuccess(r);
             response = onSuccess(temp);
             if (response == null) {
                 response = temp;
             } 
        }; 
    } 
    Allegient.Core.SoapLib.executeRequest(request, internalOnSuccess, onError, async);
     return response;
},


executeFetchXmlRequest just wraps the FetchXml with the RetrieveMultipleRequest request xml, encoding the FetchXml as well.  Then it sets up some default response handlers, allowing for the request to be made asynchronously.  The next step is the executeRequest:

executeRequest: function (requestXml, onSuccess, onError, async) {

     if (async == null) {
         async = false;
     } 
    var soapEnvelope = ""
    + "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">"
    + "<s:Body>"
    + "   <Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">"
    + requestXml
    + "    </Execute>"
    + "  </s:Body>"
    + "</s:Envelope>";
     var req = Allegient.Core.SoapLib.getXMLHttpRequest();
     req.open("POST", Allegient.Core.SoapLib._getServerUrl(), async)
     // Responses will return XML. It isn't possible to return JSON.
     req.setRequestHeader("Accept", "application/xml, text/xml, */*");
     req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
     req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
     req.onreadystatechange = function () { Allegient.Core.SoapLib.ExecuteSOAPResponse(req, onSuccess, onError); };
     req.send(soapEnvelope);
},


It’s primary purpose is to wrap the CRM Request Xml in a Soap Envelope.  It can be used to wrap any SOAP calls to CRM to help keep them DRY (Included in the library is an ExecuteWorkflowRequest to illustrate how another request type would utilize executeRequest).

It utilizes three helper functions.
  1. getXMLHttpRequest - returns an XMLHttpRequestObject in a browser specific safe manner.
  2. _getServerUrl – returns the URL for the SOAP endpoint using the context information available in the form or HTML Web Resource.
  3. ExecuteSOAPResponse – Handles the callback from the request, determining based on the readyState and status if it executed successfully or with an error, calling the appropriate call back.

The default error callback just displays the error in an alert().  The default success callback as defined by executeFetchXmlRequest is _onFetchXmlSuccess:

_onFetchXmlSuccess: function (response) {
     response = Allegient.Core.SoapLib._replaceAll(response, "<ExecuteResponse ", "<a:ExecuteResponse ");
     response = Allegient.Core.SoapLib._replaceAll(response, "<ExecuteResult ", "<a:ExecuteResult ");
     response = response.replace(/<(\/?)([^:>]*:)?([^>]+)>/g, "<$1$3>");
     var doc = Allegient.Core.SoapLib._parseXmlString(response);
     if (Allegient.Core.SoapLib._getElementText(doc.getElementsByTagName("ResponseName")[0]) == "RetrieveMultiple") {
         var elements = doc.getElementsByTagName("Entity");
         var entities = new Array();
         for (var i = 0; i < elements.length; i++) {
             entities[i] = Allegient.Core.SoapLib.XML2jsobj(elements[i]);
         } 
        return entities;
     } else {
         return doc.getElementsByTagName("Results")[0];
     }
},


There is some RegEx magic to remove the namespace prefixes from the XML so XML2jsobj returns valid property names.  Small side note here, the ExecuteResponse and ExecuteResult are the only xml elements in the response that don’t have a namesapce and my current regex doesn’t handle them correctly, hence the manual addition of the namespace prefix to the tag.  _parseXmlString returns a valid XMLDomParser that has loaded the xml passed in.  A quick walk of the XML Dom to see if this is a RetrieveMultiple Response, and if so, each entity is parsed one at a time by XML2jobj.  If it’s not a RetrieveMultiple response, the DOM results from the response are returned.  XML2jobj is based off of a version by Craig Buckler, but specifically modified for reading CRM attribute data:

XML2jsobj: function (node) {
     var data = {};
     var isNull = true;
     // append a value
     function Add(name, value) {
         isNull = false;
         if (data[name]) {
             if (data[name].constructor != Array) {
                 data[name] = [data[name]];
             } 
            data[name][data[name].length] = value;
         } 
        else {
             data[name] = value;
         } 
    }; 
    function AddAttributes(entity, node) {
         for (var i = 0; i < node.childNodes.length; i++) {
             isNull = false;
             //KeyValuePairOfstringanyType
             var valuePair = node.childNodes[i];
             var key = Allegient.Core.SoapLib._getElementText(valuePair.firstChild);
             var value = valuePair.childNodes[1].childNodes;
             if (value.length == 1 && value[0].tagName == "Value") {
                 data[key] = Allegient.Core.SoapLib._getElementText(value[0]);
             } else if (value.length == 1 && value[0].nodeType == 3) {
                 data[key] = value[0].nodeValue;
             } else {
                 data[key] = Allegient.Core.SoapLib.XML2jsobj(valuePair.childNodes[1]);
             } 
        } 
    }; 
    
     var c, cn;

     // child elements
     for (c = 0; cn = node.childNodes[c]; c++) {
         if (cn.nodeType == 1) {
             if (cn.childNodes.length == 1 && cn.firstChild.nodeType == 3) {
                 // text value
                 Add(cn.nodeName, cn.firstChild.nodeValue);
             } 
            else if (cn.nodeName == "Attributes") {
                 AddAttributes(data, cn)
             } else {
                 // sub-object
                 Add(cn.nodeName, Allegient.Core.SoapLib.XML2jsobj(cn));
             } 
        } 
    } 
    if (isNull) {
         data = null;
     } 
    return data;
},


It loops through the XML DOM, creating a new js object for each node in the xml, with special processing for the attributes collection, resulting in the response looking like the screen shot above. 

Conclusion

The CRM FetchXML SOAP Library is extremely helpful, allowing for just the FetchXML to be passed in (no need to worry about SOAP headers or the wrapping Request XML itself) and a collection of entity JavaScript objects returned.  It should now change your javascript FetchXML steps from what is listed at the top of this article, to this:
  1. Search the internet for “CRM FetchXML from JavaScript”
  2. Download this library
  3. Never repeat steps 1-2 again
  4. Use your FetchXML as is, calling executeFetchXmlRequest
  5. Easily access the data you need in the response
  6. Go home early, and sleep better at night*

*Step 6 is not required but highly likely ;)


Click here to download the source code.