Wednesday, October 8, 2008

Objectizing Ajax (XML) results





Definition: Objectize - To make an object of; to regard as an object; to place in the position of an object.


Objectizing XML documents is a very common concept incorporated into dot.net server side framework. Whether you’re creating a web service proxy or desterilizing objects what you’re doing is objectizing xml. Wouldn’t it be the coolest thing if you could do the same on the client side, regard xml results coming back from Ajax requests as objects and manipulating then with ease. So instead on dealing with Xml DOM and writing code that looks like this:


Root.childNodes[0].childNodes[0].firstChild.attributes[“name”]


You can actually write:


Root.resultset.result.businessunitid.name


While creating my own objectizer I came across Yusuke Kawasaki ObjTree.js free class library and decided to check it out.
I modified it slightly to support ms resulting xml schema and made it a bit leaner to so only the objectizing part is included.


The OnCrmPageLoad includes a very simple example of how to create the ObjTree object, passing it an xml document and receiving an object representation of that document.The post uses the “Ajax using Fetch Message” to demonstrate the usage.

Part of Yusuke Kawasaki ObjTree.js Library


if ( typeof(XML) == 'undefined' ) XML = function() {};

// constructor

XML.ObjTree = function () {
return this;
};

// object prototype
XML.ObjTree.prototype.xmlDecl = '<?xml version="1.0" encoding="UTF-8" ?>\n';
XML.ObjTree.prototype.attr_prefix = '';

// method: parseXML( xmlsource )
XML.ObjTree.prototype.parseXML = function ( xml ) {
var root;
if ( window.ActiveXObject ) {
xmldom = new ActiveXObject('Microsoft.XMLDOM');
xmldom.async = false;
xmldom.loadXML( xml );
root = xmldom.documentElement;
}
if ( ! root ) return;
return this.parseDOM( root );
};

// method: parseDOM( documentroot )
XML.ObjTree.prototype.parseDOM = function ( root ) {
if ( ! root ) return;

this.__force_array = {};
if ( this.force_array ) {
for( var i=0; i<this.force_array.length; i++ ) {
this.__force_array[this.force_array[i]] = 1;
}
}

var json = this.parseElement( root ); // parse root node
if ( this.__force_array[root.nodeName] ) {
json = [ json ];
}
if ( root.nodeType != 11 ) { // DOCUMENT_FRAGMENT_NODE
var tmp = {};
tmp[root.nodeName] = json; // root nodeName
json = tmp;
}
return json;
};

// method: parseElement( element )
XML.ObjTree.prototype.parseElement = function ( elem ) {
// COMMENT_NODE
if ( elem.nodeType == 7 ) {
return;
}

// TEXT_NODE CDATA_SECTION_NODE
if ( elem.nodeType == 3 || elem.nodeType == 4 ) {
var bool = elem.nodeValue.match( /[^\x00-\x20]/ );
if ( bool == null ) return; // ignore white spaces
return elem.nodeValue;
}

var retval;
var cnt = {};

// parse attributes
if ( elem.attributes && elem.attributes.length ) {
retval = {};
for ( var i=0; i<elem.attributes.length; i++ ) {
var key = elem.attributes[i].nodeName;
if ( typeof(key) != "string" ) continue;
var val = elem.attributes[i].nodeValue;
if ( ! val ) continue;
key = this.attr_prefix + key;
if ( typeof(cnt[key]) == "undefined" ) cnt[key] = 0;
cnt[key] ++;
this.addNode( retval, key, cnt[key], val );
}
}

// parse child nodes (recursive)
if ( elem.childNodes && elem.childNodes.length ) {
var textonly = true;
if ( retval ) textonly = false; // some attributes exists
for ( var i=0; i<elem.childNodes.length && textonly; i++ ) {
var ntype = elem.childNodes[i].nodeType;
if ( ntype == 3 || ntype == 4 ) continue;
textonly = false;
}
if ( textonly ) {
if ( ! retval ) retval = "";
for ( var i=0; i<elem.childNodes.length; i++ ) {
retval += elem.childNodes[i].nodeValue;
}
} else {
if ( ! retval ) retval = {};
for ( var i=0; i<elem.childNodes.length; i++ ) {
var key = elem.childNodes[i].nodeName.replace("#","");
if ( typeof(key) != "string" ) continue;
var val = this.parseElement( elem.childNodes[i] );
if ( ! val ) continue;
if ( typeof(cnt[key]) == "undefined" ) cnt[key] = 0;
cnt[key] ++;
this.addNode( retval, key, cnt[key], val );
}
}
}
return retval;
};

