Sunday, September 7, 2008

Coding CRM Lookup Fields

A Lookup is a reference or a pointer to another record (GUID) in crm. As opposed to other crm types, lookups are handled differently in many aspects. One “interesting” aspect which I relate to here is how a lookup DataValue is assigned when used in JavaScript.

The lookup DataValue is actually an array which holds a “lookup item” object or objects. Each lookup item exposes a set of properties we need to address in code for crm to acknowledge
its value. In reality an entity with a lookup is comprised of 3 table columns, so for example the primary contact lookup field on the account entity is actually a combination of primarycontactid (guid “{1234…}”) , primarycontactidname (name “Adi Katz”) and primarycontactiddesc columns in the database.

Now, crm only cares about GUIDs so until we start calling our selves by GUID (and not by NAME) we need to bridge that gap. Ms does relate to this gap, but only from the UI / End user perspective (auto complete feature). Unfortunately this ability is not exposed through code in any supported way and we need to build that bridge (automate the process) our selves using custom Ajax calls.

In the simplest scenario the lookup DataValue requires only a GUID (id) and entity name(typename). For example:


var LookupItem = new Object();
LookupItem.name = “Crm only cares about GUIDs (pls Save...)”;
LookupItem.id = “{D04D44AD-36D0-DC11-AA32-0003FF33509E}”;
LookupItem.typename = “contact”;
crmForm.all.<Lookup Id>.DataValue = [LookupItem];

The problem is that we usually don’t know the entity id (GUID) only its name e.g.

var LookupItem = new Object();
LookupItem.name = “Adi Katz”;
LookupItem.id = “”;
LookupItem.typename = “contact”;
crmForm.all.<Lookup Id>.DataValue = [LookupItem];

And of course this won’t work unless we lookup “Adi Katz" GUID by name and assign
it to the LookupItem.id.

The following “LookupHelper” class facilitates / automates the process by providing
the following features:

If “Adi Katz” does not exist then a clean lookup dialog is opened.
If “Adi Katz” exists more then once a lookup dialog is opened with the name “Adi Katz” inside the search box.
If one “Adi Katz” exists then the lookup is assigned a valid DataValue

The Lookup Helper works with all lookup types e.g. (single entity, customer, regarding and party lookup).

In all lookups, setting the entity name will set the lookup defaulttype property so when you open the lookup dialog the entity being searched is set in advance.

The OnCrmPageLoad bellow describes the How-to and possibilities you might encounter
in your daily “lookup programming”.


var curLookup;
function OnCrmPageLoad()
{
/*
- Both GUID (id) and name are known
- entity name is taken from the lookup
*/
curLookup = new LookupHelper( "transactioncurrencyid" );
curLookup.SetValue( "US Dollar" , "{944038FC-65C1-DC11-B67A-0003FFBB057D}" );

/*
- Search Mode
- Open the lookup dialog window
*/
curLookup = new LookupHelper( "transactioncurrencyid" );
curLookup.SetValue("");
/*
- only GUID (id) is known (usually not the case)
- name is a temporary text until you save the record
*/
curLookup = new LookupHelper( "transactioncurrencyid" );
curLookup.SetValue( "" , "{944038FC-65C1-DC11-B67A-0003FFBB057D}");
/*
- only name is known, the id is fetched from crm
- must provide PrimaryField of the lookup entity in this case currencyname
*/
curLookup = new LookupHelper( "transactioncurrencyid" , "currencyname" );
curLookup.SetValue( "US Dollar" );
//OR
curLookup = new LookupHelper( "transactioncurrencyid" );
curLookup.PrimaryField = "currencyname";
curLookup.SetValue( "US Dollar" );
/*
- Customer Lookup
- Change from account to contact entity
- the id is fetched form crm
*/
curLookup = new LookupHelper( "customerid" );
curLookup.SetEntity( "contact" , "fullname" , "Adi Katz" );
//is the same as
curLookup = new LookupHelper( "customerid" );
curLookup.SetEntity( "contact" );
curLookup.PrimaryField = "fullname";
curLookup.SetValue( "Adi Katz" );
/*
- getting the actual lookup field
*/
curLookup = new LookupHelper( "customerid" );
alert(curLookup.Lookup.DataValue[0].name); //assuming datavalue exists.
}

