Using a Combo Box user control with autocomplete suggestions in WPF MVVM

Create a new WPF project

Install the Microsoft.Xaml.Behaviors.Wpf nuget package

Download link:

https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf

Copy the download link at the download page, open package manager console and enter the command:

NuGet\Install-Package Microsoft.Xaml.Behaviors.Wpf -Version 1.1.39

Create the Command and event handling code

BaseViewModel.cs

using System;
using System.ComponentModel;
using System.Diagnostics;

namespace Autocomplete
{
    public abstract class BaseViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        protected void OnPropertyChanged(string propertyName)
        {
            VerifyPropertyName(propertyName);
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void OnPropertyChanged(int propertyValue)
        {
            VerifyPropertyName(propertyValue);
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyValue.ToString()));
            }
        }

        [Conditional("DEBUG")]
        private void VerifyPropertyName(string propertyName)
        {
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
                throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyName);
        }

        [Conditional("DEBUG")]
        private void VerifyPropertyName(int propertyValue)
        {
            if (TypeDescriptor.GetProperties(this)[propertyValue] == null)
                throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyValue.ToString());
        }
    }
}

EventArgs.cs

using System;

namespace Autocomplete
{
    public class EventArgs<T> : EventArgs
    {
        public EventArgs(T value)
        {
            Value = value;
        }

        public T Value { get; private set; }
    }
}

EventRaiser.cs

using System;

namespace Autocomplete
{
    public static class EventRaiser
    {
        public static void Raise(this EventHandler handler, object sender)
        {
            handler?.Invoke(sender, EventArgs.Empty);
        }

        public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value)
        {
            handler?.Invoke(sender, new EventArgs<T>(value));
        }

        public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs
        {
            handler?.Invoke(sender, value);
        }

        public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value)
        {
            handler?.Invoke(sender, value);
        }
    }
}

RelayCommand.cs

using System;
using System.Windows.Input;

namespace Autocomplete
{
    public class RelayCommand<T> : ICommand
    {
        private readonly Predicate<T> _canExecute;
        private readonly Action<T> _execute;

        public RelayCommand(Action<T> execute)
           : this(execute, null)
        {
            _execute = execute;
        }

        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute((T)parameter);
        }

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }

    public class RelayCommand : ICommand
    {
        private readonly Predicate<object> _canExecute;
        private readonly Action<object> _execute;

        public RelayCommand(Action<object> execute)
           : this(execute, null)
        {
            _execute = execute;
        }

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their
        // associated views should be enabled whenever a command is invoked 
        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
                CanExecuteChangedInternal += value;
            }
            remove
            {
                CommandManager.RequerySuggested -= value;
                CanExecuteChangedInternal -= value;
            }
        }

        private event EventHandler CanExecuteChangedInternal;

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChangedInternal.Raise(this);
        }
    }
}

Create the combo box view and viewmodel

SuggestionUserControl.cs

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;

namespace Autocomplete
{
    public class SuggestionModel
    {
        public string Name { get; set; }
    }

    public class SuggestionViewModel : BaseViewModel
    {
        private ICommand _command;
        private string _text;
        private bool _isDropdownOpen = false;
        private System.Windows.Visibility _visibility = System.Windows.Visibility.Hidden;
        private ObservableCollection<SuggestionModel> _suggestions = new ObservableCollection<SuggestionModel>();
        private SuggestionModel _suggestionModel;
        private static readonly string[] SuggestionValues = {
            "England",
            "Spain",
            "UK",
            "UEA",
            "USA",
            "France",
            "Germany",
            "Netherlands",
            "Estonia"
        };

        public ICommand TextChangedCommand => _command ?? (_command = new RelayCommand(
                   x =>
                   { 
                       TextChanged(x as string);
                   }));

        public ICommand DropdownSelectionChanged => _command ?? (_command = new RelayCommand(
                  x =>
                  {
                      DropdownChanged();
                  }));

        public string LabelText
        {
            get { return _text; }
            set
            {
                _text = value;
                OnPropertyChanged(nameof(LabelText));

            }
        }

