Filtered Lookup as a Drop-down Picklist in Microsoft Dynammics CRM
The ability to add a Filtered Lookup is one of those areas of core functionality that MSCRM currently lacks – this thankfully will be resolved in the forthcoming v5 of MSCRM, however at the moment we are forced to use extensions or customisations to replicate this functionality.
Given the extendibility of the platform, there are many methods for implementing a Filtered Lookup in MSCRM:
- Use a Custom Developed ASP.NET screen to select and then populate the relationship.
- Purchase the popular Stunnware Filtered Lookup tool here.
- Hack the existing Lookup field type and screen to contain a custom filter, this is completely unsupported and so not advised but you can find details on this here.
And I am sure there are many other methods beyond, this post aims to outline a simple pure-Javascript method of implementing a Filtered Lookup as a Picklist. This method has natural advantages and disadvantages but can be a simple solution for when time or budget may be an issue.
STEP 1 – Add a Dummy Picklist field to the Entity involved
This method aims to use a Picklist as the user interface for a drop-down selection of the Filtered Lookup, as such we need to create this Picklist as a new field. This is wasteful as the Picklist will simply be used for the presentation and not actually store anything, however this simply allows us to maintain a set of picklist values:
As a side note, we could use onLoad scripting to add a SELECT HTML Tag into the Entity screen to create a temporary non-MSCRM field Picklist, however this would involve injecting HTML into the Form’s Document Object Model and so would push us into unsupported territory.
STEP 2 – Add a Javascript Function to Populate the Dummy Picklist
We can implement this Filtered Drop-down via a series of Javascript functions within the onLoad script of a particular entity – starting with a function to populate the initial list of drop-down options:
This relies on other Javascript functions to send a XML Message to the MSCRM Web Service in order to receive a XML Response back from the MSCRM Web Service – this is common technique when working with MSCRM (althrough not one to be over-used as Javascript can quickly bloat for the various onLoad events) with a number of possible implementations.
The following script, which is re-used several times across this blog, is one possible implementation of this:
Microsoft post another possible implementation of this as Scripting Sample Code
here. There is also a good detailed description of how and why this AJAX concept works (as opposed to lines of script) here on Customer Effective’s shared blog.
STEP 3 – Add a second Javascript Function for populating the real Lookup relationship upon selection of the Dummy Picklist
However looking back at the first piece of scripting, the highlighted line in the script invokes another Javascript function to ensure that any selection made in the Filtered Drop-down populates the actual Lookup relationship.
For these two Javascript functions to function together – we need to declare a ‘global’ pair of arrays in the script to map a value selected in the Filtered Drop-down to the actual GUID value that needs to be populated into the associated Lookup field.
STEP 4 – Invoke the functions with the relevant Relationship Values
Finally with these pieces of script in place on our onLoad event – we can invoke a Main (i.e. non-function) piece of scripting to invoke these scripting functions for the particular Picklist field, Relationship and Lookup field that we wish to filter:
We can implement this via the following Javascript to produce a filtered drop-down listing Contacts that are associated to a particular Account, which then allows the CRM User to select a particular Primary Contact from the filtered drop-down.
The final point here is to wipe the selected value of the Dummy Picklist on save of the form with the following onSave script:
This is an unfortunate step as is hacking/changing the user’s entered DataValue, however is mostly unavoidable given how we are using the Picklist. (we could avoid this by creating a number of dummy selections in the Picklist’s possible values – however this is tricky as in most cases we cannot predict how many selections the picklist is going to have from the Filter Condition.)
Practical Example
The above scripting produces a drop-down list of Contacts from the Account screen, however I would rarely suggest using several large pieces of script to solve such a simple problem and would more often advise a client that the non-filtered selection of a Primary Contact is simply a current limitation of MSCRM.
However in other instances this concept of using a Filtered Lookup can be much more key to a project – this final section of the post aims to show one particular instance as way of an example of this.
If we take a business model which handles Orders between Customers and Suppliers, such that each Sales Order has a relationship to a Customer Account and a Supplier Account, and also needs to track an Account Number or Relationship Number that denotes the relationship between the particular Customer and Supplier:
In this instance, we may want the CRM User to specify a Customer and Supplier for an Order and then be presented with the list of Selling Relationships (if present) that link this Customer to the Supplier as way of authorising the purchase between the two parties.
By default MSCRM Behaviour, this will be a Buying Relationship Lookup showing a full list of the Buying Relationship records regardless of the Customer or Supplier selected. This would be a fairly negative experience for the User to have to search for the particular Buying Relationship records that applied for the Customer and Supplier they had already specified.
To improve this, we can use the script outlined earlier amended slightly to refresh a Buying Relationship Drop-down based on the Customer and Supplier selected:
With this in place, we can hide the actual Buying Relationship Lookup field in the Administration tab and show the Dummy Picklist field on the General tab to present a simple selection of Buying Relationships once a Customer or Supplier has been selected.
NOTE: The Customer and Supplier onChange events would naturally need to invoke the PopulateBuyingRelationshipsList() function to facilitate this happening on selection.
This gives a much simpler selection – with the added benefit that via the [defaultSelect] parameter we saw in the earlier script the first Buying Relationship between the Customer and Supplier will immediately be selected as the default, this may also aid the User Experience.
Summary
Filtered Lookup functionality will be standard customisation in the forthcoming MSCRM v5, however in the meantime, the ability here to add a simple Filtered Dropdown through Form Scripting can be a useful technique – particularly for smaller lists of information or when the filter needs to change in real-time.
Given the extendibility of the platform, there are many methods for implementing a Filtered Lookup in MSCRM:
- Use a Custom Developed ASP.NET screen to select and then populate the relationship.
- Purchase the popular Stunnware Filtered Lookup tool here.
- Hack the existing Lookup field type and screen to contain a custom filter, this is completely unsupported and so not advised but you can find details on this here.
And I am sure there are many other methods beyond, this post aims to outline a simple pure-Javascript method of implementing a Filtered Lookup as a Picklist. This method has natural advantages and disadvantages but can be a simple solution for when time or budget may be an issue.
STEP 1 – Add a Dummy Picklist field to the Entity involved
This method aims to use a Picklist as the user interface for a drop-down selection of the Filtered Lookup, as such we need to create this Picklist as a new field. This is wasteful as the Picklist will simply be used for the presentation and not actually store anything, however this simply allows us to maintain a set of picklist values:
As a side note, we could use onLoad scripting to add a SELECT HTML Tag into the Entity screen to create a temporary non-MSCRM field Picklist, however this would involve injecting HTML into the Form’s Document Object Model and so would push us into unsupported territory.
STEP 2 – Add a Javascript Function to Populate the Dummy Picklist
We can implement this Filtered Drop-down via a series of Javascript functions within the onLoad script of a particular entity – starting with a function to populate the initial list of drop-down options:
001 | /// <summary> |
002 | /// Populates a particular Picklist field with values retrieved from the MSCRM Web Service |
003 | /// </summary> |
004 | /// <param name="filteredDropdown">The CRM Picklist control to use as the Filtered Drop-down</param> |
005 | /// <param name="filterCondition">XML describing the CRM Filter for retrieving the records for the filtered drop-down</param> |
006 | /// <param name="entityType">Describes the Entity Type involved for retrieving the records for the filtered drop-down</param> |
007 |
008 | /// <param name="recordIdAttribute">Specifies the Database name of the Primary Key field for the Entity Type involved - should always be [entityType] + 'id'</param> |
009 | /// <param name="recordNameAttribute">Specifies the Database name of a String Name field to retrieve for the Drop-down</param> |
010 | /// <param name="recordAdditionalAttribute">Optional, specifies the Database name of an additional field to retrieve for the Drop-down</param> |
011 |
012 | /// <param name="relationshipAttribute">Specifies the CRM Lookup control to populate the relationship when a value is chosen from the Filtered Picklist</param> |
013 | /// <param name="allowNull">Specifies whether the filtered Picklist should include a Blank option</param> |
014 | /// <param name="defaultSelect">Specifies whether the filtered Picklist automatically selects the first option upon population</param> |
015 | PopulateDropdown = function (filteredDropdown, filterCondition, entityType, recordIdAttribute, recordNameAttribute, recordAdditionalAttribute, relationshipAttribute, allowNull, defaultSelect) { |
016 | try { |
017 | var includeAdditionalAttribute = false ; |
018 |
019 | filteredDropdown.length = 0; |
020 | if (allowNull == true ) { |
021 | filteredDropdown.AddOption( "" , null ); |
022 | } |
023 |
024 | // determine whether to inclue an additional attribute in the drop-down list (such as Job Title) |
025 | if (recordAdditionalAttribute != "" ) { |
026 | includeAdditionalAttribute = true ; |
027 | } |
028 |
029 | // Build the Fetch XML Message to run against the MSCRM Webservice |
030 | var fetchXml = |
031 | "<fetch mapping=\"logical\">" + |
032 | "<entity name=\"" + entityType + "\">" + |
033 | "<attribute name=\"" + recordIdAttribute + "\" />" + |
034 | "<attribute name=\"" + recordNameAttribute + "\" />" ; |
035 |
036 | if (includeAdditionalAttribute == true ) { |
037 | fetchXml += "<attribute name=\"" + recordAdditionalAttribute + "\" />" ; |
038 | } |
039 |
040 | fetchXml += filterCondition + |
041 | "</entity>" + |
042 | "</fetch>" ; |
043 |
044 | // Invoke function (defined above) to send the XML to the MSCRM Webservice |
045 | var resultXml = FetchXmlResponse(fetchXml); |
046 |
047 | var oXmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); |
048 | oXmlDoc.async = false ; |
049 | oXmlDoc.loadXML(resultXml.text); |
050 |
051 | // Parse the returned XML Response returned from the MSCRM Webservice |
052 | var result = oXmlDoc.getElementsByTagName( 'result' ); |
053 |
054 | if (result != null ) { |
055 | var n; |
056 | var lookupItem = new Array; |
057 | var optionText; |
058 | var optionSelected = false ; |
059 |
060 | lookupItem = relationshipAttribute.DataValue; |
061 |
062 | // cycle through results returned from the MSCRM Web Service |
063 | for (n = 0; n <= (result.length - 1); n++) { |
064 | var record_id = result[n].getElementsByTagName(recordIdAttribute); |
065 | var record_name = result[n].getElementsByTagName(recordNameAttribute); |
066 |
067 | if (record_name[0] != null ) { |
068 | optionText = record_name[0].text; |
069 | } |
070 |
071 | if (includeAdditionalAttribute == true ) { |
072 | var record_additional = result[n].getElementsByTagName(recordAdditionalAttribute); ; |
073 |
074 | if (record_additional[0] != null ) { |
075 | optionText += ", " + record_additional[0].text; |
076 | } |
077 | } |
078 |
079 | filteredDropdown.AddOption(optionText, n); |
080 |
081 | dropdownOptionText[n] = record_name[0].text; |
082 | dropdownOptionValues[n] = record_id[0].text; |
083 |
084 | if (lookupItem != null ) { |
085 | if (lookupItem[0].id == record_id[0].text) { |
086 | filteredDropdown.DataValue = n; |
087 | optionSelected = true ; |
088 | } |
089 | } |
090 | } |
091 |
092 | // has the dropdown selected option being populated by a pre-defined value in the relationship? |
093 | if (optionSelected == false ) { |
094 | if (result.length > 0) { |
095 | if (defaultSelect == true ) { |
096 | // default select to the first option populated |
097 | filteredDropdown.DataValue = 1; |
098 | } |
099 | // otherwise a blank value will be selected |
100 | } |
101 | } |
102 |
103 | filteredDropdown.onchange = function () { |
104 | DropdownSelection(filteredDropdown, entityType, relationshipAttribute); |
105 | }; |
106 | } |
107 | } |
108 | catch (ex) { |
109 | alert( "PopulateDropdown - " + ex.description + "." ); |
110 | } |
111 | } |
The following script, which is re-used several times across this blog, is one possible implementation of this:
01 | function htmlEncode(source, display, tabs) { |
02 | function special(source) { |
03 | var result = '' ; |
04 | for ( var i = 0; i < source.length; i++) { |
05 | var c = source.charAt(i); |
06 | if (c < ' ' || c > '~' ) { |
07 | c = '&#' + c.charCodeAt() + ';' ; |
08 | } |
09 | result += c; |
10 | } |
11 | return result; |
12 | } |
13 |
14 | function format(source) { |
15 | // Use only integer part of tabs, and default to 4 |
16 | tabs = (tabs >= 0) ? Math.floor(tabs) : 4; |
17 | // split along line breaks |
18 | var lines = source.split(/\r\n|\r|\n/); |
19 |
20 | // expand tabs |
21 | for ( var i = 0; i < lines.length; i++) { |
22 | var line = lines[i]; |
23 | var newLine = '' ; |
24 | for ( var p = 0; p < line.length; p++) { |
25 | var c = line.charAt(p); |
26 | if (c === '\t' ) { |
27 | var spaces = tabs - (newLine.length % tabs); |
28 | for ( var s = 0; s < spaces; s++) { |
29 | newLine += ' ' ; |
30 | } |
31 | } |
32 | else { |
33 | newLine += c; |
34 | } |
35 | } |
36 | // Leading or ending spaces will be removed from a HTML Request, unless flagged as a nbsp type character |
37 | newLine = newLine.replace(/(^ )|( $)/g, ' ' ); |
38 | lines[i] = newLine; |
39 | } |
40 | // re-join lines |
41 | var result = lines.join( '<br />' ); |
42 | // break up contiguous blocks of spaces with non-breaking spaces |
43 | result = result.replace(/ /g, ' ' ); |
44 |
45 | return result; |
46 | } |
47 | var result = source; |
48 |
49 | // ampersands (&) |
50 | result = result.replace(/\&/g, '&' ); |
51 | // less-thans (<) |
52 | result = result.replace(/\</g, '<' ); |
53 | // greater-thans (>) |
54 | result = result.replace(/\>/g, '>' ); |
55 |
56 | if (display) { |
57 | // format for display |
58 | result = format(result); |
59 | } |
60 | else { |
61 | // Replace quotes if it isn't for display, |
62 | // since it's probably going in an html attribute. |
63 | result = result.replace( new RegExp( '"' , 'g' ), '"' ); |
64 | } |
65 |
66 | result = special(result); |
67 | return result; |
68 | } |
69 |
70 | // Fires an XML Message to the MSCRM Web Service |
71 | FetchXmlResponse = function (fetchXml) { |
72 | try { |
73 | var xml = |
74 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + |
75 | "<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\">" + |
76 | GenerateAuthenticationHeader() + |
77 | "<soap:Body>" + |
78 | "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" + |
79 | "<fetchXml>" + htmlEncode(fetchXml) + "</fetchXml>" + |
80 | "</Fetch>" + |
81 | "</soap:Body>" + |
82 | "</soap:Envelope>" ; |
83 |
84 | var xmlHttpRequest = new ActiveXObject( "Msxml2.XMLHTTP" ); |
85 | xmlHttpRequest.Open( "POST" , "/mscrmservices/2007/CrmService.asmx" , false ); |
86 | xmlHttpRequest.setRequestHeader( "SOAPAction" , "http://schemas.microsoft.com/crm/2007/WebServices/Fetch" ); |
87 | xmlHttpRequest.setRequestHeader( "Content-Type" , "text/xml; charset=utf-8" ); |
88 | xmlHttpRequest.setRequestHeader( "Content-Length" , xml.length); |
89 |
90 | xmlHttpRequest.send(xml); |
91 |
92 | return xmlHttpRequest.responseXML; |
93 | } |
94 | catch (ex) { |
95 | alert( "FetchXMLResponse - " + ex.description + "" ); |
96 | } |
97 | } |
STEP 3 – Add a second Javascript Function for populating the real Lookup relationship upon selection of the Dummy Picklist
However looking back at the first piece of scripting, the highlighted line in the script invokes another Javascript function to ensure that any selection made in the Filtered Drop-down populates the actual Lookup relationship.
01 | /// <summary> |
02 | /// Function that runs when a drop-down value has been selected in our custom Filtered Drop-down |
03 | /// </summary> |
04 | /// <param name="filteredDropdown">The CRM Picklist control used as the Filtered Drop-down</param> |
05 | /// <param name="entityType">Describes the Entity Type involved for the relationship</param> |
06 | /// <param name="relationshipAttribute">Specifies the CRM Lookup control to populate with the selected value for the relationship</param> |
07 | DropdownSelection = function (filteredDropdown, entityType, relationshipAttribute) { |
08 | try { |
09 | var lookupData = new Array(); |
10 | var lookupItem = new Object(); |
11 |
12 | // check that the Picklist is not blank |
13 | if (filteredDropdown.DataValue != null ) { |
14 | // build Lookup Value to populate for the Lookup field in the relationship |
15 | lookupItem.id = dropdownOptionValues[filteredDropdown.DataValue]; |
16 | lookupItem.typename = entityType; |
17 | lookupItem.name = dropdownOptionText[filteredDropdown.DataValue]; |
18 | lookupData[0] = lookupItem; |
19 |
20 | // populate the Lookup field in the relationship as the Lookup Value |
21 | relationshipAttribute.DataValue = lookupData; |
22 | relationshipAttribute.ForceSubmit = true ; |
23 | } |
24 | else { |
25 | // populate the Lookup field in the relationship as Blank |
26 | relationshipAttribute.DataValue = null ; |
27 | relationshipAttribute.ForceSubmit = true ; |
28 | } |
29 | } |
30 | catch (ex) { |
31 | alert( "DropdownSelection - " + ex.description + "." ); |
32 | } |
33 | } |
1 | // 'global' arrays to store assocation between values in Filtered Dropdown and Relationship values |
2 | var dropdownOptionText = new Array(); |
3 | var dropdownOptionValues = new Array(); |
Finally with these pieces of script in place on our onLoad event – we can invoke a Main (i.e. non-function) piece of scripting to invoke these scripting functions for the particular Picklist field, Relationship and Lookup field that we wish to filter:
01 | try { |
02 | // Main |
03 | if (crmForm.FormType == 2) { |
04 | var filterCondition = "<filter type='and'>" + |
05 | "<condition attribute='parentcustomerid' operator='eq' value='" + crmForm.ObjectId + "'/>" + |
06 | "<condition attribute='statecode' operator='eq' value='" + "0" + "'/>" + |
07 | "</filter>" ; |
08 |
09 | PopulateDropdown(crmForm.all.new_contactdropdown, filterCondition, "contact" , "contactid" , "fullname" , "jobtitle" , crmForm.all.primarycontactid, true , false ); |
10 | } |
11 | } |
12 | catch (ex) { |
13 | alert( "Main - " + ex.description + "." ); |
14 | } |
The final point here is to wipe the selected value of the Dummy Picklist on save of the form with the following onSave script:
1 | if ( crmForm.all.new_contactdropdown.DataValue != null ) |
2 | { |
3 | crmForm.all.new_contactdropdown.DataValue = null ; |
4 | } |
Practical Example
The above scripting produces a drop-down list of Contacts from the Account screen, however I would rarely suggest using several large pieces of script to solve such a simple problem and would more often advise a client that the non-filtered selection of a Primary Contact is simply a current limitation of MSCRM.
However in other instances this concept of using a Filtered Lookup can be much more key to a project – this final section of the post aims to show one particular instance as way of an example of this.
If we take a business model which handles Orders between Customers and Suppliers, such that each Sales Order has a relationship to a Customer Account and a Supplier Account, and also needs to track an Account Number or Relationship Number that denotes the relationship between the particular Customer and Supplier:
In this instance, we may want the CRM User to specify a Customer and Supplier for an Order and then be presented with the list of Selling Relationships (if present) that link this Customer to the Supplier as way of authorising the purchase between the two parties.
By default MSCRM Behaviour, this will be a Buying Relationship Lookup showing a full list of the Buying Relationship records regardless of the Customer or Supplier selected. This would be a fairly negative experience for the User to have to search for the particular Buying Relationship records that applied for the Customer and Supplier they had already specified.
To improve this, we can use the script outlined earlier amended slightly to refresh a Buying Relationship Drop-down based on the Customer and Supplier selected:
01 | PopulateBuyingRelationshipsList = function () { |
02 | try { |
03 | var customerAccountId = null ; |
04 | var supplierAccountId = null ; |
05 |
06 | var custLookupItem = new Array; |
07 | custLookupItem = crmForm.all.customerid.DataValue; |
08 |
09 | if (custLookupItem != null ) { |
10 | customerAccountId = custLookupItem[0].id; |
11 | } |
12 |
13 | var suppLookupItem = new Array; |
14 | suppLookupItem = crmForm.all.new_supplieraccountid.DataValue; |
15 |
16 | if (suppLookupItem != null ) { |
17 | supplierAccountId = suppLookupItem[0].id; |
18 | } |
19 |
20 | if ((customerAccountId != null ) && (supplierAccountId != null )) { |
21 | var filterCondition = "<condition attribute='new_customeraccountid' operator='eq' value='" + customerAccountId + "' />" + |
22 | "<condition attribute='new_supplieraccountid' operator='eq' value='" + supplierAccountId + "' />" + |
23 | "<condition attribute='statecode' operator='eq' value='" + "0" + "' />" ; |
24 | PopulateDropdown(crmForm.all.new_accountnumber, filterCondition, "new_buyingrelationship" , "new_buyingrelationshipid" , "new_name" , "new_accountnumber" , crmForm.all.new_buyingrelationshipid, true , true ); |
25 | } |
26 | } |
27 | catch (ex) { |
28 | alert( "PopulateBuyingRelationshipsList - " + ex.description + "." ); |
29 | } |
30 | } |
31 |
32 | try { |
33 | // Main |
34 | PopulateBuyingRelationshipsList(); |
35 | } |
36 | catch (ex) { |
37 | alert( "Main - " + ex.description + "." ); |
38 | } |
NOTE: The Customer and Supplier onChange events would naturally need to invoke the PopulateBuyingRelationshipsList() function to facilitate this happening on selection.
This gives a much simpler selection – with the added benefit that via the [defaultSelect] parameter we saw in the earlier script the first Buying Relationship between the Customer and Supplier will immediately be selected as the default, this may also aid the User Experience.
Summary
Filtered Lookup functionality will be standard customisation in the forthcoming MSCRM v5, however in the meantime, the ability here to add a simple Filtered Dropdown through Form Scripting can be a useful technique – particularly for smaller lists of information or when the filter needs to change in real-time.