Wednesday, September 10, 2008

CRM Lookup Preview


I thought the best way to appreciate some of the samples I posted would be to combine them into a meaningful solution.
I came up with this neat idea that I call “Lookup Preview” much like the grid preview feature.
It started out simple but as the hours flew I decided to make it a generic solution to support all lookup types and common fetch requests.

The “lookup preview” combines the “custom tooltip” and “Ajax using fetch message” posts.
Because the result came out more then I intended it to be (in other words complicated) I decided to make it programmer friendly and build a comfortable API around it. So I won’t dive into the bit and bytes but rather write about how to implement the solution.

I few notes before I start, just in case someone from ms is reading this. I do believe that one of the major disadvantages working with crm 4.0 is the lack of Interactive API on the client side. Ms will either create a complete solution much like the form assistant and lookup control or leave a black hole. As dynamics becomes more and more a platform and less a crm product it requires more entry points for development purposes. I do believe that ms is going that way and If you ask me version 10 will be called dynamics10.0 framework, who would even remember that this was ever a crm application.

Now, at some point, it passed my mind that this could actually be a sellable “add-on”. As it may, the only way to interact with the crmForm is by writing unsupported DOM manipulations. So unless someone at ms is interested in investing in my Lookup Preview the solution is free for all.

I’ll try to update the solution on regular basis and add more features to it when I can. If you’re having any issues please comment on them and I’ll try to response when I can.

Ok, let’s start...

The basic idea behind the lookup preview is the ability to create some sort of fetch that retrieves information related to the current lookup DataValue. The nice thing about FetchXml in v4.0 is that you can also retrieve information from an entity related entities so, for example, if I wanted to show the user business unit on an account owner lookup all I need is to create a fetch that implements the linked entity (business unit) column name. This is actually a 3 levels deep solutions since you can see the current account data with the system user data and all related system user entities. Quite handy I’d say.

The API is very simple to use. The following example refers to the “to” lookup on the email entity.

The “to” lookup, as the regarding and customer lookups, can contain several entity types. Therefore it is mandatory to provide fetch information for each entity type. Although I could have taken the types from the lookup itself I decided that you may choose not to show preview for some entity types and thus made it a public choice.

The OnCrmPageLoad function describes the usage. If you have any questions, again, you are welcome to comment.









function OnCrmPageLoad()
{
/*
Reference the to Lookup, Provide the lookup (control) id.
*/
var PrvToLui = new LookupPreview("to");

/*
Add Account Preview Information ,
returns an account entity wrapper object
*/
var accountEntity = PrvToLui.AddEntity("account");

/*
Add Account Preview Attributes,
Provide lable and schema name for each attribute
*/
accountEntity.AddAttribute("Street","address1_line1");
accountEntity.AddAttribute("City","address1_city");
accountEntity.AddAttribute("State","address1_stateorprovince");
accountEntity.AddAttribute("Zip","address1_postalcode");

/*
Add Related Entity (Primary Contact) Information
AddLinked requires the entity name and
lookup field schema name on the account.
*/

var contactEntity = accountEntity.AddLinked("contact","primarycontactid");
contactEntity.AddAttribute("Primary Contact","fullname");
contactEntity.AddAttribute("Contact Phone","telephone1");

/* ------------------------------------------------------------------ */

//Contact Entity Preview
var contactEntity = PrvToLui.AddEntity("contact");
contactEntity.AddAttribute("Street","address1_line1");
contactEntity.AddAttribute("City","address1_city");
contactEntity.AddAttribute("State","address1_stateorprovince");
contactEntity.AddAttribute("Zip","address1_postalcode");
/* ------------------------------------------------------------------ */

//Lead Entity Preview
var leadEntity = PrvToLui.AddEntity("lead");
leadEntity.AddAttribute("Full Name","fullname");
/* ------------------------------------------------------------------ */

//Systemuser Entity Preview
var userEntity = PrvToLui.AddEntity("systemuser");
userEntity.AddAttribute("Preferred Phone","preferredphonecode");
userEntity.AddAttribute("Main Phone","address1_telephone1");

var buEntity = userEntity.AddLinked("businessunit","businessunitid");
buEntity.AddAttribute("Business Unit","name");
}