// method: addNode( hash, key, count, value )
XML.ObjTree.prototype.addNode = function ( hash, key, cnts, val ) {
key = this.key_qualify(key);
if ( this.__force_array[key] ) {
if ( cnts == 1 ) hash[key] = [];
hash[key][hash[key].length] = val; // push
} else if ( cnts == 1 ) { // 1st sibling
hash[key] = val;
} else if ( cnts == 2 ) { // 2nd sibling
hash[key] = [ hash[key], val ];
} else { // 3rd sibling and more
hash[key][hash[key].length] = val;
}
};

XML.ObjTree.prototype.key_qualify = function( key ){
return key.replace(/\W/gi,"");
}
// method: xml_escape( text )
XML.ObjTree.prototype.xml_escape = function ( text ) {
return String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
};

/*
Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the Artistic license. Or whatever license I choose,
which I will do instead of keeping this documentation like it is.
*/



The original xml result looks like this


<resultset morerecords=\"0\" paging-cookie=\"&lt;cookie page=&quot;1&quot;&gt;&lt;fullname last=&quot;SYSTEM&quot; first=&quot;INTEGRATION&quot; /&gt;&lt;systemuserid last=&quot;{D874E288-2C8C-43D5-AEBA-5404888BC185}&quot; first=&quot;{B2C53269-CFF5-4F26-A4E5-669284EA6E96}&quot; /&gt;&lt;/cookie&gt;\">
<result>
<fullname>INTEGRATION</fullname>
<businessunitid dsc=\"0\" name=\"MicrosoftCRM\">
{80E4E0DF-65C1-DC11-B67A-0003FFBB057D}
</businessunitid>
<systemuserid>{B2C53269-CFF5-4F26-A4E5-669284EA6E96}</systemuserid>
</result>
<result>
<fullname>LitwareInc Administrator</fullname>
<businessunitid dsc=\"0\" name=\"MicrosoftCRM\">
{80E4E0DF-65C1-DC11-B67A-0003FFBB057D}
</businessunitid>
<systemuserid>{9BF5E0DF-65C1-DC11-B67A-0003FFBB057D}</systemuserid>
</result>
</resultset>



Usage Example:



function OnCrmPageLoad()
{
/* Get All Users */
var fetchXml = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">';
fetchXml += '<entity name="systemuser">';
fetchXml += '<attribute name="fullname"/>';
fetchXml += '<attribute name="businessunitid"/>';
fetchXml += '<attribute name="title"/>';
fetchXml += '<attribute name="address1_telephone1"/>';
fetchXml += '<attribute name="systemuserid"/>';
fetchXml += '<order attribute="fullname" descending="false"/>';
fetchXml += '</entity>';
fetchXml += '</fetch>';

/* Make the fetch and retrieve xml result */
var resxml = Fetch(fetchXml);
/* Create an ObjTree Object */
var xotree = new XML.ObjTree();
/* Objectize xml result */
var tree = xotree.parseXML( resxml );

/*
if the original node contains only data then the property is treated as string
*/
alert( tree.resultset.result[0].fullname )
/*
if the original node contains attributes or children then you should treat it as
object and reference its properties e.g. object.text and object.propertyName
*/
alert( tree.resultset.result[0].businessunitid.text ); //GUID
alert( tree.resultset.result[0].businessunitid.name ); //OrgName
}

function Fetch( xml )
{
var Xml = "<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\">"
Xml += GenerateAuthenticationHeader()
Xml += "<soap:Body>";
Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
Xml += "<fetchXml>";
Xml += _HtmlEncode(xml); // Microsoft _HtmlEncode function
Xml += "</fetchXml>";
Xml += "</Fetch>";
Xml += "</soap:Body>";
Xml += "</soap:Envelope>";

var XmlHttp = CreateXmlHttp(); // Microsot CreateXmlHttp function
XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
XmlHttp.send(Xml);

return XmlHttp.responseXML.text
}

OnCrmPageLoad();

Friday, October 3, 2008

Playing with Notes


This post is more of an example about how one can utilize the “Show Fetch in IFRAME” post in order to consolidate customer information in a focal location.

Consider a scenario where a contact center service rep needs to see the customer’s related notes (annotations) as part of the case service workflow. In order to achieve that using out of box functionality the rep needs to open the customer’s account page, navigate to the notes tab and select the desired document out of all the notes (text and annotations). Most customers find it an unacceptable behavior as do I.

