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.
Drop-down selection of a Primary Contact for an Account
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:
Adding a dummy Picklist attribute to the Account screen in MSCRM in order to allow for a Filtered Drop-down selection of a Primary Contact
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:
015 | PopulateDropdown = function (filteredDropdown, filterCondition, entityType, recordIdAttribute, recordNameAttribute, recordAdditionalAttribute, relationshipAttribute, allowNull, defaultSelect) { |
017 | var includeAdditionalAttribute = false ; |
019 | filteredDropdown.length = 0; |
020 | if (allowNull == true ) { |
021 | filteredDropdown.AddOption( "" , null ); |
025 | if (recordAdditionalAttribute != "" ) { |
026 | includeAdditionalAttribute = true ; |
031 | "<fetch mapping=\"logical\">" + |
032 | "<entity name=\"" + entityType + "\">" + |
033 | "<attribute name=\"" + recordIdAttribute + "\" />" + |
034 | "<attribute name=\"" + recordNameAttribute + "\" />" ; |
036 | if (includeAdditionalAttribute == true ) { |
037 | fetchXml += "<attribute name=\"" + recordAdditionalAttribute + "\" />" ; |
040 | fetchXml += filterCondition + |
045 | var resultXml = FetchXmlResponse(fetchXml); |
047 | var oXmlDoc = new ActiveXObject( "Microsoft.XMLDOM" ); |
048 | oXmlDoc.async = false ; |
049 | oXmlDoc.loadXML(resultXml.text); |
052 | var result = oXmlDoc.getElementsByTagName( 'result' ); |
054 | if (result != null ) { |
056 | var lookupItem = new Array; |
058 | var optionSelected = false ; |
060 | lookupItem = relationshipAttribute.DataValue; |
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); |
067 | if (record_name[0] != null ) { |
068 | optionText = record_name[0].text; |
071 | if (includeAdditionalAttribute == true ) { |
072 | var record_additional = result[n].getElementsByTagName(recordAdditionalAttribute); ; |
074 | if (record_additional[0] != null ) { |
075 | optionText += ", " + record_additional[0].text; |
079 | filteredDropdown.AddOption(optionText, n); |
081 | dropdownOptionText[n] = record_name[0].text; |
082 | dropdownOptionValues[n] = record_id[0].text; |
084 | if (lookupItem != null ) { |
085 | if (lookupItem[0].id == record_id[0].text) { |
086 | filteredDropdown.DataValue = n; |
087 | optionSelected = true ; |
093 | if (optionSelected == false ) { |
094 | if (result.length > 0) { |
095 | if (defaultSelect == true ) { |
097 | filteredDropdown.DataValue = 1; |
103 | filteredDropdown.onchange = function () { |
104 | DropdownSelection(filteredDropdown, entityType, relationshipAttribute); |
109 | alert( "PopulateDropdown - " + ex.description + "." ); |
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:
01 | function htmlEncode(source, display, tabs) { |
02 | function special(source) { |
04 | for ( var i = 0; i < source.length; i++) { |
05 | var c = source.charAt(i); |
06 | if (c < ' ' || c > '~' ) { |
07 | c = '&#' + c.charCodeAt() + ';' ; |
14 | function format(source) { |
16 | tabs = (tabs >= 0) ? Math.floor(tabs) : 4; |
18 | var lines = source.split(/\r\n|\r|\n/); |
21 | for ( var i = 0; i < lines.length; i++) { |
24 | for ( var p = 0; p < line.length; p++) { |
25 | var c = line.charAt(p); |
27 | var spaces = tabs - (newLine.length % tabs); |
28 | for ( var s = 0; s < spaces; s++) { |
37 | newLine = newLine.replace(/(^ )|( $)/g, ' ' ); |
41 | var result = lines.join( '<br />' ); |
43 | result = result.replace(/ /g, ' ' ); |
50 | result = result.replace(/\&/g, '&' ); |
52 | result = result.replace(/\</g, '<' ); |
54 | result = result.replace(/\>/g, '>' ); |
58 | result = format(result); |
63 | result = result.replace( new RegExp( '"' , 'g' ), '"' ); |
66 | result = special(result); |
71 | FetchXmlResponse = function (fetchXml) { |
74 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + |
76 | GenerateAuthenticationHeader() + |
79 | "<fetchXml>" + htmlEncode(fetchXml) + "</fetchXml>" + |
84 | var xmlHttpRequest = new ActiveXObject( "Msxml2.XMLHTTP" ); |
85 | xmlHttpRequest.Open( "POST" , "/mscrmservices/2007/CrmService.asmx" , false ); |
87 | xmlHttpRequest.setRequestHeader( "Content-Type" , "text/xml; charset=utf-8" ); |
88 | xmlHttpRequest.setRequestHeader( "Content-Length" , xml.length); |
90 | xmlHttpRequest.send(xml); |
92 | return xmlHttpRequest.responseXML; |
95 | alert( "FetchXMLResponse - " + ex.description + "" ); |
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.
07 | DropdownSelection = function (filteredDropdown, entityType, relationshipAttribute) { |
09 | var lookupData = new Array(); |
10 | var lookupItem = new Object(); |
13 | if (filteredDropdown.DataValue != null ) { |
15 | lookupItem.id = dropdownOptionValues[filteredDropdown.DataValue]; |
16 | lookupItem.typename = entityType; |
17 | lookupItem.name = dropdownOptionText[filteredDropdown.DataValue]; |
18 | lookupData[0] = lookupItem; |
21 | relationshipAttribute.DataValue = lookupData; |
22 | relationshipAttribute.ForceSubmit = true ; |
26 | relationshipAttribute.DataValue = null ; |
27 | relationshipAttribute.ForceSubmit = true ; |
31 | alert( "DropdownSelection - " + ex.description + "." ); |
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.
2 | var dropdownOptionText = new Array(); |
3 | var dropdownOptionValues = new Array(); |
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:
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" + "'/>" + |
09 | PopulateDropdown(crmForm.all.new_contactdropdown, filterCondition, "contact" , "contactid" , "fullname" , "jobtitle" , crmForm.all.primarycontactid, true , false ); |
13 | alert( "Main - " + ex.description + "." ); |
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:
1 | if ( crmForm.all.new_contactdropdown.DataValue != null ) |
3 | crmForm.all.new_contactdropdown.DataValue = null ; |
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:
Entity Relationship Digram for showing an example of using this Filtered Lookup concept
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.
Selecting the Buying Relationship via non-filtered Lookup between the Customer Account and Supplier Account
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 () { |
03 | var customerAccountId = null ; |
04 | var supplierAccountId = null ; |
06 | var custLookupItem = new Array; |
07 | custLookupItem = crmForm.all.customerid.DataValue; |
09 | if (custLookupItem != null ) { |
10 | customerAccountId = custLookupItem[0].id; |
13 | var suppLookupItem = new Array; |
14 | suppLookupItem = crmForm.all.new_supplieraccountid.DataValue; |
16 | if (suppLookupItem != null ) { |
17 | supplierAccountId = suppLookupItem[0].id; |
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 ); |
28 | alert( "PopulateBuyingRelationshipsList - " + ex.description + "." ); |
34 | PopulateBuyingRelationshipsList(); |
37 | alert( "Main - " + ex.description + "." ); |
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.
Dummy Picklist field showing the list of possible Buying Relationships between the selected Customer and Supplier
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.