DEV Community

Cover image for Enhancing User Experience: Implementing Typos Tolerance in .NET MAUI Autocomplete
Jollen Moyani for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

Enhancing User Experience: Implementing Typos Tolerance in .NET MAUI Autocomplete

TLDR: Learn to configure the Syncfusion .NET MAUI AutoComplete control to tolerate typo mistakes by implementing custom filtering logic using the Soundex algorithm.

Typos toleration in search is a feature that significantly enhances the user experience in search functionality. It allows for variations in spelling and provides suggested corrections so users can quickly find what they need without constantly editing their search terms.

It’s beneficial for complex or difficult-to-spell search terms, reducing user frustration and improving overall search accuracy.

In this blog, we’ll see how to incorporate the typos toleration feature in the Syncfusion .NET MAUI AutoComplete control.

Default behavior of .NET MAUI Autocomplete control

Syncfusion .NET MAUI Autocomplete( SfAutocomplete ) control is highly optimized to load and populate suggestions quickly from a huge volume of data depending on the user’s input characters. It allows users to select an item from the suggestion list. It displays the selected item in the input view with the text and clear button.

By default, it does not tolerate typos, meaning it won’t recognize or provide suggestions for mistyped input.

.NET MAUI Autocomplete control

.NET MAUI Autocomplete control

How to make Autocomplete tolerate typos?

We can use custom filtering options to create the .NET MAUI Autocomplete control that tolerates typos.

An algorithm is employed to compare the user-entered search term with a list of possible suggestions or results and identify the possible matches, even if there are slight variations or mistakes in the search term.

The Soundex algorithm is one such algorithm that is commonly used for this purpose.

What is the Soundex algorithm?

The Soundex algorithm is a phonetic algorithm that converts a word to a code based on its pronunciation, which can be used to compare words for similarity.

It is commonly used for matching names or words with variations in spelling due to regional or language differences.

How does the Soundex algorithm work?

The Soundex algorithm encodes words into a four-character code based on their sound. The first character is the word’s first letter, and subsequent characters represent certain consonant sounds in the word.

Vowels and certain consonant sounds are ignored. The resulting code allows for variations in spelling and pronunciation to be mapped to the same code, enabling easier search and comparison of similar-sounding words.

For example the words Smith, Smyth, and Smithe will all be mapped to the same Soundex code S530.

Implementing Soundex in Csharp

Here, we’ll create a ToleratingTyposHelper class that uses the Soundex algorithm and DL(Damerau-Levenshtein) Distance algorithm to determine whether two strings match, considering common typos such as misspellings, extra or missing letters, etc.

Let’s see the steps to do so:

Step 1: First, let’s define the ToleratingTyposHelper class with a list of Soundex terms and the following three methods for calculating string similarity:

  • IsMatching(): Determines whether two strings are a match by processing them using the Soundex algorithm and then calculating their DL Distance.
  • CalcualteDistance(): Calculates the DL Distance between two strings.
  • ProcessOnSoundexAlgorithmn(): Processes a string using the Soundex algorithm, which replaces each letter with a corresponding digit based on its phonetic properties.

Step 2: In the constructor, initialize the list of soundexTerms. Each term represents a group of phonetically similar letters:

  • Group 1: a, e, I, o, u, h, y, w
  • Group 2: b, f, p, v
  • Group 3: c, g, j, k, q, s, x, z
  • Group 4: d, t
  • Group 5: l
  • Group 6: m, n
  • Group 7: r

Step 3: Define the GetMinValue() method to return the minimum value from an array of integers.

Step 4: Now, define the GetDamerauLevenshteinDistance() method to calculate the DL Distance between two strings. This algorithm measures the difference between two strings by counting the minimum number of operations required to transform one string into another. The allowed operations are insertion, deletion, substitution, and transposition (swapping two adjacent characters).

Step 5: The CalcualteDistance() method uses the DL Distance algorithm to calculate the similarity between two strings.

Step 6: The ProcessOnSoundexAlgorithmn() method is used to process a string using the Soundex algorithm. This algorithm replaces each letter in the string with a corresponding digit based on its phonetic properties. For example, the letter c is replaced with 2 because it sounds similar to s and z.