In order to transform the above behavior into a “One Click Process” procedure I’ve added a new IFRAME to the case entity called IFRAME_relatednotes and attached the iframe results to the customer’s lookup onchange event. Each time the customer lookup changes the iframe is filled with the selected customer annotations.

Follow the “Display Fetch in IFRAME” post in order to assign valid FetchViewer Notes Parameters.I’ve also added the FetchViewer Class functionally as part of this example for the sake of completeness.






var customerLookup;
function OnCrmPageLoad()
{
customerLookup = crmForm.all.customerid;

window.CustomerRelatedNotes = new FetchViewer("IFRAME_relatednotes");
CustomerRelatedNotes.LayoutXml = getLayoutXml();
CustomerRelatedNotes.Entity = "annotation";
CustomerRelatedNotes.QueryId = "{DDDFF6AE-2F52-4640-B2BB-2BA59DA0777C}";
//First Time
SetRelatedNotesFetchXml();
CustomerRelatedNotes.RegisterOnTab(0); //IFRAME ON THE DEFAULT TAB
//Consequent lookup selections
crmForm.all.customerid.attachEvent( "onchange" , function(){
SetRelatedNotesFetchXml();
CustomerRelatedNotes.Refresh();
});
}

/*
The function construct a valid fetchexml depending on the customer selection taking the customer type into account.
If the customer lookup does not contain data then the fetch uses an empty GUID
*/
function SetRelatedNotesFetchXml(){

var customerGuid = "{00000000-0000-0000-0000-000000000000}"
var customerType = "account"

if( customerLookup.DataValue != null )
{
customerGuid = customerLookup.DataValue[0].id;
customerType = customerLookup.DataValue[0].typename;
}

var fetchXml = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">';
fetchXml += '<entity name="annotation">';
fetchXml += '<attribute name="subject"/>';
fetchXml += '<attribute name="notetext"/>';
fetchXml += '<attribute name="filename"/>';
fetchXml += '<attribute name="annotationid"/>';
fetchXml += '<order attribute="subject" descending="false"/>';
fetchXml += '<filter type="and">';
fetchXml += '<condition attribute="isdocument" operator="eq" value="1"/>';
fetchXml += '</filter>';
fetchXml += '<link-entity name="' + customerType + '" from="' + customerType + 'id" to="objectid" alias="aa">';
fetchXml += '<filter type="and">';
fetchXml += '<condition attribute="' + customerType + 'id" operator="eq" uitype="' + customerType + '" value="' + customerGuid + '"/>';
fetchXml += '</filter>';
fetchXml += '</link-entity>';
fetchXml += '</entity></fetch>';

CustomerRelatedNotes.FetchXml = fetchXml;
}

function getLayoutXml(){
return '<grid name="resultset" object="5" jump="" select="1" icon="1" preview="1"><row name="result" id="annotationid"><cell name="subject" width="200" /><cell name="notetext" width="200" /><cell name="filename" width="80" /></row></grid>';
}

