XAF FilterController case insensitive search

Sometimes you are asked by a customer to implement a functionality that you have already created in the past from someone else.

In this case, a customer and dear friend from Italy asked me “Jose, can we make the XAF filter case insensitive?”

And my answer was yes, I have done in the past for a customer in El Salvador. At that time the approach we used was to save everything in uppercases and do all the searches in uppercases also

I didn’t like that approach at all, so it was time get creative again.

As always I start my research by spending some time reading tickets at https://www.devexpress.com/Support/Center/ which is the greatest source of information for all things related to DevExpress

Here is what I found

  1. https://www.devexpress.com/Support/Center/Question/Details/Q356778/how-can-i-activate-case-insensitive-search-in-lookup-views this approach actually works but the problem is that the name of the fields are hardcoded
  2. https://www.devexpress.com/Support/Center/Question/Details/Q343797/root-listview-filterbytext-include-the-collection-propertys this second approach just append extra nodes to the criteria so its also not good but what I have learned with this example is when is the right moment to alter the criteria.

My first idea was to edit the criteria that were sent to the collection source that means that I need a way to get the values of each node on the criteria, so I can edit them one by one. After some more research, this is what I have found

https://www.devexpress.com/Support/Center/Question/Details/Q539396/criteriaoperator-how-to-get-list-of-parameters-and-their-values

this approach seems perfect to get the properties and values of each node till test it and find out this

Michael (DevExpress Support)6 years ago

It is possible to write a hierarchy walker or visitor, but only for a specific subset of criteria operators.

What Michael means is that only you can only get the values of simple criteria and by simple, I mean the ones that do not involve functions. So, I was set back to step one again.

So, I’m back to step one but let’s see what I’ve learned from the other tickets

  1. I know the exact moment when the criteria are evaluated
  2. I know the criteria that are evaluated and how many nodes it contains
  3. I know which the possible properties are involved in the creation of the criteria

Now this is the flow that I will use to make the search case insensitive

  1. Locate the controller where the search action lives, for that, I can use the property frame and the method get controller
  2. Interrupt the search action, for that, I will wire the event execute of the FilterController FullTextFilterAction action
  3. Find out what are the possible properties that I can use to build the search criteria for that I will use GetFullTextSearchProperties
  4. Build the new criteria, this is somehow tricky because the properties shown in the list view might represent complex objects and that is shown in the column is either the object default property or any other property that can be deep inside the properties tree. Also this step involve find out what is the type of the value being displayed on the list view, if the value is a string I will evaluate its value converted to upper cases using the function upper from the criteria language, if the search value is not a string but a number I will try to cast it to the type of the display column, if the cast is not invalid I will evaluate the search value as a number
  5. Set the new filter to the list view