/*
Parameters:

lookupId - Mandatory, The lookup schema name.
primaryField - Optional, Required for fetch. can be set here or later
entity - Optional, Taken from the lookup control
*/
function LookupHelper( lookupId , primaryField , entity )
{
//Lookup Reference
this.Lookup = document.getElementById( lookupId );
//Checks if the lookupId is valid
if(isUndefined(this.Lookup))
return alert('lookup ' + lookupId + ' is undefined');


/* Private Fields */

var Instance = this;
var data = this.Lookup.lookuptypenames.split(',')[0].split(':');
var Entity = isUndefined(entity)? data[0]:entity;
var PK = Entity + "id";

/* Public Fields */

//Holds the lookup text e.g. Adi Katz
this.Text = "Value Selected (Save...)";
//Holds the default operator for the fetchxml condition e.g. 'fullname eq "Adi Katz"'
this.Operator = "eq";
//Holds the lookup entity primary field e.g. contact = fullname , account = name
this.PrimaryField = primaryField;

/*public Functions

Descrition:

Sets the Entity and PrimaryKey - Required For Fetch
Sets the Primary Field - Required For Fetch
Sets the Default Lookup Type - Feature for lookup popup window
If the text is supplied then the id is Fetched from crm

Parameters:

entity - Mandatory, Required For Fetch
primaryField - Mandatory (unless supplied in the ctor),Required For Fetch
text - Optional
*/

this.SetEntity = function( entity , primaryField , text )
{
Entity = entity;
PK = entity + "id";

if(!isUndefined(primaryField))
Instance.PrimaryField = primaryField;

var regDefType = new RegExp(entity + ":(\\d+)","gi");
regDefType.exec(Instance.Lookup.lookuptypenames);
Instance.Lookup.defaulttype = RegExp.$1;

if(!isUndefined(text))
Instance.SetValue(text);
}

/*
Description:
Sets The lookup DataValue

if text is undefined and guid is supplied then
use default text e.g. 'Value Selected (Save...)'
else
use supplied text

if guid is supplied then
set the lookup datavalue
else if text is undefined then
open clean lookup dialog
else
fetch for lookup item id
if 1 record is returned then set datavalue
else
open lookup dialog with specified search condition

Parameters:
guid - record id - global unique identifier
text - name displayed inside the lookup
*/
this.SetValue = function( text , guid )
{
if(!isUndefined(text))
Instance.Text = text;

if(!isUndefined(guid))
{
var LookupItem = new Object();
LookupItem.name = text;
LookupItem.id = guid;
LookupItem.typename = Entity;
Instance.Lookup.DataValue = [LookupItem];
}
else if(isUndefined(text)) Search( "" );
else LookupValue( text );
}

/* Clear Lookup Value - DataVale = null */
this.Clear = function()
{
Instance.Lookup.DataValue = null;
}

/* Private Functions

builds a valid Fetchxml for the current lookup type
fetch value and ( set datavalue or open lookup dialog )
*/
function LookupValue( text )
{
var xmlHttp = CreateXmlHttp();
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>";

var fetchxml = "<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>";

fetchxml += "<entity name='" + Entity + "'>";
fetchxml += "<attribute name='" + PK + "'/>";
fetchxml += "<filter type='and'>";
fetchxml += "<condition attribute='" +
Instance.PrimaryField + "' operator='" + Instance.Operator + "' value='" +
Instance.Text + "'/>";
fetchxml += "</filter>";
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 resultRecords = resultDoc.selectNodes("//" + PK );

if( resultRecords.length == 1 )
{
var guid = resultRecords[0].text;
Instance.SetValue( Instance.Text , guid );
}
else
{
Search( Instance.Text );
}
}

/* Fill search
condtion and open lookup dialog */

function Search( text ){
Instance.Lookup.AddParam("search" , text );
Instance.Lookup.click();
}

/* Checks object definition */
function isUndefined(obj){
return obj == null || typeof(obj) == "undefined" || obj == "";
}
}

OnCrmPageLoad();

8 comments:

Michael Dodd said...

Good stuff. Question:

How would I go about modifying the existing Contract Lookup field in the Incident entity? It's currently set to filter on the CustomerId, but I need to broaden that filter to include Contracts to which that Customer is a parent (ie, filter on the Contracts for that Account/Contact and also the Contracts for the Parent Account (of a Contact or of a Sub-Account).

Any ideas?

Stephen Maij said...

Hi Adi Katz,

Nice work :)

I use this script to preload lookup's.

countryDiv = document.all.ctac_countryid_d.getElementsByTagName("DIV")[0];
countryDiv.innerText = "NL";
setTimeout("crmForm.all.ctac_countryid.Lookup(true, true, 'NL', true)", 1000);

Kind regards,

Stephen
www.maijspace.nl

Adi Katz said...

Looks familiar:

http://forums.microsoft.com/Dynamics/ShowPost.aspx?PostID=3076736&SiteID=27

Adi

Stephen Maij said...

Yes its similar but I changed a couple of things to make it work in all situations. Sometimes I was getting a overflow exception and sometimes the lookup wasn't resolving the text to a guid.

Keep up the good work!

Kind regards,

Stephen

Anonymous said...

Hi. How can I retrieve currencyname by using transactioncurrencyid? I'm sorry I'm very new to all of this I don't really get the gist of the logic behind what you wrote above. Sorry!

Adi Katz said...

Hi,

Post a new thread on ms forums and I’m sure that I or others will be able to help you out.

It’s impossible to understand the exact scenario from your comment.

Adi

Anonymous said...

Hi Adi,
It seems to be very helpfull, but
(exclusing lines 4-37), how do I
use the code?
Does this filter responsiblecontactid by customerid
in Incident form?

CSC said...

Hi Adi,
Congratz for this post, I find it really useful. I have some posts about lookups and how to create a custom filtered lookup view in runtime - CRM 2011. I'd be honored if we can make a blog link exchange. My blog is http://crmstuff.blogspot.com . Are you interested in this exchange? Thanks, Cornel