function FetchViewer( iframeId )
{
var Instance = this;
var vDynamicForm;
var m_iframeTab;
var m_iframeDoc;

Instance.Entity = "";
Instance.Iframe = null;
Instance.FetchXml = "";
Instance.QueryId = "";
Instance.LayoutXml = "";

Instance.RegisterOnTab = function( tabIndex )
{
Instance.Iframe = document.getElementById( iframeId );

if( !Instance.Iframe )
return alert( "Iframe " + iframeId + " is undefined" );

m_iframeDoc = getIframeDocument();
var loadingGifHTML = "<table height='100%' width='100%' style='cursor:wait'>";
loadingGifHTML += "<tr>";
loadingGifHTML += "<td valign='middle' align='center'>";
loadingGifHTML += "<img alt='' src='/_imgs/AdvFind/progress.gif'/>";
loadingGifHTML += "<div/><b>Loading View...</b>";
loadingGifHTML += "</td></tr></table>";
m_iframeDoc.body.innerHTML = loadingGifHTML;

if( parseInt( "0" + tabIndex ) == 0 ) Instance.Refresh();
else Instance.Iframe.attachEvent( "onreadystatechange" , RefreshOnReadyStateChange );
}

function RefreshOnReadyStateChange()
{
if( Instance.Iframe.readyState != 'complete' )
return;

Instance.Refresh();
}

Instance.Refresh = function()
{
if( !Instance.Iframe )
return alert( "Iframe " + iframeId + " is undefined" );

m_iframeDoc = getIframeDocument();

Instance.Iframe.detachEvent( "onreadystatechange" , RefreshOnReadyStateChange );

var create = m_iframeDoc.createElement;
var append1 = m_iframeDoc.appendChild;
vDynamicForm = create("<FORM name='vDynamicForm' method='post'>");

var append2 = vDynamicForm.appendChild;
append2(create("<INPUT type='hidden' name='FetchXml'>"));
append2(create("<INPUT type='hidden' name='LayoutXml'>"));
append2(create("<INPUT type='hidden' name='EntityName'>"));
append2(create("<INPUT type='hidden' name='DefaultAdvFindViewId'>"));
append2(create("<INPUT type='hidden' name='ViewType'>"));
append1( vDynamicForm );

vDynamicForm.action = "/" + ORG_UNIQUE_NAME + "/AdvancedFind/fetchData.aspx";
vDynamicForm.FetchXml.value = Instance.FetchXml;
vDynamicForm.LayoutXml.value = Instance.LayoutXml;
vDynamicForm.EntityName.value = Instance.Entity;
vDynamicForm.DefaultAdvFindViewId.value = Instance.QueryId;
vDynamicForm.ViewType.value = 1039;
vDynamicForm.submit();

Instance.Iframe.attachEvent( "onreadystatechange" , OnViewReady );
}

function OnViewReady()
{
if( Instance.Iframe.readyState != 'complete' ) return;

Instance.Iframe.style.border = 0;
Instance.Iframe.detachEvent( "onreadystatechange" , OnViewReady );
m_iframeDoc = getIframeDocument();
m_iframeDoc.body.scroll = "no";
m_iframeDoc.body.style.padding = "0px";
}

function getIframeDocument(){
return Instance.Iframe.contentWindow.document;
}
}

OnCrmPageLoad();

Wednesday, October 1, 2008

Finding crmForm elements By Css Class Names


There are situations where you need to change form elements text or appearance dynamically, i.e. changing a section name or page title, but find it “impossible” since the form html DOM doesn’t include identifiers for those elements.

Most examples I’ve seen regarding similar requests use a simple drill down (or up) mechanism from a certain anchor element, usually the closest element the target.
For example:

var anchorElement = document.getElementById(“An Element WithID”);
var targetElement = anchorElement.parentElement.parentElement.parentElement;
targetElement.innerText = "New Text";


This type of method is less then elegant and is a highly unmanageable technique.

In order to address this issue I’ve created a simple getElementsByClassName function which is a method similar to the already known getElementsByTagName function. The function receives an anchor element ( or null ) and a css class Name and return an array of html element that match the class Name. This type of mechanism is actually very popular in most JavaScript libraries like jquery and prototype.

The following code includes getElementsByClassName function and two common requests that are handled by that function.



function OnCrmPageLoad()
{
//Example #1: Change Address Section Text
var anchorNode = document.getElementById( "tab0" );
var secBarCss = "ms-crm-Form-SectionBar";
var addrSecTxt = "Address";
var addrSecElm = null;

var results = getElementsByClassName( secBarCss , anchorNode );
for( var i = 0 ; i < results.length ; i++ )
{
if( results[i].innerText == addrSecTxt ){
addrSecElm = results[i]; break;
}
}

addrSecElm.innerText = "Partial Address Information";

//Example #2: Change Account title form account: name to account: name (accountnumber)
anchorNode = null;
titleCss = "ms-crm-Form-Title";
titleTag = "SPAN";
titleElem = null;

results = getElementsByClassName( titleCss , anchorNode );
for( var i = 0 ; i < results.length ; i++ )
{
if( results[i].tagName == titleTag ){
titleElem = results[i]; break;
}
}

var accountNumber = crmForm.all.accountnumber.DataValue;
if (accountNumber == null)
accountNumber = "Not Supplied";

titleElem.innerText = titleElem.innerText + " (" + accountNumber + ")";
}

function getElementsByClassName(className, anchorNode)
{
if(!anchorNode) anchorNode = document.body;
var result = [];
var regEx = new RegExp("\\b" + className + "\\b");
var children = anchorNode.getElementsByTagName("*");
for( var i = 0 ; i < children.length ; i++ )
{
if( regEx.test( children[i].className ) )
result.push( children[i] );
}
return result;
}

OnCrmPageLoad();