TL;DR: Learn how to integrate smart AI-based searching into the .NET MAUI ComboBox using custom filters and Semantic Kernel. The tutorial walks you through creating a custom filtering logic to handle spelling mistakes or incomplete inputs, ensuring more accurate search results. Additionally, it covers integrating Semantic Kernel for advanced AI-powered filtering, enhancing the user experience in data-driven applications.
Introduction
The .NET MAUI ComboBox control is a versatile selection component that lets users type a value or pick an option from a predefined list. It’s designed to display suggestions from large datasets based on user input efficiently.
In this blog, we’ll demonstrate how to implement smart AI-powered searching, allowing the ComboBox to return accurate results, even when there are no exact matches, by integrating the .NET MAUI ComboBox with Semantic Kernel.
Implementing the Smart AI Searching
To implement smart AI searching, we will use the custom filtering feature of the .NET MAUI ComboBox control. We will first walk you through implementing custom filtering and then integrate AI-driven search. For this demonstration, we use Semantic Kernel, an excellent tool for incorporating AI into .NET applications.
Custom filtering
The .NET MAUI ComboBox control allows you to apply custom filter logic to suggest items that meet specific criteria, leveraging the FilterBehavior property.
Step 1: Creating the Business Model for Food Search
First, create a simple business model for food search. Below is an example of how to define it:
// Model.cs
public class FoodModel
{
public string? Name { get; set; }
}
Next, create the ViewModel, which contains a collection of food items.
// ViewModel.cs
public class FoodViewModel : INotifyPropertyChanged
{
private ObservableCollection foods;
public ObservableCollection Foods
{
get { return foods; }
set { foods = value; OnPropertyChanged(nameof(Foods)); }
}
public FoodViewModel()
{
foods = new ObservableCollection
{
new FoodModel { Name = "Acai Bowl" },
new FoodModel { Name = "Aloo Gobi" },
new FoodModel { Name = "Arepas" },
new FoodModel { Name = "Baba Ganoush" },
// More food items...
};
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Step 2: Creating a Custom Filter Class
Now, create a class that implements the IComboBoxFilterBehavior interface. This class will define custom filtering logic.
public class ComboBoxCustomFilter: IComboBoxFilterBehavior
{
}
Step 3: Implementing the GetMatchingIndexes Method
Next, implement the GetMatchingIndexes method from the IComboBoxFilterBehavior interface to create a suggestion list. This list will include the filtered items based on your custom logic and will be displayed in the drop-down of the .NET MAUI ComboBox control. This method takes the following arguments:
- source : This argument refers to the ComboBox that owns the filter behavior. It gives access to properties like ItemsSource and other relevant data.
- filterInfo : This argument contains the text entered by the user in the ComboBox. You can use this input to generate a filtered suggestion list that will appear in the drop-down.
Below is an example of filtering and displaying a list of foods in the ComboBox. The list shows only the food items that start with the text entered by the user:
public class ComboBoxCustomFilter : IComboBoxFilterBehavior
{
public async Task<object?> GetMatchingIndexes(SfComboBox source, ComboBoxFilterInfo filterInfo)
{
IEnumerable? itemssource = source.ItemsSource as IEnumerable;
var filteredItems = from FoodModel item in itemssource
where item.Name.StartsWith(filterInfo.Text, StringComparison.CurrentCultureIgnoreCase)
select item;
return await Task.FromResult(filteredItems);
}
}</object?>
Step 4: Applying Custom Filtering to ComboBox
Finally, bind the custom filter to the ComboBox control using the FilterBehavior property.
<ContentPage.BindingContext>
<local:FoodViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout>
<syncfusion:SfTextInputLayout Hint="Choose Food Item"
ContainerType="Outlined"
WidthRequest="248"
ContainerBackground="Transparent">
<editors:SfComboBox x:Name="combobox"
DropDownPlacement="Bottom"
MaxDropDownHeight="200"
IsEditable="True"
TextSearchMode="StartsWith"
IsFilteringEnabled="True"
DisplayMemberPath="Name"
TextMemberPath="Name"
ItemsSource="{Binding Foods}">
<editors:SfComboBox.FilterBehavior>
<local:ComboBoxCustomFilter/>
</editors:SfComboBox.FilterBehavior>
</editors:SfComboBox>
</syncfusion:SfTextInputLayout>
</VerticalStackLayout>
The following image demonstrates the output of the above custom filtering sample.
Integrating Semantic Kernel with your .NET MAUI app
Semantic Kernel is an open-source software development kit (SDK) created by Microsoft, designed to help developers build intelligent applications powered by large language models (LLMs). This SDK simplifies the integration of LLMs like OpenAI, Azure OpenAI, Google, and Hugging Face Transformers into traditional programming environments.
In this example, we’ll focus on using Azure OpenAI, but you can choose any chat completion service. If you’re opting for Azure OpenAI, ensure you have access and set up a deployment via the Azure portal. For instructions, refer to the Create and Deploy Azure OpenAI Service Guide.
In this blog, we’re using the Semantic Kernel NuGet package, available in the NuGet Gallery. Before starting, install this package in your .NET MAUI application to proceed with the integration.
Setting up Semantic Kernel
Now, let’s begin creating the Semantic Kernel and Chat completion.
Step 1: Installing Semantic Kernel
Begin by installing the Semantic Kernel NuGet package in your .NET MAUI application.
Step 2: Install the Necessary NuGet Packages
Then, install the necessary NuGet packages and include the appropriate namespaces outlined in this getting started documentation.
Install-Package Microsoft.SemanticKernel
Step 3: Setting Up Semantic Kernel
In this setup, we will utilize Azure OpenAI with the GPT-35 model , specifically deployed under the name GPT35Turbo. To establish a successful connection to the service, replace the endpoint, deployment name, and key with your specific details.
Step 4: Chat Completion and Filtering
Now, you can utilize the Chat completion feature to define the chat history. The ChatHistory includes messages from the system, user, and assistant. In our application, we have used the following messages as input:
- System Message: Act as a filtering assistant.
User Message: Filter the list items based on the user input using characters starting with phonetic algorithms like Soundex or Damerau-Levenshtein Distance. \” +\r$\” The filter should ignore spelling mistakes and be case insensitive.
Assistant Message – This structured approach will help you set up and utilize Semantic Kernel effectively for your chat application.
Acai Bowl\nAloo Gobi\nArepas\nBaba Ganoush\nBagels\nBahn Xeo\nBaklava\nBanana Bread
Step 5: Utilizing Chat Completion
Next, utilize the chat completion feature to obtain completion results using the GetChatMessageContentAsync method. We have previously defined the ChatHistory format for the AI response, allowing us to receive the properly formatted response message.
Below is the complete code for the ComboBoxAzureAIService class. Note that I have commented out the Google Gemini codes. If you wish to use them, simply uncomment the GetGoogleGeminiAIKernel method.
public class ComboBoxAzureAIService
{
private const string endpoint = "https://YOUR_ACCOUNT.openai.azure.com/";
private const string deploymentName = "GPT35Turbo";
private const string key = "";
private IChatCompletionService? _chatCompletion;
private Kernel? _kernel;
private ChatHistory? _chatHistory;
internal bool IsCredentialValid = false;
private Uri? _uriResult;
public ComboBoxAzureAIService()
{
ValidateCredential();
}
private async void ValidateCredential()
{
#region Azure OpenAI
// Use below method for Azure Open AI
this.GetAzureOpenAIKernel();
#endregion
#region Google Gemini
// Use below method for Google Gemini
// this.GetGoogleGeminiAIKernel();
#endregion
bool isValidUri = Uri.TryCreate(endpoint, UriKind.Absolute, out _uriResult)
&& (_uriResult.Scheme == Uri.UriSchemeHttp || _uriResult.Scheme == Uri.UriSchemeHttps);
if (!isValidUri || !endpoint.Contains("http") || string.IsNullOrEmpty(key)
|| key.Contains("API key") || string.IsNullOrEmpty(deploymentName)
|| deploymentName.Contains("deployment name"))
{
ShowAlertAsync();
return;
}
try
{
if (_chatHistory != null && _chatCompletion != null)
{
// Test the semantic kernel with message
_chatHistory.AddSystemMessage("Hello, Test Check");
await _chatCompletion.GetChatMessageContentAsync(chatHistory: _chatHistory, kernel: _kernel);
}
}
catch (Exception)
{
// Handle any exceptions that indicate the credentials or endpoint are invalid.
ShowAlertAsync();
return;
}
IsCredentialValid = true;
}
#region Azure OpenAI
private void GetAzureOpenAIKernel()
{
// Create the chat history
_chatHistory = new ChatHistory();
try
{
var builder = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(deploymentName, endpoint, key);
// Get the kernel from build
_kernel = builder.Build();
// Get the chat completion from kernel
_chatCompletion = _kernel.GetRequiredService();
}
catch (Exception)
{
return;
}
}
#endregion
#region Google Gemini
private void GetGoogleGeminiAIKernel()
{
// Add package Microsoft.SemanticKernel.Connectors.Google
// _chatHistory = new ChatHistory();
// IKernelBuilder _kernelBuilder = Kernel.CreateBuilder();
// _kernelBuilder.AddGoogleAIGeminiChatCompletion(modelId: "NAME_OF_MODEL", apiKey: key);
// Kernel _kernel = _kernelBuilder.Build();
// _chatCompletion = _kernel.GetRequiredService();
}
#endregion
private async void ShowAlertAsync()
{
if (Application.Current?.MainPage != null && !IsCredentialValid)
{
await Application.Current.MainPage.DisplayAlert("Alert",
"The Azure API key or endpoint is missing or incorrect. Please verify your credentials. " +
"You can also continue with the offline data.", "OK");
}
}
public async Task GetCompletion(string prompt, CancellationToken cancellationToken)
{
if (_chatHistory != null && _chatCompletion != null)
{
if (_chatHistory.Count > 5)
{
_chatHistory.RemoveRange(0, 2); // Remove the message history to avoid exceeding the token limit
}
_chatHistory.AddUserMessage(prompt);
try
{
cancellationToken.ThrowIfCancellationRequested();
var chatResponse = await _chatCompletion.GetChatMessageContentAsync(chatHistory: _chatHistory, kernel: _kernel);
cancellationToken.ThrowIfCancellationRequested();
_chatHistory.AddAssistantMessage(chatResponse.ToString());
return chatResponse.ToString();
}
catch (RequestFailedException ex)
{
Debug.WriteLine($"Request failed: {ex.Message}");
throw;
}
catch (Exception ex)
{
Debug.WriteLine($"An error occurred: {ex.Message}");
throw;
}
}
return "";
}
}
Connecting to Semantic Kernel
Our .NET MAUI application can connect to the Semantic Kernel chat completion service through a custom filtering class. This custom filtering class utilizes the GetMatchingIndexes method, which is triggered each time input text is entered into the .NET MAUI ComboBox control. By connecting to the Semantic Kernel chat completion service, we generate a prompt based on the input text and retrieve the response message, which is then converted into an output collection.
To establish this connection, we will modify the existing ComboBoxFilterBehavior to integrate with the Semantic Kernel chat completion service. Below is the implementation of the ComboBoxCustomFilter class.
public class ComboBoxCustomFilter : IComboBoxFilterBehavior
{
private readonly ComboBoxAzureAIService _azureAIService;
public ObservableCollection Items { get; set; }
public ObservableCollection FilteredItems { get; set; } = new ObservableCollection();
private CancellationTokenSource? _cancellationTokenSource;
public ComboBoxCustomFilter()
{
_azureAIService = new ComboBoxAzureAIService();
Items = new ObservableCollection();
_cancellationTokenSource = new CancellationTokenSource();
}
public async Task<object?> GetMatchingIndexes(SfComboBox source, ComboBoxFilterInfo filterInfo)
{
Items = (ObservableCollection)source.ItemsSource;
// If credential is not valid, the filtering data shows as empty
if (!_azureAIService.IsCredentialValid || string.IsNullOrEmpty(filterInfo.Text))
{
_cancellationTokenSource?.Cancel();
FilteredItems.Clear();
return await Task.FromResult(FilteredItems);
}
string listItems = string.Join(", ", Items!.Select(c => c.Name));
// Join the first five items with newline characters for demo output template for AI
string outputTemplate = string.Join("\n", Items.Take(5).Select(c => c.Name));
// Cancel the previous token if the user types continuously
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;
// Passing the User Input, ItemsSource, Reference output, and CancellationToken
var filteredItems = await FilterItemsUsingAzureAI(filterInfo.Text, listItems, outputTemplate, cancellationToken);
return await Task.FromResult(filteredItems);
}
public async Task<observablecollection> FilterItemsUsingAzureAI(string userInput, string itemsList, string outputTemplate, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(userInput))
{
var prompt = $"Filter the list items based on the user input using character starting with and phonetic algorithms like Soundex or Damerau-Levenshtein Distance. " +
$"The filter should ignore spelling mistakes and be case insensitive. " +
$"Return only the filtered items with each item on a new line without any additional content like explanations, hyphens, numberings, and minus signs. Ignore phrases like 'Here are the filtered items.' " +
$"Only return items that are present in the List Items. " +
$"Ensure that each filtered item is returned in its entirety without missing any part of its content. " +
$"Arrange the filtered items so that those starting with the user input's first letter appear at the top, followed by other matches. " +
$"The example data is for reference; do not provide it as output. Filter the items from the list properly. " +
$"Here is the User input: {userInput}, " +
$"List of Items: {itemsList}. " +
$"If no items are found, return 'Empty'. " +
$"Do not include 'Here are the filtered items:' in the output. Check this demo output template, and return output like this: {outputTemplate}.";
var completion = await _azureAIService.GetCompletion(prompt, cancellationToken);
var filteredItems = completion.Split('\n')
.Select(x => x.Trim())
.Where(x => !string.IsNullOrEmpty(x))
.ToList();
if (FilteredItems.Count > 0)
FilteredItems.Clear();
FilteredItems.AddRange(
Items.Where(i => filteredItems.Any(item => i.Name!.StartsWith(item)))
);
cancellationToken.ThrowIfCancellationRequested();
}
return FilteredItems;
}
}</observablecollection</object?>
The image below illustrates the results of an AI-based search using custom filters.
GitHub reference
For more details, refer to the Smart AI Search GitHub demo.
Conclusion
Thanks for reading! In this blog, we explored how to implement a smart AI search that delivers seamless results, even when there are no exact matches, using the .NET MAUI ComboBox control. Try the steps shared here and leave feedback in the comments section below!
This feature is available in the latest 2024 Volume 3 release. You can check out all the features in our Release Notes and What’s New pages.
You can download and check out our MAUI demo app from Google Play and the Microsoft Stores.
The existing customers can download the new version of Essential Studio on the License and Downloads page. If you are not a Syncfusion customer, try our 30-day free trial to check out our incredible features.
You can also contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!