function LookupPreview( lookupId )
{
var Instance = this;
Instance.Lookup = document.getElementById( lookupId );

if(isNullOrEmpty(Instance.Lookup))
return;

//Public
Instance.Entities = [];

Instance.AddEntity = function(entityName)
{
var Entity = new Object();
Entity.Name = entityName;
Entity.AddAttribute = function( labelName , attrName )
{
var Attributes = Instance.Entities[entityName].Attributes;
var attribute = new Attribute(entityName , attrName , labelName , attrName);
Attributes[Attributes.length] = attribute;
DisplayAttributes[DisplayAttributes.length] = attribute;
}

Entity.Attributes = [];
Entity.LinkedByName = [];
Entity.LinkedByIndex = [];
Entity.AddLinked = function( lnkEntityName , referencingAttribute )
{
var LinkEntity = new Object();
LinkEntity.Name = lnkEntityName;
LinkEntity.Attributes = [];
LinkEntity.RefAttribute = referencingAttribute;
LinkEntity.AddAttribute = function( labelName , attrName )
{
var attribute = new Attribute(entityName , attrName , labelName , LinkEntity.RefAttribute + "." + attrName);
LinkEntity.Attributes[LinkEntity.Attributes.length] = attribute;
DisplayAttributes[DisplayAttributes.length] = attribute;
}
Entity.LinkedByIndex[Entity.LinkedByIndex.length] = Entity.LinkedByName[name] = LinkEntity;
return LinkEntity;
}
Instance.Entities[entityName] = Entity;
return Entity;
}

Instance.Show = function(dataElement)
{
var control = Instance.Lookup;
var DataValue = control.DataValue;

if( isNullOrEmpty(DataValue) )
return;
if( isNullOrEmpty(Instance.Entities[DataValue[dataElement.Index].typename]) )
return;

TooltipPopup = window.createPopup();

if( !dataElement.PreviewHTML )
{
var ToolTipHTML = "<fieldset style='width:100%;height:100%;border:1px solid gray;background-color: #d8e8ff;filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#eff3ff,EndColorStr=#c6dfff);padding-left:2px;'><legend style='background-color:#eff3ff;width:100;padding-left:13px;border:1px solid gray;font-size:12px;font-family:tahoma'><b>Preview</b></legend>";
var xmlHttp = CreateXmlHttp();
var xml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"";
xml += " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
xml += " 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>";

var fetchxml = "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>";
var DataVal = control.DataValue[dataElement.Index];
var entity = DataVal.typename;
fetchxml += "<entity name='" + entity + "'>";
var attributes = Instance.Entities[entity].Attributes;
for( var i = 0 ; i < attributes.length ; i++ )
fetchxml += "<attribute name='" + attributes[i].Name + "'/>";
fetchxml += "<filter type='and'>";
fetchxml += "<condition attribute='" + entity + "id' operator='eq' value='" + DataVal.id + "'/>";
fetchxml += "</filter>";

for( var i = 0 ; i < Instance.Entities[entity].LinkedByIndex.length ; i++ )
{
var linked = Instance.Entities[entity].LinkedByIndex[i];
fetchxml += "<link-entity name='" + linked.Name + "' from='" + linked.Name + "id' to='" + linked.RefAttribute+ "' visible='false' link-type='outer'>";
for( var j = 0 ; j < linked.Attributes.length ; j++ )
fetchxml += "<attribute name='" + linked.Attributes[j].Name + "'/>";
fetchxml += "</link-entity>";
}
fetchxml += "</entity>";
fetchxml += "</fetch>";

xml += _HtmlEncode(fetchxml);
xml += "</fetchXml>";
xml += "</Fetch>";
xml += "</soap:Body>";
xml += "</soap:Envelope>";

xmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false );
xmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
xmlHttp.send(xml);

var resultDoc = loadXmlDocument(xmlHttp.responseXML.text);
var previewHtml = "<br style='line-height:2px'/><table width='100%' style='font:12 px tahoma'>";
dataElement.Width = 0;

for( var i = 0 , j = 1; i < DisplayAttributes.length ; i++ )
{
var attribute = DisplayAttributes[i];
if( attribute.Entity != entity ) continue;

var attrNode = resultDoc.selectSingleNode("//" + attribute.XPathName );
var attrValue = (attrNode)? attrNode.text:"";

dataElement.Height = (j++) * 22;
var maxLength = ( attrValue.length + attribute.Label.length ) * 9;
if( dataElement.Width < maxLength )
dataElement.Width = maxLength;
previewHtml += "<tr><td style='padding:3px 0px 0px 0px;color:brown'><nobr>" + attribute.Label + "</nobr></td><td><nobr>" + attrValue + "</nobr></td></tr>";

}

dataElement.Height += 20;

ToolTipHTML += previewHtml;
ToolTipHTML += "</table></fieldset>";
dataElement.PreviewHTML = ToolTipHTML;
}

TooltipPopup.document.body.innerHTML = dataElement.PreviewHTML;

var Position = getControlPostion();
var Left = Position.X + 1;
var Top = Position.Y + 5;

TooltipPopup.show( Left , Top - dataElement.parentElement.scrollTop , dataElement.Width, dataElement.Height , null );
}

Instance.Hide = function()
{
if( TooltipPopup )
TooltipPopup.hide();
}