Step 7: Finally, the IsMatching() method uses the Soundex Algorithm and the DL Distance algorithm to determine whether two strings match. It first processes both strings using the Soundex algorithm and then calculates their DL Distance. The strings are considered a match if the DL Distance is less than or equal to a certain threshold (not specified in the code).

Refer to the following code example.

public class ToleratingTyposHelper
{
        public ToleratingTyposHelper()
        {
            soundexTerms.Add("aeiouhyw");
            soundexTerms.Add("bfpv");
            soundexTerms.Add("cgikqsxz");
            soundexTerms.Add("dt");
            soundexTerms.Add("l");
            soundexTerms.Add("mn");
            soundexTerms.Add("r");
        }
        List<string> soundexTerms = new List<string>();
        /// <summary>
        /// Based on Soundex Algorithmn and DL Distance Algorithmn
        /// </summary>
        /// <returns>The matching.</returns>
        /// <param name="value1">Value1.</param>
        /// <param name="value2">Value2.</param>
        public int IsMatching(string value1, string value2)
        {
            var val1 = ProcessOnSoundexAlgorithmn(value1);
            var val2 = ProcessOnSoundexAlgorithmn(value2);
            return CalcualteDistance(val1, val2);
        }
        public int GetMinValue(int[] value)
        {
            int minValue = 0;
            foreach (var item in value)
            {
                if (item < minValue)
                    minValue = item;
            }
            return minValue;
        }
        public int GetDamerauLevenshteinDistance(string source, string target)
        {
            var bounds = new { Height = source.Length + 1, Width = target.Length + 1 };
            int[,] matrix = new int[bounds.Height, bounds.Width];
            for (int height = 0; height < bounds.Height; height++) { matrix[height, 0] = height; };
            for (int width = 0; width < bounds.Width; width++) { matrix[0, width] = width; };
            for (int height = 1; height < bounds.Height; height++)
            {
                for (int width = 1; width < bounds.Width; width++)
                {
                    int cost = (source[height - 1] == target[width - 1]) ? 0 : 1;
                    int insertion = matrix[height, width - 1] + 1;
                    int deletion = matrix[height - 1, width] + 1;
                    int substitution = matrix[height - 1, width - 1] + cost;
                    int distance = Math.Min(insertion, Math.Min(deletion, substitution));
                    if (height > 1 && width > 1 && source[height - 1] == target[width - 2] && source[height - 2] == target[width - 1])
                    {
                        distance = Math.Min(distance, matrix[height - 2, width - 2] + cost);
                    }
                    matrix[height, width] = distance;
                }
            }
            return matrix[bounds.Height - 1, bounds.Width - 1];
        }
        /// <summary>
        /// DL Algorithmn Implementation
        /// </summary>
        /// <returns>The distance.</returns>
        /// <param name="value1">Value1.</param>
        /// <param name="value2">Value2.</param>
        public int CalcualteDistance(string value1, string value2)
        {
            int lengthValue1 = value1.Length;
            int lengthValue2 = value2.Length;
            var matrix = new int[lengthValue1 + 1, lengthValue2 + 1];
            for (int i = 0; i <= lengthValue1; i++)
                matrix[i, 0] = i;
            for (int j = 0; j <= lengthValue2; j++)
                matrix[0, j] = j;
            for (int i = 1; i <= lengthValue1; i++)
            {
                for (int j = 1; j <= lengthValue2; j++)
                {
                    int cost = value2[j - 1] == value1[i - 1] ? 0 : 1;
                    var vals = new int[] {
                         matrix[i - 1, j] + 1,
                         matrix[i, j - 1] + 1,
                         matrix[i - 1, j - 1] + cost
                    };
                    matrix[i, j] = GetMinValue(vals);
                    if (i > 1 && j > 1 && value1[i - 1] == value2[j - 2] && value1[i - 2] == value2[j - 1])
                        matrix[i, j] = Math.Min(matrix[i, j], matrix[i - 2, j - 2] + cost);
                }
            }
            return matrix[lengthValue1, lengthValue2];
        }
        /// <summary>
        /// Soundex Algorithmn Implementation
        /// </summary>
        /// <returns>The on soundex algorithmn.</returns>
        /// <param name="value1">Value1.</param>
        /// <param name="moreAccuracy">If set to <c>true</c> more accuracy.</param>
        public string ProcessOnSoundexAlgorithmn(string value1, bool moreAccuracy = true)
        {
            string stringValue = string.Empty;
            foreach (var item in value1.ToLower())
            {
                for (int i = 0; i < soundexTerms.Count; i++)
                {
                    if (soundexTerms[i].Contains(item.ToString()))
                    {
                        stringValue += i.ToString();
                        continue;
                    }
                }
            }
            if (stringValue.Length > 0)
            {
                if (moreAccuracy)
                {
                    stringValue = stringValue.Insert(0, value1[0].ToString());
                    stringValue = stringValue.Replace("0", "");
                }
            }
            return stringValue;
        }
 }
