Friday, September 5, 2008

Adding Presence to CRM Form fields

As you all know “Presence” is now part of CRM 4.0. To enable presence the client machine only
needs communicator installed.


In CRM 4.0 the presence control is only enabled for contact and system user entities on some
views and inside lookups. You can read more about it here.

What I was really after is integrating presence for other CRM Form fields of type email or phone.
This was actually very simple since all you need to do is create an instance of “
Name.NameCtrl“ control and show or hide it using two simple methods.

The majority of the code takes care of the positioning of the Presence on the crmForm on the right edge of each CRM field (like a lookup).

When you add presence to a field also send the tab index where the field resides for positioning purposes because crmForm scrolling is tab related.

The Presence DLL is installed with office. So if you’re having trouble seeing it,
usually running a repair on office solves the problem.






Add the code to the entity onload event


var ocs = new ActiveXObject("Name.NameCtrl");


function OnCrmPageLoad()
{
AddPresence( "emailaddress1" , 0 );
AddPresence( "telephone1" , 0 );
}

function AddPresence( elementId , tabIndex )
{
if( typeof(ocs) == "object" )
{
var element = document.getElementById( elementId );
element.tab = tabIndex;
element.attachEvent( "onmouseover" , ShowOOUI );
element.attachEvent( "onmouseout" , HideOOUI );
window. attachEvent( "onscroll" , HideOOUI );
}
}


// show Messenger icons
function ShowOOUI()
{
var element = event.srcElement;
var elemVal = element.DataValue;
if( !(element && elemVal) )
return;

var offsetX = -20;
var offsetY = -2;

for (var parent = element; parent; parent = parent.offsetParent)
{
if (parent.offsetLeft)
{
offsetX += parent.offsetLeft;
}

if (parent.offsetTop)
{
offsetY += parent.offsetTop;
}
}

var scrollTop = document.getElementById("tab" + element.tab).scrollTop;
ocs.ShowOOUI(elemVal, 0, offsetX + element.offsetWidth, offsetY - scrollTop);
}

function HideOOUI()
{
ocs.HideOOUI();
}

OnCrmPageLoad();

Wednesday, September 3, 2008

Quick MS CRM CTI Integration

Although this is just a partial cti solution I wanted to demonstrate how easy it is to create cti integration app using Microsoft crm.

I added a single aspx page that I call cti.aspx inside the ISV folder. In order to avoid using CrmService Proxy or creating a new asp.net application I used my former post “Ajax using fetch message” to call CrmService.Fetch. The cti.aspx page receives a parameter which reflects the type of search to make e.g. p for phone numbers or n for names and the organization name. I only implemented the phone number search for the sake of this example.

The page (Page_Load) constructs a FetchXml using the received parameter (p or n) value and hands it over to the client side Fetch function. The function returns a FetchResult. If the result contains zero accounts then a new account page is opened inside the iframe, if a single result is returned then the accountid is taken from the result and concatenated into a valid account url. If there is more then 1 account then the iframe is loaded with an advanced find page which uses the original fetch to build and execute the advanced find form dynamically.

I didn’t add the functions from my former post “Ajax using fetch message” so just copy the functions without the OnCrmPageLoad function to this implementation.

A few tips before you start:
1. Construct a fetchxml using the crm advanced find window. Again my former post shows exactly how to accomplish that.
2. After you create an advanced find query copy the results to a notepad and replace all (“) with (‘)
3. The fetchxml is assigned to a client side function so it should be contained in a single line.
4. This app uses a valid fetchxml with an OR filter for all the account telephone fields . Try passing the cti.aspx?p=[a number that exist more then once] to see the fetch query inside the advanced find form.
5. The advanced find url built into this example is taken from my test environment. you need to replace it with a valid QueryId GUID.

It took me 10 minutes to write this down which was the whole idea to begin with.
I hope you find this useful.

Page Url: http://[CRM]/[ORG]/ISV/cti.aspx?p=555-555-0100&o=[ORG]