Instance.OnLookupChange = function()
{
var jump = 0;
if( Instance.Lookup.DataValue != null )
{
var DataElementsLen = Instance.Lookup.parentElement.previousSibling.childNodes[0].childNodes.length;
var DataValuesLen = Instance.Lookup.DataValue.length;
jump = (DataElementsLen == DataValuesLen)? 1:2;

for( var i = 0 ; i < DataValuesLen*jump ; i+=jump )
{
var DataValueElemet = Instance.Lookup.parentElement.previousSibling.childNodes[0].childNodes[i];
if( !isNullOrEmpty(DataValueElemet.onmouseover) ) continue;

DataValueElemet.Preview = Instance;
DataValueElemet.Index = i/jump;
DataValueElemet.onmouseover = function(){
this.Preview.Show(this);
}

DataValueElemet.onmouseout = function(){
this.Preview.Hide();
}
}
}
}

//Private
var TooltipPopup;
var DisplayAttributes;

function Init()
{
DisplayAttributes = [];
Instance.Lookup.attachEvent( "onchange" , Instance.OnLookupChange );
Instance.OnLookupChange(); //First Time
}

function Attribute(entityName,attrName,attrLabel,attrXpathName)
{
this.Name = attrName;
this.Entity = entityName;
this.Label = attrLabel;
this.XPathName = attrXpathName;
}

function getControlPostion()
{
control = event.srcElement;
var Position = new Object();
var controlHeight = control.offsetHeight;
var iY = 0, iX = 0;
while( control != null )
{
iY += control.offsetTop;
iX += control.offsetLeft;
control = control.offsetParent;
}
Position.X = iX + screenLeft;
Position.Y = iY + screenTop + controlHeight;
return Position;
}

function isNullOrEmpty( obj ){
return obj == null || typeof(obj) == "undefined" || obj == "";
}

Init();
}

OnCrmPageLoad();

14 comments:

Anonymous said...

Hi Adi,

Thank you very much for this code - it works really well!

I want to expand the use of your code - for example on an onload of a form, I want to find the 'telephone2' number of the 'contactid' and use it to populate a new field on the form.

I can almost do this - but I can only get it working when I mouseover the 'contactid'. I want this to happen automatically when the form loads. I think my problems lie with the attach event, but I'm having problems figuring out how.

Can you give me a pointer of what I will need to change in your code.

Thank you Adi, your help is much appreciated.

Adi Katz said...

I advise you not to change the lookup preview unless you're looking to expand its distinct functionality.

What you're trying to achieve can easily be implemented using this post.

Anonymous said...

Hi Adi,

I have used the fetchXML to achieve what I wanted to do.

Thank you for your help!

Anonymous said...

Hello Adi,

thank you for this code.

Is it possible to extend it so that you can use AddLinked on an AddLinked-entity itself (in other words to cascade the entities deeper)?

Thanks for this great blog.
Burkhard

Adi Katz said...

Hi Burkhard,

Nice thought, and yes you can! As long as the fetchxml supports your query, which it does.

My code makes a very simple iteration over the linked entities.

You’ll need to make a recursion call to each entity linked entities to build the fetch correctly.

Adi

Anonymous said...

This is awesome! Thank you!!!

Moon

Anonymous said...

Hi Adi,

So if I wanted to add this code to the Account "Primary Contact" field, I would just need to change this line of code:

var PrvToLui = new LookupPreview("to");

to this?

var PrvToLui = new LookupPreview("primarycontactid");

and this might work for me?

I'm kinda new at this CRM stuff, but this really inspired me!

Thanks!
OneShoe

Adi Katz said...

Hi,

Thanks for the compliment.

If you need to implement the lookup preview on a contact then you should change the id sent to the lookup preview object constructor and
Add an entity of type contact.

For example:

*/
var PrvPCLui = new LookupPreview("primarycontactid");
/*
Add Account Preview Information ,
returns an account entity wrapper object
*/
var contactEntity = PrvPCLui.AddEntity("contact");

/*
Add Account Preview Attributes,
Provide lable and schema name for each attribute
*/
contactEntity.AddAttribute("Street","address1_line1");
contactEntity.AddAttribute("City","address1_city");
contactEntity.AddAttribute("State","address1_stateorprovince");
contactEntity.AddAttribute("Zip","address1_postalcode");

Anonymous said...

the last example fails for me on Account entity.

in addition to the error, is there a way to have several previews for one form?

Adi Katz said...

As many as you like. The preview is per lookup value. If it’s a multilookup like the from attribute on the email entity then you can have multiple previews for the same lookup . If it’s a customer lookup then you can build a preview for contact and another preview for account. If you have 2 or more single lookups on the form then each lookup value can have its own preview.

Anonymous said...

wonder why i'm getting an error then. I copied the code you gave to the othter person on top of your replacing those lines and I get an error.

Djack said...

This is awesome!
I have only a small problem, sometime the Preview window width is too small compare to the contain to be displayed.
could you help me with this

Adi Katz said...

Lines 166,167 handle the width and height of the preview, you can play with the numbers to get better results.

Adi

Vincent said...

How do I deal with a picklist field? The preview displays the internal number and I would like to see the text.

Vincent