Enter fullscreen mode Exit fullscreen mode

Configuring .NET MAUI AutoComplete control with custom filtering

This section will teach us how to configure the .NET MAUI Autocomplete control with a custom filtering logic.

Step 1: First, create a new .NET MAUI app.

Step 2: Syncfusion .NET MAUI controls are available on Nuget.org. To add the .NET MAUI Autocomplete to your project, open the NuGet package manager in Visual Studio, search for Syncfusion.Maui.Inputs and install it.

Step 3: Now, register the handler for Syncfusion core in the MauiProgram.cs file.

Refer to the following code example.

using Microsoft.Extensions.Logging;
using Syncfusion.Maui.Core.Hosting;
namespace GoogleSearchDemo;
public static class MauiProgram
{
     public static MauiApp CreateMauiApp()
     {
    var builder = MauiApp.CreateBuilder();
    builder
       .ConfigureSyncfusionCore()
       .UseMauiApp<App>()
       .ConfigureFonts(fonts =>
       {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
       });
         #if DEBUG
         builder.Logging.AddDebug();
         #endif
         return builder. Build();
     }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: The .NET MAUI Autocomplete control is configured entirely in C# code or by XAML markup. Add the NuGet to the project as discussed in the above reference section. Now, add the namespace as shown in the following code.

xmlns:editors="clr-namespace:Syncfusion.Maui.Inputs;assembly=Syncfusion.Maui.Inputs"
Enter fullscreen mode Exit fullscreen mode

Step 4: Adding a Grid control to .NET MAUI AutoComplete

Here, we’ll add a Grid to the Autocomplete control, defining various properties.

  • Placeholder: This property displays a text hint inside the control before the user inputs any value.
  • MaxDropDownHeight: This property sets the maximum height of the drop-down list that appears when it gets opened.
  • TextSearchMode: This property sets the search mode for matching items in the Autocomplete control’s data source. It is set to Contains, meaning the control will show all items containing the typed text. The WidthRequest and HeightRequest properties are used to set the preferred width and height for the control, respectively.
<Grid Margin="0,20,0,0">
  <editors:SfAutocomplete HeightRequest="50"
                          Placeholder="Search something" MaxDropDownHeight="250"
                          TextSearchMode="Contains"
                          WidthRequest="300">
  </editors:SfAutocomplete>
</Grid>
Enter fullscreen mode Exit fullscreen mode

Step 5: Set the custom filtering class

Let’s apply our custom filter logic to the Autocomplete control using the FilterBehavior and SearchBehavior properties. This will suggest items based on our filter criteria. The default value of FilterBehavior and SearchBehavior is null.

Refer to the following code example.

<Grid Margin="0,20,0,0">
  <editors:SfAutocomplete HeightRequest="50"
                          Placeholder="Search something" MaxDropDownHeight="250"
                          TextSearchMode="Contains"
                          WidthRequest="300">
      <editors:SfAutocomplete.FilterBehavior>
          <local:CustomFiltering/>
      </editors:SfAutocomplete.FilterBehavior>
   </editors:SfAutocomplete>
</Grid>
Enter fullscreen mode Exit fullscreen mode

Finally, the UI part is finished. Let’s configure the backend to see how to implement the CustomFiltering class.

Integrating typos toleration in CustomFiltering class using Soundex algorithm

Let’s explore the steps to integrate the tolerating typo class in our custom filter logic.

Step 1: The CustomFiltering is a C# class that implements the IAutocompleteFilterBehavior interface. This interface defines the behavior of an autocomplete filter that can be used with a SfAutocomplete control. Then, create an object for the ToleratingTyposHelper class.

Refer to the following code example.

public class CustomFiltering : IAutocompleteFilterBehavior
{
   ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Here, in the CustomFiltering class’s constructor, we will initialize a list of items with vegetable names. These names will be used to filter and return the matching items when a user types in the SfAutocomplete control.

public class CustomFiltering : IAutocompleteFilterBehavior
{
    public CustomFiltering()
    {
        items.Add("Carrots");
        items.Add("Broccoli");
        items.Add("Cauliflower");
        items.Add("Spinach");
        items.Add("Tomatoes");
        items.Add("Bell peppers");
        items.Add("Onions");
        items.Add("Potatoes");
        items.Add("Cabbage");
        items.Add("Zucchini");
    }