Here is the code devided into logical sections(c#,js,html)


<%@ Page Language="C#" AUTOEVENTWIREUP="true"%>

<script runat="server">

private String FetchXml = String.Empty;
private String OrgName = String.Empty;

protected void Page_Load( Object sender , EventArgs e )
{
try
{
OrgName = Request.QueryString["o"] + "";
switch(Request.QueryString.Keys[0])
{
case "n": // n for name
//Assemble FetchXml
break;
case "p": // p for phone
FetchXml = String.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'><entity name='account'><attribute name='accountid'/><order attribute='name' descending='false'/><filter type='and'><filter type='or'><condition attribute='telephone1' operator='eq' value='{0}'/><condition attribute='address1_telephone2' operator='eq' value='{0}'/><condition attribute='address1_telephone3' operator='eq' value='{0}'/><condition attribute='address2_telephone1' operator='eq' value='{0}'/><condition attribute='address2_telephone2' operator='eq' value='{0}'/><condition attribute='address2_telephone3' operator='eq' value='{0}'/></filter></filter></entity></fetch>", Request.QueryString[0]);
break;
}
}
catch( Exception ex )
{
Response.Write( ex.ToString() );
}
}
</script>




<script>
var resultFrame;

function window.onload()
{
resultFrame = document.all.resultFrame;
var FetchResult = Fetch( "<%=FetchXml%>" );
switch( FetchResult.documentElement.childNodes.length )
{
case 0:
resultFrame.src = "/<%=OrgName%>/sfa/accts/edit.aspx#";
break;
case 1:
resultFrame.src = "/<%=OrgName%>/sfa/accts/edit.aspx?id=" + FetchResult.selectSingleNode("//accountid").text + "#";
break;
default:
resultFrame.src = "/<%=OrgName%>/AdvancedFind/AdvFind.aspx?EntityCode=1&QueryId={00000000-0000-0000-00AA-000010001001}&ViewType=1039"
resultFrame.onreadystatechange = OnAdvancedFindIframeReady;
break;
}
}

function OnAdvancedFindIframeReady()
{
if( resultFrame.readyState != 'complete' ) return;

resultFrame.contentWindow.document.all.advFind.FetchXml = "<%=FetchXml%>";
window.setTimeout(Run,1000);
}

function Run()
{
resultFrame.contentWindow.document.all.btnRun.click();
}

/*
you can either refere to an opener (if one exists) to get the AuthenticationHeader or
construct it your self like so.
*/
function GenerateAuthenticationHeader()
{
var token = "<soap:Header>";
token += "<CrmAuthenticationToken xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
token += "<AuthenticationType xmlns=\"http://schemas.microsoft.com/crm/2007/CoreTypes\">0</AuthenticationType>";
token += "<OrganizationName xmlns=\"http://schemas.microsoft.com/crm/2007/CoreTypes\">";
token += "<%=OrgName%>";
token += "</OrganizationName>";
token += "<CallerId xmlns=\"http://schemas.microsoft.com/crm/2007/CoreTypes\">00000000-0000-0000-0000-000000000000</CallerId>";
token += "</CrmAuthenticationToken>";
token += "</soap:Header>";
return token;
}

/* Add the “ajax using Fetch message functions here */

</script>




<html>
<head runat="server">
<title>Untitled Page</title>
</head>
<body scroll="no" style="margin:0px">
<iframe id="resultFrame" src="about:blank" style="width:100%;height:100%"></iframe>
</body>
</html>

Monday, September 1, 2008

Ajax using Fetch Message


Ajax is a collection of technologies complied into one word (“mess”), as such retrieving information from crm can be cumbersome and error prone. Although there are tools that can help you accomplish the task I found they require too match code and sometimes you find your self wasting hours looking for meaningless errors.


Now, all I wanted is; to create a fetch (using advanced find), hand it over to a js method (Fetch) and expect an xmldocument (FetchResult) I can query with ease. The code below is the result of my quest “to the promised land”.



To get the FetchXml, All you have to do is:

1. Create an advanced find query using ,well, crm advanced find , press ctrl + N to open the advanced find in a new window to reveal the address bar (or press F11 twice)


2. In the address bar paste this simple js snippet that returns the
FetchXml:
javascript:void( alert( advFind.FetchXml ))


3. Press ctrl + A and ctrl + c to copy the fetch into your working js document or onload event.


The CreateXmlHttpObject() and FetchEncode() are functions Microsoft use in their code. I pasted them here as part of the infrastructure to avoid dependency. All you really need is the Fetch() function. Now this is unsupported but because the code is considered infrastructure you
should consider putting it inside global.js and make it available to all crm Forms (entities).


If MS is listening, one of the most needed features ,beside filtered lookups, is an "OnGlobal" event (like the onload event) that integrates code as such to the global.js file.


The example below uses a static fetch that returns the current user; you may concatenate dynamic values as needed.




function OnCrmPageLoad()
{
var FetchResult = Fetch('<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"><entity name="systemuser"><attribute name="fullname"/><attribute name="businessunitid"/><attribute name="title"/><attribute name="address1_telephone1"/><attribute name="systemuserid"/><order attribute="fullname" descending="false"/><filter type="and"><condition attribute="systemuserid" operator="eq-userid"/></filter></entity></fetch>');
alert( FetchResult.selectSingleNode("//fullname").text ); //User Fullname
}

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 += FetchEncode(xml); // Microsoft _HtmlEncode function
Xml += "</fetchXml>";
Xml += "</Fetch>";
Xml += "</soap:Body>";
Xml += "</soap:Envelope>";

var XmlHttp = CreateXmlHttpObject(); // 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);

var XmlDoc = new ActiveXObject("Msxml2.DOMDocument");
XmlDoc.async = false;
XmlDoc.resolveExternals = false;
XmlDoc.loadXML(XmlHttp.responseXML.text);

return XmlDoc;
}

function CreateXmlHttpObject() //CreateXmlHttp
{
var oXmlHttp = null;
if(window.XMLHttpRequest)
{
oXmlHttp = new XMLHttpRequest();
}
else
{
var arrProgIds = ["Msxml2.XMLHTTP","Microsoft.XMLHTTP"];
for(var iCount = 0; iCount < arrProgIds.length;iCount++)
{
try
{
oXmlHttp = new ActiveXObject(arrProgIds[iCount]);
break;
}
catch(e){}
}
}

return oXmlHttp;
}

function FetchEncode( strInput ) //_HtmlEncode
{
var c;
var HtmlEncode = '';

if(strInput == null)
{
return null;
}
if (strInput == '')
{
return '';
}

for(var cnt = 0; cnt < strInput.length; cnt++)
{
c = strInput.charCodeAt(cnt);

if (( ( c > 96 ) && ( c < 123 ) ) ||
( ( c > 64 ) && ( c < 91 ) ) ||
( c == 32 ) ||
( ( c > 47 ) && ( c < 58 ) ) ||
( c == 46 ) ||
( c == 44 ) ||
( c == 45 ) ||
( c == 95 ))
{
HtmlEncode = HtmlEncode + String.fromCharCode(c);
}
else
{
HtmlEncode = HtmlEncode + '&#' + c + ';';
}
}

return HtmlEncode;
}

OnCrmPageLoad();