From d8bca15610cdab35f828c8a1b18785ab4aa31ccd Mon Sep 17 00:00:00 2001 From: Corvin Date: Tue, 7 Oct 2025 20:00:02 +0200 Subject: [PATCH 1/4] Refactor FieldsViewModel and enhance AutoSuggestBox Refactored `FieldsViewModel` to use `CommunityToolkit.Mvvm` attributes, reducing boilerplate code and improving maintainability. Added `AutoSuggestBox3` with dynamic filtering, interactive item templates, and a command to remove suggestions. Enhanced `AutoSuggestBox` behavior to support interactive elements like buttons without closing the popup. Updated `Fields.xaml` to include new `AutoSuggestBox` variations and bindings. Introduced a new `AutoSuggestTextBoxWithInteractiveTemplate` sample to demonstrate interactive item templates. Added tests to validate interactive `AutoSuggestBox` behavior. Performed general cleanup and modernization, including concise syntax and collection initializers. --- src/MainDemo.Wpf/Domain/FieldsViewModel.cs | 154 ++++++++---------- src/MainDemo.Wpf/Fields.xaml | 120 +++++++++----- .../AutoSuggestBox.cs | 17 +- .../MaterialDesignTheme.AutoSuggestBox.xaml | 2 + ...SuggestTextBoxWithInteractiveTemplate.xaml | 42 +++++ ...gestTextBoxWithInteractiveTemplate.xaml.cs | 88 ++++++++++ .../AutoSuggestTextBoxTests.cs | 39 +++++ 7 files changed, 341 insertions(+), 121 deletions(-) create mode 100644 tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml create mode 100644 tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs diff --git a/src/MainDemo.Wpf/Domain/FieldsViewModel.cs b/src/MainDemo.Wpf/Domain/FieldsViewModel.cs index 805ad8f07f..e933d2b1f1 100644 --- a/src/MainDemo.Wpf/Domain/FieldsViewModel.cs +++ b/src/MainDemo.Wpf/Domain/FieldsViewModel.cs @@ -1,60 +1,35 @@ using System.Collections.ObjectModel; using System.Windows.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MaterialDesignDemo.Shared.Domain; namespace MaterialDesignDemo.Domain; -public class FieldsViewModel : ViewModelBase +public partial class FieldsViewModel : ObservableObject { - private string? _name; - private string? _name2; - private string? _password1 = string.Empty; - private string? _password2 = "pre-filled"; private string? _password1Validated = "pre-filled"; private string? _password2Validated = "pre-filled"; - private string? _text1; - private string? _text2; - private ObservableCollection? _autoSuggestBox1Suggestions; - private string? _autoSuggestBox1Text; private readonly List? _originalAutoSuggestBox1Suggestions; - private ObservableCollection>? _autoSuggestBox2Suggestions; - private string? _autoSuggestBox2Text; private readonly List>? _originalAutoSuggestBox2Suggestions; + private readonly List _originalAutoSuggestBox3Suggestions; - public string? Name - { - get => _name; - set => SetProperty(ref _name, value); - } + [ObservableProperty] + private string? _name; - public string? Name2 - { - get => _name2; - set => SetProperty(ref _name2, value); - } + [ObservableProperty] + private string? _name2; - public string? Text1 - { - get => _text1; - set => SetProperty(ref _text1, value); - } + [ObservableProperty] + private string? _text1; - public string? Text2 - { - get => _text2; - set => SetProperty(ref _text2, value); - } + [ObservableProperty] + private string? _text2; - public string? Password1 - { - get => _password1; - set => SetProperty(ref _password1, value); - } + [ObservableProperty] + private string? _password1 = string.Empty; - public string? Password2 - { - get => _password2; - set => SetProperty(ref _password2, value); - } + [ObservableProperty] + private string? _password2 = "pre-filled"; public string? Password1Validated { @@ -80,43 +55,64 @@ public string? Password2Validated public FieldsTestObject TestObject => new() { Name = "Mr. Test" }; - public ObservableCollection? AutoSuggestBox1Suggestions + [ObservableProperty] + private ObservableCollection? _autoSuggestBox1Suggestions; + + [ObservableProperty] + private ObservableCollection>? _autoSuggestBox2Suggestions; + + [ObservableProperty] + private List _autoSuggestBox3Suggestions; + + + [ObservableProperty] + private string? _autoSuggestBox1Text; + + partial void OnAutoSuggestBox1TextChanged(string? value) { - get => _autoSuggestBox1Suggestions; - set => SetProperty(ref _autoSuggestBox1Suggestions, value); + if (_originalAutoSuggestBox1Suggestions != null && value != null) + { + var searchResult = _originalAutoSuggestBox1Suggestions.Where(x => IsMatch(x, value)); + AutoSuggestBox1Suggestions = new(searchResult); + } } - public ObservableCollection>? AutoSuggestBox2Suggestions + [ObservableProperty] + private string? _autoSuggestBox2Text; + + partial void OnAutoSuggestBox2TextChanged(string? value) { - get => _autoSuggestBox2Suggestions; - set => SetProperty(ref _autoSuggestBox2Suggestions, value); + if (_originalAutoSuggestBox2Suggestions != null && value != null) + { + var searchResult = _originalAutoSuggestBox2Suggestions.Where(x => IsMatch(x.Key, value)); + AutoSuggestBox2Suggestions = new(searchResult); + } } - public string? AutoSuggestBox1Text + [ObservableProperty] + private string? _autoSuggestBox3Text; + + partial void OnAutoSuggestBox3TextChanged(string? value) { - get => _autoSuggestBox1Text; - set + if (value is not null) { - if (SetProperty(ref _autoSuggestBox1Text, value) && - _originalAutoSuggestBox1Suggestions != null && value != null) - { - var searchResult = _originalAutoSuggestBox1Suggestions.Where(x => IsMatch(x, value)); - AutoSuggestBox1Suggestions = new ObservableCollection(searchResult); - } + var searchResult = _originalAutoSuggestBox3Suggestions.Where(x => IsMatch(x, value)); + AutoSuggestBox3Suggestions = new(searchResult); } } - public string? AutoSuggestBox2Text + [RelayCommand] + private void RemoveAutoSuggestBox3Suggestion(string suggestion) { - get => _autoSuggestBox2Text; - set + _originalAutoSuggestBox3Suggestions.Remove(suggestion); + if (string.IsNullOrEmpty(AutoSuggestBox3Text)) + { + AutoSuggestBox3Suggestions = new(_originalAutoSuggestBox3Suggestions); + } + else { - if (SetProperty(ref _autoSuggestBox2Text, value) && - _originalAutoSuggestBox2Suggestions != null && value != null) - { - var searchResult = _originalAutoSuggestBox2Suggestions.Where(x => IsMatch(x.Key, value)); - AutoSuggestBox2Suggestions = new ObservableCollection>(searchResult); - } + var searchResult = _originalAutoSuggestBox3Suggestions.Where(x => IsMatch(x, AutoSuggestBox3Text!)); + AutoSuggestBox3Suggestions = new(searchResult); } } @@ -128,12 +124,16 @@ public FieldsViewModel() SetPassword1FromViewModelCommand = new AnotherCommandImplementation(_ => Password1 = "Set from ViewModel!"); SetPassword2FromViewModelCommand = new AnotherCommandImplementation(_ => Password2 = "Set from ViewModel!"); - _originalAutoSuggestBox1Suggestions = new List() - { + _originalAutoSuggestBox1Suggestions = + [ "Burger", "Fries", "Shake", "Lettuce" - }; + ]; - _originalAutoSuggestBox2Suggestions = new List>(GetColors()); + _originalAutoSuggestBox2Suggestions = new(GetColors()); + _originalAutoSuggestBox3Suggestions = + [ + "jsmith", "jdoe", "mscott", "pparker", "bwilliams", "ljohnson", "abrown", "dlee", "cmiller", "tmoore" + ]; AutoSuggestBox1Suggestions = new ObservableCollection(_originalAutoSuggestBox1Suggestions); } @@ -158,20 +158,10 @@ private static IEnumerable> GetColors() } } -public class FieldsTestObject : ViewModelBase +public partial class FieldsTestObject : ObservableObject { + [ObservableProperty] private string? _name; + [ObservableProperty] private string? _content; - - public string? Name - { - get => _name; - set => SetProperty(ref _name, value); - } - - public string? Content - { - get => _content; - set => SetProperty(ref _content, value); - } } diff --git a/src/MainDemo.Wpf/Fields.xaml b/src/MainDemo.Wpf/Fields.xaml index 432e141ac0..d8a4a38311 100644 --- a/src/MainDemo.Wpf/Fields.xaml +++ b/src/MainDemo.Wpf/Fields.xaml @@ -692,36 +692,36 @@ + VerticalAlignment="Center" + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Simple source list" /> + Suggestions="{Binding AutoSuggestBox1Suggestions}" + Text="{Binding AutoSuggestBox1Text, UpdateSourceTrigger=PropertyChanged}" /> + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="AutoSuggestBox with ItemTemplate" /> + materialDesign:HintAssist.Hint="Color" + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -733,23 +733,23 @@ + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Filled AutoSuggestBox" /> + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Style="{StaticResource MaterialDesignFilledAutoSuggestBox}" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -760,23 +760,23 @@ + Style="{StaticResource MaterialDesignSubtitle1TextBlock}" + Text="Outlined AutoSuggestBox" /> + materialDesign:TextFieldAssist.HasClearButton="True" + DropDownElevation="Dp0" + Style="{StaticResource MaterialDesignOutlinedAutoSuggestBox}" + Suggestions="{Binding AutoSuggestBox2Suggestions}" + Text="{Binding AutoSuggestBox2Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" + ValueMember="Key"> + Height="20" + Background="{Binding Value, Converter={StaticResource ColorToBrushConverter}}" + CornerRadius="10" /> @@ -784,6 +784,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs index 71d028d35e..699c1a5702 100644 --- a/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs +++ b/src/MaterialDesignThemes.Wpf/AutoSuggestBox.cs @@ -207,6 +207,10 @@ private void AutoSuggestionListBox_PreviewMouseDown(object sender, MouseButtonEv if (_autoSuggestBoxList is null || e.OriginalSource is not FrameworkElement element) return; + // If the user clicked on an interactive element, let it handle the event. + if (IsInteractiveElement(e.OriginalSource as DependencyObject)) + return; + var selectedItem = element.DataContext; if (!_autoSuggestBoxList.Items.Contains(selectedItem)) return; @@ -236,7 +240,18 @@ void OnSelectionChanged(object s, SelectionChangedEventArgs args) #endregion #region Methods - + private static bool IsInteractiveElement(DependencyObject? element) + { + while (element is not null) + { + if (element is ButtonBase or TextBoxBase or ComboBox) + { + return true; + } + element = VisualTreeHelper.GetParent(element); + } + return false; + } private void CloseAutoSuggestionPopUp() { IsSuggestionOpen = false; diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml index ca15834f6b..9a3bc6c955 100644 --- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml +++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.AutoSuggestBox.xaml @@ -273,6 +273,8 @@ Style="{StaticResource MaterialDesignElevatedCard}"> + + + + + + + + + + + + + + + + + + diff --git a/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs b/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs new file mode 100644 index 0000000000..5e5c461fef --- /dev/null +++ b/tests/MaterialDesignThemes.UITests/Samples/AutoSuggestBoxes/AutoSuggestTextBoxWithInteractiveTemplate.xaml.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace MaterialDesignThemes.UITests.Samples.AutoSuggestBoxes; + +/// +/// Interaction logic for AutoSuggestTextBoxWithInteractiveTemplate.xaml +/// +public partial class AutoSuggestTextBoxWithInteractiveTemplate : UserControl +{ + public AutoSuggestTextBoxWithInteractiveTemplate() + { + DataContext = new AutoSuggestTextBoxWithInteractiveTemplateViewModel(); + InitializeComponent(); + } +} + +public partial class AutoSuggestTextBoxWithInteractiveTemplateViewModel : ObservableObject +{ + private List _baseSuggestions; + + [ObservableProperty] + private List _suggestions = []; + + [ObservableProperty] + private string? _autoSuggestText; + + partial void OnAutoSuggestTextChanged(string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + var searchResult = _baseSuggestions.Where(x => IsMatch(x.Name, value)); + Suggestions = new(searchResult); + } + else + { + Suggestions = new(_baseSuggestions); + } + } + + public AutoSuggestTextBoxWithInteractiveTemplateViewModel() + { + _baseSuggestions = + [ + new("Apples"), + new("Bananas"), + new("Beans"), + new("Mtn Dew"), + new("Orange") + ]; + Suggestions = new(_baseSuggestions); + } + + private static bool IsMatch(string item, string currentText) + { +#if NET6_0_OR_GREATER + return item.Contains(currentText, StringComparison.OrdinalIgnoreCase); +#else + return item.IndexOf(currentText, StringComparison.OrdinalIgnoreCase) >= 0; +#endif + } +} + +public partial class SuggestionThing2(string name) : ObservableObject +{ + public string Name { get; } = name; + + [ObservableProperty] + private int _count = 0; + + [RelayCommand] + private void IncrementCount() => Count++; +} diff --git a/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs b/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs index f1302b56bf..b8f0f1b182 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/AutoSuggestBoxes/AutoSuggestTextBoxTests.cs @@ -232,6 +232,45 @@ static async Task AssertViewModelProperty(AutoSuggestBox autoSuggestBox) recorder.Success(); } + [Test] + public async Task AutoSuggestBox_ClickingButtonInInteractiveItemTemplate_DoesNotSelectOrClosePopup() + { + await using var recorder = new TestRecorder(App); + + // Arrange + IVisualElement suggestBox = (await LoadUserControl()).As(); + IVisualElement popup = await suggestBox.GetElement(); + IVisualElement suggestionListBox = await popup.GetElement(); + + // Act + await suggestBox.MoveKeyboardFocus(); + await suggestBox.SendInput(new KeyboardInput("a")); + await Task.Delay(50, TestContext.Current!.CancellationToken); + + // Find the button in the first suggestion item + var thirdListBoxItem = await suggestionListBox.GetElement("/ListBoxItem[2]"); + var button = await thirdListBoxItem.GetElement