    List<String> items = new List<String>();
    ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The GetMatchingItemsAsync method from IAutocompleteFilterBehavior is defined to return the filtered items that match the user’s input text. This method takes two parameters:

public class CustomFiltering : IAutocompleteFilterBehavior
{
    public CustomFiltering()
    {
       items.Add("Carrots");
       items.Add("Broccoli");
       items.Add("Cauliflower");
       items.Add("Spinach");
       items.Add("Tomatoes");
       items.Add("Bell peppers");
       items.Add("Onions");
       items.Add("Potatoes");
       items.Add("Cabbage");
       items.Add("Zucchini");
    }

    List<String> items = new List<String>();
    ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
    public Task<object> GetMatchingItemsAsync(SfAutocomplete source, AutocompleteFilterInfo filterInfo)
    {
        return PossibleItems(filterInfo.Text);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: The PossibleItems method is called from the GetMatchingItemsAsync method to filter the list of items based on the user’s input text. This method takes the user’s input text as a parameter and returns a filtered list.

public class CustomFiltering : IAutocompleteFilterBehavior
{
    public CustomFiltering()
    {
       items.Add("Carrots");
       items.Add("Broccoli");
       items.Add("Cauliflower");
       items.Add("Spinach");
       items.Add("Tomatoes");
       items.Add("Bell peppers");
       items.Add("Onions");
       items.Add("Potatoes");
       items.Add("Cabbage");
       items.Add("Zucchini");
    }

    List<String> items = new List<String>();
    ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
    public Task<object> GetMatchingItemsAsync(SfAutocomplete source, AutocompleteFilterInfo filterInfo)
    {
        return PossibleItems(filterInfo.Text);
    }
    private async Task<object> PossibleItems(string query)
    {
        if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
        {
           return new List<string>();
       }
       List<String> filteredItems = new List<string>();
      foreach (var item in items)
      {
           var result = AutoCompleteSearch(query, item);
           if(!string.IsNullOrWhiteSpace(result))
           {
              filteredItems.Add(result);
            }
       }
       return filteredItems;
   }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The AutoCompleteSearch method is defined to search for matching items based on the user’s input text. This method takes two parameters: value1 and value2, which represent the user’s input text and the current item in the list being searched, respectively.

public class CustomFiltering : IAutocompleteFilterBehavior
{
     public CustomFiltering()
     {
          items.Add("Carrots");
          items.Add("Broccoli");
          items.Add("Cauliflower");
          items.Add("Spinach");
          items.Add("Tomatoes");
          items.Add("Bell peppers");
          items.Add("Onions");
          items.Add("Potatoes");
          items.Add("Cabbage");
          items.Add("Zucchini");
     }

     List<String> items = new List<String>();
     ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
     public Task<object> GetMatchingItemsAsync(SfAutocomplete source, AutocompleteFilterInfo filterInfo)
     {
         return PossibleItems(filterInfo.Text);
     }
     private async Task<object> PossibleItems(string query)
     {
         if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
         {
            return new List<string>();
         }
         List<String> filteredItems = new List<string>();
         foreach (var item in items)
         {
              var result = AutoCompleteSearch(query, item);
                if(!string.IsNullOrWhiteSpace(result))
                {
                    filteredItems.Add(result);
                }
         }
         return filteredItems;
   }
   public new string AutoCompleteSearch(object value1, object value2)
   {
       var string1 = value1.ToString().ToLower();
       var string2 = value2.ToString().ToLower();
       if (string1.Length > 0 && string2.Length > 0)
          if (string1[0] != string2[0])
            return null;
       return String.Empty;
   }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: The AutoCompleteSearch method uses the Damerau-Levenshtein distance algorithm to calculate the distance between the user’s input text and the current item in the searched list.

This algorithm determines how similar two strings are by counting the minimum number of operations needed to transform one string into another.

The AutoCompleteSearch method also uses the Soundex algorithm to match items with similar pronunciations. The Soundex algorithm is a phonetic algorithm that encodes words based on their pronunciation, and items with the same Soundex code are considered to have similar pronunciations.

public class CustomFiltering : IAutocompleteFilterBehavior
{
    public CustomFiltering()
    {
        items.Add("Carrots");
        items.Add("Broccoli");
        items.Add("Cauliflower");
        items.Add("Spinach");
        items.Add("Tomatoes");
        items.Add("Bell peppers");
        items.Add("Onions");
        items.Add("Potatoes");
        items.Add("Cabbage");
        items.Add("Zucchini");
    }

    List<String> items = new List<String>();
    ToleratingTyposHelper toleratingTyposHelper = new ToleratingTyposHelper();
    public Task<object> GetMatchingItemsAsync(SfAutocomplete source, AutocompleteFilterInfo filterInfo)
    {
       return PossibleItems(filterInfo.Text);
    }
    private async Task<object> PossibleItems(string query)
    {
       if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
       {
           return new List<string>();
       }
       List<String> filteredItems = new List<string>();
       foreach (var item in items)
       {
          var result = AutoCompleteSearch(query, item);
           if(!string.IsNullOrWhiteSpace(result))
           {
               filteredItems.Add(result);
           }
        }
        return filteredItems;
    }
    public new string AutoCompleteSearch(object value1, object value2)
    {
        var string1 = value1.ToString().ToLower();
        var string2 = value2.ToString().ToLower();
        if (string1.Length > 0 && string2.Length > 0)
            if (string1[0] != string2[0])
               return null;
        var originalString1 = string.Empty;
        var originalString2 = string.Empty;
        if (string1.Length < string2.Length)
        {
            originalString2 = string2.Remove(string1.Length);
            originalString1 = string1;
        }
        if (string2.Length < string1.Length)
        {
           return null;
        }
        if (string2.Length == string1.Length)
        {
            originalString1 = string1;
            originalString2 = string2;
        }
        bool IsMatchSoundex = this.toleratingTyposHelper.ProcessOnSoundexAlgorithmn(originalString1) == this.toleratingTyposHelper.ProcessOnSoundexAlgorithmn(originalString2);
        int Distance = this.toleratingTyposHelper.GetDamerauLevenshteinDistance(originalString1, originalString2);
        if (IsMatchSoundex || Distance <= 4)
        {
            return value2.ToString();
         }
         return String.Empty;
    }
}
Enter fullscreen mode Exit fullscreen mode

Overall, the CustomFiltering class implements an autocomplete filter that uses the Damerau-Levenshtein distance algorithm and the Soundex algorithm to match items based on their similarity to the user’s input text. This implementation allows for more flexible and accurate filtering of items in the .NET MAUI Autocomplete control.

After executing the above code examples, we’ll get the output as shown in the following image.

Integrating typos toleration feature in Syncfusion .NET MAUI Autocomplete control

Integrating typos toleration feature in Syncfusion .NET MAUI Autocomplete control

GitHub reference

For more details, refer to integrating typos toleration in the .NET MAUI Autocomplete control demo on GitHub.

Conclusion

Thanks for reading! This blog taught us how to add a typos tolerance feature for search queries in the Syncfusion .NET MAUI Autocomplete control. We hope the steps outlined in this post have been helpful and informative.

If you want to try out the .NET MAUI Autocomplete control, you can download the Essential Studio for .NET MAUI and evaluate it yourself.

As always, if you need any assistance or have any further questions, please don’t hesitate to reach out to us through our support forum, support portal, or feedback portal. We are here to help and support you in any way we can.

Related blogs

Top comments (0)