A .NET MAUI Entry-derived control that shows app-provided suggestions while the user types.
The control is responsible for displaying suggestions, handling selection, and preserving familiar Entry behavior. Your app remains responsible for filtering data and updating ItemsSource.
- β¨ Features
- π Getting Started
- π Properties Reference
- π― Basic Usage
- π‘ Usage Examples
- ποΈ Platform Support Matrix
- π¨ Advanced Customization
- π Troubleshooting
- π Releasing
- π Support
- π€ Contributing
- π License
- π Acknowledgments
- π Real-time suggestions driven by your app's filtering logic
- π¨ Custom item templates for rich suggestion rendering
- π Binding-first and event-based APIs
- π± Cross-platform support for Android, iOS, Windows, and MacCatalyst
- π― Entry compatibility with familiar MAUI
Entryproperties and events - βοΈ Selection and dropdown control through bindable properties
Add the NuGet package to your project:
dotnet add package zoft.MauiExtensions.Controls.AutoCompleteEntryπ¦ View on NuGet
Register the control in MauiProgram.cs:
using CommunityToolkit.Maui;
using zoft.MauiExtensions.Controls;
namespace AutoCompleteEntry.Sample
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.UseZoftAutoCompleteEntry() // π Add this line
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
}Note
UseZoftAutoCompleteEntry()is the only registration required for this control. The sample app also callsUseMauiCommunityToolkit()because the sample uses CommunityToolkit features in other places. If your app already uses CommunityToolkit, keep that call; otherwise it is not required just to useAutoCompleteEntry.
Add the control namespace to the XAML file where you want to use the control:
xmlns:zoft="http://zoft.MauiExtensions/Controls"<zoft:AutoCompleteEntry
Placeholder="Search for a country"
ItemsSource="{Binding FilteredList}"
DisplayMemberPath="Country"
TextMemberPath="Country"
SelectedSuggestion="{Binding SelectedItem}"
TextChangedCommand="{Binding TextChangedCommand}" />public sealed class CountryItem
{
public string Group { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}
public sealed partial class SampleViewModel : ObservableObject
{
private readonly List<CountryItem> _allCountries =
[
new() { Group = "Group A", Country = "Ecuador" },
new() { Group = "Group A", Country = "Netherlands" },
new() { Group = "Group B", Country = "England" }
];
[ObservableProperty]
public partial ObservableCollection<CountryItem> FilteredList { get; set; } = [];
[ObservableProperty]
public partial CountryItem? SelectedItem { get; set; }
[RelayCommand]
private void TextChanged(string? text)
{
var filter = text ?? string.Empty;
FilteredList = new ObservableCollection<CountryItem>(
_allCountries.Where(item =>
item.Country.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
item.Group.Contains(filter, StringComparison.OrdinalIgnoreCase)));
}
}This is the key pattern for the control:
- The user types.
TextChangedCommandorTextChangedfires.- Your app filters its data.
- Your app assigns the filtered results to
ItemsSource.
For complete working examples, see the sample app in sample\AutoCompleteEntry.Sample.
| Property | Type | Default | Description |
|---|---|---|---|
ItemsSource |
IList |
null |
Collection of suggestion items to display |
SelectedSuggestion |
object |
null |
Currently selected suggestion item (two-way binding) |
DisplayMemberPath |
string |
"" |
Property path for displaying items in the suggestion list |
TextMemberPath |
string |
"" |
Property path for the text value when an item is selected |
ItemTemplate |
DataTemplate |
null |
Custom template for rendering suggestion items |
IsSuggestionListOpen |
bool |
false |
Controls whether the suggestion dropdown is open |
UpdateTextOnSelect |
bool |
true |
Whether selecting an item updates the text field |
ShowBottomBorder |
bool |
true |
Controls the visibility of the bottom border |
TextChangedCommand |
ICommand |
null |
Command executed when the user types (receives the current text as parameter) |
AutoCompleteEntry inherits from Entry, so all standard Entry properties are available:
| Property | Description |
|---|---|
Text |
The current text value |
Placeholder |
Placeholder text when empty |
PlaceholderColor |
Color of the placeholder text |
TextColor |
Color of the input text |
FontSize, FontFamily, FontAttributes |
Text formatting |
IsReadOnly |
Whether the text can be edited |
MaxLength |
Maximum character length |
CursorPosition |
Current cursor position |
ClearButtonVisibility |
When to show the clear button |
HorizontalTextAlignment, VerticalTextAlignment |
Text alignment |
CharacterSpacing |
Spacing between characters |
IsTextPredictionEnabled |
Enable/disable text prediction |
ReturnType |
Keyboard return key type |
| Event | EventArgs | Description |
|---|---|---|
TextChanged |
AutoCompleteEntryTextChangedEventArgs |
Fired when text changes and includes the reason |
SuggestionChosen |
AutoCompleteEntrySuggestionChosenEventArgs |
Fired when a suggestion is selected |
CursorPositionChanged |
AutoCompleteEntryCursorPositionChangedEventArgs |
Fired when cursor position changes |
Plus all inherited Entry events: Completed, Focused, Unfocused
AutoCompleteEntryTextChangedEventArgs.Reason helps you distinguish why TextChanged fired:
UserInput: the user typed in the controlProgrammaticChange: your code changedTextSuggestionChosen: the user picked an item from the suggestions list
TextChangedCommand only runs for UserInput, which makes it the recommended hook for filtering.
AutoCompleteEntry does not filter your data source internally. Instead, it acts as a UI shell around your own filtering logic.
The most common setup is:
- Bind
ItemsSourceto a filtered collection. - Use
DisplayMemberPathto control how items appear in the suggestion list. - Use
TextMemberPathto control what text is written back into the entry when an item is selected. - Use either:
- binding-based filtering with
TextChangedCommand(recommended) - event-based filtering with
TextChanged
- binding-based filtering with
<zoft:AutoCompleteEntry
Placeholder="Search for a country or group"
ItemsSource="{Binding FilteredList}"
TextMemberPath="Country"
DisplayMemberPath="Country"
TextChangedCommand="{Binding TextChangedCommand}"
SelectedSuggestion="{Binding SelectedItem}"
HeightRequest="50" />ViewModel implementation:
public partial class SampleViewModel : ObservableObject
{
private readonly List<CountryItem> _allCountries = new()
{
new CountryItem { Group = "Group A", Country = "Ecuador" },
new CountryItem { Group = "Group B", Country = "Netherlands" },
// ... more items
};
[ObservableProperty]
private ObservableCollection<CountryItem> _filteredList = new();
[ObservableProperty]
private CountryItem? _selectedItem;
public SampleViewModel()
{
FilteredList = new ObservableCollection<CountryItem>(_allCountries);
}
[RelayCommand]
private void TextChanged(string? text)
{
var filter = text ?? string.Empty;
var filtered = _allCountries.Where(item =>
item.Country.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
item.Group.Contains(filter, StringComparison.OrdinalIgnoreCase));
FilteredList = new ObservableCollection<CountryItem>(filtered);
}
}
public sealed class CountryItem
{
public string Group { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}<zoft:AutoCompleteEntry
Placeholder="Search countries with custom display"
ItemsSource="{Binding FilteredList}"
TextMemberPath="Country"
DisplayMemberPath="Country"
TextChangedCommand="{Binding TextChangedCommand}"
SelectedSuggestion="{Binding SelectedItem}"
ShowBottomBorder="{Binding ShowBottomBorder}"
HeightRequest="50">
<zoft:AutoCompleteEntry.ItemTemplate>
<DataTemplate x:DataType="vm:CountryItem">
<Grid ColumnDefinitions="Auto,*"
Padding="12,8"
HeightRequest="44">
<Border Grid.Column="0"
BackgroundColor="Red"
WidthRequest="4"
HeightRequest="28"
StrokeShape="RoundRectangle 2" />
<VerticalStackLayout Grid.Column="1"
Margin="12,0">
<Label Text="{Binding Country}"
FontSize="16"
FontAttributes="Bold"
TextColor="Black" />
<Label Text="{Binding Group}"
FontSize="12"
TextColor="Gray" />
</VerticalStackLayout>
</Grid>
</DataTemplate>
</zoft:AutoCompleteEntry.ItemTemplate>
</zoft:AutoCompleteEntry><zoft:AutoCompleteEntry
Placeholder="Search for a country or group"
ItemsSource="{Binding FilteredList}"
TextMemberPath="Country"
DisplayMemberPath="Country"
TextChanged="AutoCompleteEntry_TextChanged"
SuggestionChosen="AutoCompleteEntry_SuggestionChosen"
CursorPositionChanged="AutoCompleteEntry_CursorPositionChanged"
ClearButtonVisibility="WhileEditing"
HeightRequest="50" />Code-behind implementation:
private void AutoCompleteEntry_TextChanged(object sender, AutoCompleteEntryTextChangedEventArgs e)
{
// Only filter when the user is actually typing
if (e.Reason == AutoCompleteEntryTextChangeReason.UserInput)
{
var autoComplete = sender as AutoCompleteEntry;
ViewModel.FilterList(autoComplete.Text);
}
}
private void AutoCompleteEntry_SuggestionChosen(object sender, AutoCompleteEntrySuggestionChosenEventArgs e)
{
// Handle the selected suggestion
if (e.SelectedItem is CountryItem selectedCountry)
{
ViewModel.SelectedItem = selectedCountry;
// Perform additional actions like navigation or validation
}
}
private void AutoCompleteEntry_CursorPositionChanged(object sender, AutoCompleteEntryCursorPositionChangedEventArgs e)
{
// Track cursor position for analytics or custom behavior
Console.WriteLine($"Cursor moved to position: {e.CursorPosition}");
}
private void AutoCompleteEntry_Completed(object sender, EventArgs e)
{
if (sender is AutoCompleteEntry autoCompleteEntry)
{
// `GetExactMatch` is an app-provided helper, not part of AutoCompleteEntry.
// For example, your ViewModel could implement it by returning the first item
// whose display text exactly matches the current entry text.
ViewModel.SelectedItem = ViewModel.GetExactMatch(autoCompleteEntry.Text);
}
}// Programmatically open/close the suggestion list
autoCompleteEntry.IsSuggestionListOpen = true;
// Control text updates on selection
autoCompleteEntry.UpdateTextOnSelect = false; // Keep original text when selecting
// Customize appearance
autoCompleteEntry.ShowBottomBorder = false; // Remove bottom border
autoCompleteEntry.ClearButtonVisibility = ClearButtonVisibility.WhileEditing;
// Handle selection programmatically
autoCompleteEntry.SelectedSuggestion = mySelectedItem;| Feature | Windows | Android | iOS | MacCatalyst | Notes |
|---|---|---|---|---|---|
| Core Functionality | |||||
| Text Input & Filtering | β | β | β | β | Full support |
| ItemsSource Binding | β | β | β | β | Full support |
| Selection Events | β | β | β | β | Full support |
| Appearance & Styling | |||||
| ItemTemplate | β | β | β | β | Windows: Planned for future release |
| ShowBottomBorder | β | β | β | β | Windows: Planned for future release |
| Text Styling | β | β | β | β | Fonts, colors, alignment |
| Behavior | |||||
| UpdateTextOnSelect | β | β | β | β | Full support |
| IsSuggestionListOpen | β | β | β | β | Full support |
| CursorPosition | β | β | β | β | Full support |
| Entry Features | |||||
| ClearButtonVisibility | β | β | β | β | Full support |
| Placeholder | β | β | β | β | Full support |
| IsReadOnly | β | β | β | β | Full support |
| MaxLength | β | β | β | β | Full support |
- β Fully Supported - Feature works as expected
- β Not Implemented - Feature exists in API but not yet implemented on this platform
β οΈ Limited Support - Feature works with some limitations
While the AutoCompleteEntry works great on Windows, some advanced styling features are still in development:
- ItemTemplate: Currently displays items using their string representation. Custom templates are planned for a future release.
- ShowBottomBorder: This styling option doesn't affect the Windows presentation currently.
All core functionality including filtering, selection, and data binding works perfectly on Windows.
Use ItemTemplate when you want richer suggestion rows than plain text:
<zoft:AutoCompleteEntry.ItemTemplate>
<DataTemplate x:DataType="models:Product">
<Grid ColumnDefinitions="60,*,Auto"
RowDefinitions="Auto,Auto"
Padding="16,12">
<!-- Product Image -->
<Image Grid.RowSpan="2"
Source="{Binding ImageUrl}"
WidthRequest="50"
HeightRequest="50"
Aspect="AspectFill" />
<!-- Product Info -->
<Label Grid.Column="1"
Text="{Binding Name}"
FontSize="16"
FontAttributes="Bold" />
<Label Grid.Column="1" Grid.Row="1"
Text="{Binding Category}"
FontSize="12"
TextColor="Gray" />
<!-- Price -->
<Label Grid.Column="2" Grid.RowSpan="2"
Text="{Binding Price, StringFormat='${0:F2}'}"
FontSize="14"
FontAttributes="Bold"
VerticalOptions="Center"
HorizontalOptions="End" />
</Grid>
</DataTemplate>
</zoft:AutoCompleteEntry.ItemTemplate><Style x:Key="CustomAutoCompleteStyle" TargetType="zoft:AutoCompleteEntry">
<Setter Property="BackgroundColor" Value="{DynamicResource SurfaceColor}" />
<Setter Property="TextColor" Value="{DynamicResource OnSurfaceColor}" />
<Setter Property="PlaceholderColor" Value="{DynamicResource OnSurfaceVariantColor}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="HeightRequest" Value="56" />
<Setter Property="Margin" Value="16,8" />
<Setter Property="ShowBottomBorder" Value="True" />
</Style>- Efficient Filtering: Use proper indexing and async operations for large datasets
- Template Complexity: Keep ItemTemplates lightweight for smooth scrolling
- Data Virtualization: Consider implementing virtualization for very large lists
- Debouncing: Implement debouncing in your TextChangedCommand for better UX
// Example: Debounced filtering
private CancellationTokenSource _filterCancellation;
[RelayCommand]
private async Task TextChanged(string text)
{
_filterCancellation?.Cancel();
_filterCancellation = new CancellationTokenSource();
try
{
await Task.Delay(300, _filterCancellation.Token); // Debounce
await FilterItemsAsync(text);
}
catch (TaskCanceledException)
{
// Filtering was cancelled by newer input
}
}Issue: Suggestions not appearing
- β
Ensure
ItemsSourceis properly bound and contains data - β
Check
DisplayMemberPathmatches your data model properties - β Verify the control has sufficient height to display suggestions
Issue: Selection not working
- β
Confirm
SelectedSuggestionbinding is two-way - β
Check
TextMemberPathproperty is correctly set - β
Ensure the selected item exists in the current
ItemsSource
Issue: Custom templates not rendering (Windows)
β οΈ ItemTemplate is not yet implemented on Windows platform- β Use DisplayMemberPath for basic text display on Windows
Issue: Performance issues with large datasets
- β Implement efficient filtering logic
- β Use proper async/await patterns
- β Consider pagination or virtual scrolling
If you find this project helpful, please consider supporting its development:
Your support helps maintain and improve this project for the entire .NET MAUI community. Thank you! π
This repository uses manual GitHub Releases plus a manual GitHub Actions publish workflow.
- Update the package version in
src\Directory.build.props. - Move the pending notes from
CHANGELOG.mdinto a new version section. - Merge the release changes to
main. - Create a draft GitHub Release with tag
vX.Y.Z. - Click Generate release notes and refine the result into a short human-friendly summary.
- Run the Publish package workflow manually from the Actions tab.
- Provide:
ref: the branch or tag to buildrelease_tag: the existing GitHub release tag if you want validation and release asset uploadpublish_to_nuget: enable only when you want to push to NuGet.orgattach_to_release: enable only when you want the built.nupkg/.snupkgattached to the release
- Publish the GitHub Release after the notes and assets look correct.
- Pull requests to
mainonly run CI when code or project files change; they do not create releases or publish packages. - GitHub's generated release notes are grouped by
.github\release.yml. - Publishing to NuGet requires the
NUGET_API_KEYrepository secret. CHANGELOG.mdis the long-lived human changelog; GitHub Releases are the release-specific summary.
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE.md file for details.
- Inspired by platform-native autocomplete controls
- Built with β€οΈ for the .NET MAUI community