Changing Search Behavior in Microsoft CRM 2011
In this post, we’re going to show you how you can completely take over Search Functionality in Microsoft CRM 2011 in a fully supported manner. The default behavior of the Search Box is to Search for the term you enter against the Entities on the grid below it. It uses a BeginsWith predicate so you have to know the first few letters of the Entity you’re looking for. While this is sufficient most of the time, there are plenty of cases where you might only know another piece of the name (Think of a Law Partnership that has several names, and you can only remember the name of your attorney whose name appears at the end of the list. Having a true LIKE based search vs a BeginsWith search would be quite handy. For now I’ll show you how easy that is to do. However you can do so much more. You can easily modify this to include a LIKE Based Search along with finding matches in all related entities including all relationship types. So yes, you could search for “Dev” and it would find any say, Account with “Dev” anywhere in the accountname, but it would also find any contact with Bill in specified fields, and opportunity, and Article and any other entity you wanted to include. I’ll show you the easy way first and cover the more complex scenarios in later articles
There’s a common belief/perception that within MSCRM, you can’t change any of the Out of the Box behavior in a supported manner. You can modify all sorts of things and you can build custom solutions to meet your needs, but when it comes to changing the default behaviors, not much can be done. That’s the belief anyway. We’ve had many clients request customized searches and each request is different, but they all stem from the nature of the way MSCM’s search works and needing a more expansive mechanism…. Most involved using SQL Server Full-Text Search , building a Full-Text index on the Entity’s underlying attributes fields that need to be search and then displaying the results in an ASP.NET Grid, a JavaScript grid or Silverlight Control.
Another common technique is to build custom entities that contain pieces of meta-data about your target and allow those to be searched (they’d usually contain a hyperlink or similar hook to the content that’s desired).
One reason people think OOB Search can’t be changed is the fact you aren’t allowed to modify the underlying ASP.NET pages or much of the out of the box content in any supported manner. And while Unsupported to those new to MSCRM, sounds like it’s optional, it’s always usually a very big no-no and means you’re likely going to cause yourself a bunch of headaches down the road. For client engagements, unless the clients specifically tell you to do something unsupported after you’ve explained all the downsides and warned them against it, you may be asked to do it anyway. Most people aren’t willing to take the risks once they understand them, but there are always exceptions.
Just to make sure everyone understands what we’re talking about, below is a picture of a typical Account screen with the Search box in the Upper Right Hand corner:
In this case, if you searched for “Bes” you’d get one hit returned, Best o’ Things. But if you searched for “Goods” you’d get nothing returned. That’s what we’ll address here.
The feature that enables you to pull all of this off is the message support that you can register Plugins to. I personally have been doing MSCRM development for quite a while, written more Plugins than I can count, and it never really dawned on me that this was possible. In this post, I changed the Plugin Registration tool to display the available messages a little differently. My friend and former co-worker Nirnay Patel had a post which goes through all the messages. If you’ve registered a plugin, you’ve seen all of those, but in almost every case you only think of using a few of them, namely Create, Update & Delete. But there are so many more and well, it only makes sense that they’re there for a reason. We also know that everything that’s done in the CRM Application does its thing by calling the OrganizationService or DiscoveryService. That’s where the light went off. What method must be fired when you execute a Search? RetrieveMultiple comes to mind. So I checked the message list and low and behold, RetrieveMultiple is in fact a supported message. Not only is it supported for a very large # of Entities, one of them includes UserQuery.
So, the key to everything here is to build a plugin that fires on the RetrieveMultiple event. Here’s the first snippet that gets us there:
public class SearchPlugin : IPlugin { /// <summary> /// String literal containing the value "Query" /// </summary> public const String QueryLiteral = "Query"; public const String LIKE = "%"; public void Execute(IServiceProvider serviceProvider) { #region String ParentEntity = String.Empty; String OriginalSearch = String.Empty; #endregion // Obtain the execution context from the service provider. var ContextInstance = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Get a reference to the Organization service. IOrganizationService ServiceInstance =((IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory))).CreateOrganizationService(ContextInstance.InitiatingUserId); // NOTICE that the InputParameters Contains the word Query if (ContextInstance.Depth < 2 && ContextInstance.InputParameters.Contains(QueryLiteral) && ContextInstance.InputParameters[QueryLiteral] is QueryExpression)
{ QueryExpression QueryPointer = (ContextInstance.InputParameters[QueryLiteral] as QueryExpression);
The first part of this should be nothing new, but the portions of code in Bold will probably be new. As you can see, we’re getting a reference to an InputParameter, but instead of TARGET, or Entity or any of the usual items, it’s a QueryExpression. So the first thing we’re doing is grabbing a reference to the QueryExpression.
Now is where things get interesting. What we’re going to do is actually Modify the QueryExpression from the InputParameter. So when the Plugin Finishes firing, a new QueryExpression will be returned to CRM and the modified QueryExpression is the one that will be executed. Now, keep in mind we’re only making one small modification to our QueryExpression but we could do all sorts of things. You can use a configuration entity like I discussed in yesterday’s post to provide the names of other entities you wanted to search, you can give it the names of the attributes you wanted to search and specify the relationships between them all. B/c you have access to the QueryExpression before it executes, you can add LinkEntity items, FilterExpression, ConditionExpression and just about anything else. You can even perform intermediate queries against other entities to get the IDs (Guids) of them to set as part of values, effectively hard coding in references to other entities. With that much power, there’s pretty much nothing that’s off the table. You can include related entities, you can include entities that are grandchildren, greatgrandchildren etc. You can even get non-related entities. And you can configure this so that the behavior can be modified by End users. This in essence would be providing the end users with a fully configurable way to manage search. Everything else is configurable, Search is very important, so why shouldn’t it be something people can change right? Anyway, here’s the remaining part of the code:
if (null != QueryPointer) { // Check if the request is coming from any Search View // We know this b/c Criteria isn't null and the Filters Count > 1 if (null != QueryPointer.Criteria && QueryPointer.Criteria.Filters.Count > 1) { ParentEntity = ContextInstance.PrimaryEntityName; OriginalSearch = QueryPointer.Criteria.Filters[1].Conditions[0].Values[0].ToString(); OriginalSearch = String.Format(CultureInfo.CurrentCulture, "{0}{1}", LIKE,OriginalSearch); } ConditionExpression NewCondition = null; FilterExpression NewFilter = null; if (null != QueryPointer.Criteria) { //Change the default BeginsWith to Contains/Like in the basic search query foreach (FilterExpression FilterSet in QueryPointer.Criteria.Filters) { foreach (ConditionExpression ConditionSet in FilterSet.Conditions) { if (ConditionSet.Operator == ConditionOperator.Like) { ConditionSet.Values[0] = OriginalSearch; } } } }} ContextInstance.InputParameters[QueryLiteral] = QueryPointer;
It looks like a lot is going on here, but keep this in mind. In the default QueryExpression, we basically have a predicate that says “Where AccountName LIKE SearchValue%”. The wildcard is included in the value that’s being set on the ConditionSet. So all we’re doing is changing that value to include a Wildcard at the beginning as well – so it now looks like “WHERE AccountName LIKE %SearchValue%” (And honestly, if you step through the code as it executes, you’ll see that this is literally what’s being done. So we look for the LIKE Condition and change the value. That’s it. Then, we simply return this updated QueryExpression to the CRM Instance.
As I said, we could modify just about anything. We could conceivably change the search term altogether if we wanted (although not sure why you’d want to). More common scenarios might be to add a filter condition to put an AND AdministrativeViewingOnly = False if the UserId isn’t an administrator (there are other ways , and probably better ones to accomplish restricting access, but I”m just trying to drive home what all can be done>).
The most common ask is to have related entities searched and have them searched in a specific way. Accounts + Contacts for instance. Opportunity + OpportunityProducts. You get the idea. In addition to these, you may have a few other Child Entities that you want to include, and you can easily include the entities and define what fields are searched in configuration and then have the QueryExpression dynamically updated to include all of these. It’s very powerful and with the proper tooling you could give the clients an amazingly powerful search screen that they can configure as they please.
Provided I have time, I’ll show you how this is done by creating a configuration Entity, a User Friendly Form, and then some decision logic inside the Plugin that completely modifies the QueryExpression in ways a little more substantive than just changing the Search Value.
The full code is provided below for your reference. It is also available for download here:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; using System.Globalization; namespace Xrm.Plugins { public class SearchPlugin : IPlugin { /// <summary> /// String literal containing the value "Query" /// </summary> public const String QueryLiteral = "Query"; public const String LIKE = "%"; public void Execute(IServiceProvider serviceProvider) { #region String ParentEntity = String.Empty; String OriginalSearch = String.Empty; #endregion // Obtain the execution context from the service provider. var ContextInstance = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Get a reference to the Organization service. IOrganizationService ServiceInstance = ((IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory))). CreateOrganizationService(ContextInstance.InitiatingUserId); // Critical Point here - NOTICE that the InputParameters Contains the word Query if (ContextInstance.Depth < 2 && ContextInstance.InputParameters.Contains(QueryLiteral) && ContextInstance.InputParameters[QueryLiteral] is QueryExpression) { QueryExpression QueryPointer = (ContextInstance.InputParameters[QueryLiteral] as QueryExpression); //Verify the conversion worked as expected - if not, everything else is useless if (null != QueryPointer) { // Check if the request is coming from any Search View // We know this b/c Criteria isn't null and the Filters Count > 1 if (null != QueryPointer.Criteria && QueryPointer.Criteria.Filters.Count > 1) { ParentEntity = ContextInstance.PrimaryEntityName; OriginalSearch = QueryPointer.Criteria.Filters[1].Conditions[0].Values[0].ToString(); OriginalSearch = String.Format(CultureInfo.CurrentCulture, "{0}{1}", LIKE,OriginalSearch); } ConditionExpression NewCondition = null; FilterExpression NewFilter = null; if (null != QueryPointer.Criteria) { //Change the default 'BeginsWith'Operator to 'Contains/Like' operator in the basic search query foreach (FilterExpression FilterSet in QueryPointer.Criteria.Filters) { foreach (ConditionExpression ConditionSet in FilterSet.Conditions) { if (ConditionSet.Operator == ConditionOperator.Like) { ConditionSet.Values[0] = OriginalSearch; } } } } } ContextInstance.InputParameters[QueryLiteral] = QueryPointer; } } } }
————————————————————–
KeyWords: Dynamics4, Dynamics4.com, QueryByAttribute, QueryExpression, OrganizationContext, Plugin,IServiceProvider, Plugin Assembly, Microsoft CRM Search, ConditionExpression, FilterExpression, IPluginExecutionContextIOrganizationServiceFactory, ConditionSet, FilterSet, CrmConnection, IOrganizationService William Ryan, Bill Ryan, QueryByAttribute, Customize MSCRM Search, Customize Dynamics CRM Search, Customizing Microsoft Dynamics CRM 2011 Search, Dynamics4, CRM South East, Atlanta, Greenville, Miami
The post Changing Search Behavior in Microsoft CRM 2011 – Part 1 appeared first on dynamics four.