You can cross reference the points above with the sample code below

 

   using System;
   using System.Collections.Generic;
   using System.Diagnostics;
   using System.Linq;
   using System.Text;
   using DevExpress.Data.Filtering;
   using DevExpress.ExpressApp;
   using DevExpress.ExpressApp.Actions;
   using DevExpress.ExpressApp.Editors;
   using DevExpress.ExpressApp.Layout;
   using DevExpress.ExpressApp.Model;
   using DevExpress.ExpressApp.Model.NodeGenerators;
   using DevExpress.ExpressApp.SystemModule;
   using DevExpress.ExpressApp.Templates;
   using DevExpress.ExpressApp.Utils;
   using DevExpress.Persistent.Base;
   using DevExpress.Persistent.Validation;

   public class FilterControllerCaseInsensitive : ViewController<ListView>
   {
       public FilterControllerCaseInsensitive()
       {

       }
       FilterController standardFilterController;
       protected override void OnActivated()
       {
           standardFilterController = Frame.GetController<FilterController>();
           if (standardFilterController == null)
               return;

           //we should wire the execution of the filter
           standardFilterController.FullTextFilterAction.Execute += FullTextFilterAction_Execute;



       }
       protected override void OnDeactivated()
       {
           base.OnDeactivated();
           if (standardFilterController == null)
               return;

           //we should unwire the execution of the filter
           standardFilterController.FullTextFilterAction.Execute -= FullTextFilterAction_Execute;
       }


       private void FullTextFilterAction_Execute(object sender, ParametrizedActionExecuteEventArgs e)
       {
           //we locate filter with the key FilterController.FullTextSearchCriteriaName then we convert it to case insensitive
           if (!string.IsNullOrEmpty(e.ParameterCurrentValue as string) && View.CollectionSource.Criteria.ContainsKey(FilterController.FullTextSearchCriteriaName))
               View.CollectionSource.Criteria[FilterController.FullTextSearchCriteriaName] = GetCaseInsensitiveCriteria(e.ParameterCurrentValue, View.CollectionSource.Criteria[FilterController.FullTextSearchCriteriaName]);
       }


       private CriteriaOperator GetCaseInsensitiveCriteria(object searchValue, CriteriaOperator initialCriteria)
       {

           //we get a list of all the properties that can be involved in the filter
           var SearchProperties = standardFilterController.GetFullTextSearchProperties();
           //we declare a model class and a property name,the values on this variables will change if we property involve is a navigation property (another persistent object)
           IModelClass ModelClass = null;
           string PropertyName = string.Empty;

           //we declare a list of operators to contains new operators we are going to create
           List<CriteriaOperator> Operator = new List<CriteriaOperator>();
           //we iterate all the properties
           foreach (var CurrentProperty in SearchProperties)
           {
               //here we split the name with a dot, if length is greater than 1 it means its a navigation properties, beware that this may fail with a deep tree of properties like category.subcategory.categoryname
               var Split = CurrentProperty.Split('.');
               if (Split.Length > 1)
               {

                   Debug.WriteLine(string.Format("{0}","its a complex property"));
                   var CurrentClass = this.View.Model.ModelClass;
                   for (int i = 0; i < Split.Length; i++)
                   {

                       //if its a navigation property we locate the type in the BOModel
                       IModelMember member = CurrentClass.OwnMembers.Where(m => m.Name == Split[i]).FirstOrDefault();
                       //then we set the model class and property name to the values of the navigation property like category.name where category is the model class and name is the property
                       CurrentClass = this.Application.Model.BOModel.GetClass(member.Type);
                       if (CurrentClass == null)
                           continue;

                       ModelClass = CurrentClass;
                       PropertyName = Split[i + 1];
                     

                   }
                   Debug.WriteLine(string.Format("{0}:{1}", "ModelClass", ModelClass.Name));
                   Debug.WriteLine(string.Format("{0}:{1}", "PropertyName", PropertyName));
                   
                   
                  

               }
               else
               {
                   //else the model class will be the current class where the filter is executing, and the property will be the current property we are evaluating
                   ModelClass = this.View.Model.ModelClass;
                   PropertyName = CurrentProperty;
               }

               //we look for the property on the class model own member
               var Property = ModelClass.OwnMembers.Where(m => m.Name == PropertyName).FirstOrDefault();
               if (Property != null)
               {
                   //if the property is a string it means that we can set it to upper case
                   if (Property.Type == typeof(string))
                   {
                       searchValue = searchValue.ToString().ToUpper();
                       //we create an operator where we set the value of the property to upper before we compare it, also we change the comparison value to upper
                       CriteriaOperator Operand = CriteriaOperator.Parse("Contains(Upper(" + CurrentProperty + "), ?)", searchValue);
                       //we added to the list of operators that will concatenate with OR
                       Operator.Add(Operand);
                   }
                   else
                   {
                       //if the property is not a string we need to try to cast the value to the correct type so we do a catch try, if we manage to cast the value it will be added to the operators list
                       try
                       {

                           var ConvertedType = Convert.ChangeType(searchValue, Property.Type);
                           CriteriaOperator operand = new BinaryOperator(CurrentProperty, ConvertedType, BinaryOperatorType.Equal);
                           Operator.Add(operand);
                       }
                       catch (Exception)
                       {

                           //silent exception, this will happen if the casting was not successful so we won't add the operand on this case
                       }
                   }




               }
           }

           //we concatenate everything with an OR 
           var alloperators = CriteriaOperator.Or(Operator.ToArray());
           Debug.WriteLine(string.Format("{0}:{1}", "alloperators", alloperators));
           return alloperators;
       }


   }