        public SuggestionModel SelectedSuggestionModel
        {
            get { return _suggestionModel; }
            set
            {
                _suggestionModel = value;
                DropdownChanged();
                if (_suggestionModel != null && !string.IsNullOrEmpty(_suggestionModel.Name))
                {
                    LabelText = _suggestionModel.Name;
                }
                
                OnPropertyChanged(nameof(SelectedSuggestionModel)); 
            }
        }

        public bool IsDropdownOpen
        {
            get { return _isDropdownOpen; }
            set
            {
                _isDropdownOpen = value;
                OnPropertyChanged(nameof(IsDropdownOpen));

            }
        }

        public System.Windows.Visibility Visibility 
        {
            get { return _visibility; }
            set
            {
                _visibility = value;
                OnPropertyChanged(nameof(Visibility));
            }
        }

        public ObservableCollection<SuggestionModel> Suggestions
        {
            get { return _suggestions; }
            set
            {
                _suggestions = value;
                OnPropertyChanged(nameof(Suggestions));
            }
        }

        private void DropdownChanged()
        {
            IsDropdownOpen = false;
            Visibility = System.Windows.Visibility.Hidden;
        }

        private void TextChanged(string text)
        {
            if (text is null)
                return;

            var suggestions = SuggestionValues.ToList().Where(p => p.ToLower().Contains(text.ToLower())).ToList();

            Suggestions = new ObservableCollection<SuggestionModel>();

            foreach(var suggestion in suggestions)
            {
                SuggestionModel suggestionStr = new SuggestionModel
                {
                    Name = suggestion
                };

                Suggestions.Add(suggestionStr);
            }

            if (!string.IsNullOrEmpty(text) && Suggestions.Count > 0)
            {
                IsDropdownOpen = true;
                Visibility = System.Windows.Visibility.Visible;
            }
            else
            {
                IsDropdownOpen = false;
                Suggestions = new ObservableCollection<SuggestionModel>();
                Visibility = System.Windows.Visibility.Hidden;
            }
            
        }
    }
}
[code]

<strong>Suggestion.xml</strong>

[code language="xml"]
<UserControl x:Class="Autocomplete.Suggestion"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Autocomplete"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             mc:Ignorable="d" >

    <UserControl.DataContext>
        <local:SuggestionViewModel />
    </UserControl.DataContext>

    <StackPanel Orientation="Vertical" >
        <TextBox Name="textBox"
                 VerticalAlignment="Center" 
                 VerticalContentAlignment="Center"
                 HorizontalAlignment="Center" 
                 Width="200" 
                 Height="30"
                 Margin="2"
                 Text="{Binding LabelText}" >

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="TextChanged">
                    <i:InvokeCommandAction CommandParameter="{Binding Text, ElementName=textBox}"
                                           Command="{Binding TextChangedCommand}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBox>
  
        <ComboBox  
            Width="200"  
            Height="30"
            VerticalContentAlignment="Bottom"
            IsDropDownOpen="{Binding IsDropdownOpen}"
            Visibility="{Binding Visibility}"
            SelectedItem="{Binding SelectedSuggestionModel}"
            ItemsSource="{Binding Path=Suggestions}" 
            DisplayMemberPath="Name">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="DropDownOpened">
                    <i:InvokeCommandAction Command="{Binding DropdownSelectionChanged}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </ComboBox>
    </StackPanel>
</UserControl>

Use the combo box user control in your code

Once the control is implemented we can then apply it where we link, in this example we place in the main window.

MainWindow.xaml

<Window x:Class="Autocomplete.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Autocomplete"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:Suggestion/>
    </Grid>
</Window>

On running the application we have the combo box displayed, but not yet populated:

If we type in “e” for example, see that the combo box is populated with the list of suggested entries that all contain this letter:

And on selecting one of the entries, the combo box becomes hidden and the text box is populated with the selected text:

Purchase the Visual Studio 2019 project from here:

Koji link