当 ComboBox 上的 SelectedIndex 发生变化时,防止文本发生变化

Prevent text from changing when SelectedIndex changes on ComboBox

我正在从 ComboBox 创建 UserControl。我的目标是让用户能够写出卡片名称的一部分,当他写的时候,匹配字符串的卡片如下所示,然后用户可以使用光标 select他想要的卡片。

这是我目前的情况:

XAML:

<UserControl x:Class="UrSimulator.View.UserControls.SearchMaxedCardComboBox"
             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:System="clr-namespace:System;assembly=mscorlib"
             mc:Ignorable="d">
    <UserControl.Resources>
        <DataTemplate x:Key="MaxedCardTemplate">
            <!-- The template works so I've removed it to avoid clutter -->
        </DataTemplate>
    </UserControl.Resources>
        <ComboBox x:Name="SearchBox"
                  HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" 
                  IsEditable="True" IsSynchronizedWithCurrentItem="False" IsTextSearchEnabled="False" 
                  ItemTemplate="{StaticResource MaxedCardTemplate}"
                  TextBoxBase.TextChanged="ComboBox_TextChanged" 
                  GotFocus="ComboBox_GotFocus" 
                  LostFocus="ComboBox_LostFocus"/>
</UserControl>

后面的代码:

public partial class SearchMaxedCardComboBox : UserControl
{
    public InMemoryManager InMemoryManager { get; set; } // In Memory Database where cards are stored
    public CardBase SelectedCard { get; set; }

    private string DefaultText; 

    public SearchMaxedCardComboBox()
    {
        InitializeComponent();
        DefaultText = Properties.UIStrings.ui_calculator_search_card; // == Name (min 2 chars)
        SearchBox.Text = DefaultText;
    }

    private void ComboBox_GotFocus(object sender, RoutedEventArgs e)
    {
        ComboBox control = sender as ComboBox;
        control.Text = "";
        control.IsDropDownOpen = true;
    }
    private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
    {
        ComboBox control = sender as ComboBox;
        control.IsDropDownOpen = false;

        if (SelectedCard == null)
            control.Text = DefaultText;
        else
            control.Text = SelectedCard.Name;
    }
    private void ComboBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        ComboBox control = sender as ComboBox;

        if (control.Text == DefaultText)
            return;

        Debug.Assert(InMemoryManager != null);            

        List<string> names;
        if (control.Text.Length < 2) // If a search happens with 1 char or an empty string it slows down too much
            names = new List<string>();
        else
            names = InMemoryManager.LookForCardNames(control.Text); // List<string> with the names

        List<CardBase> cards = new List<CardBase>();
        foreach (string name in names)
            cards.Add(InMemoryManager.GetCardBase(name));

        control.Items.Clear();
        foreach (CardBase card in cards)
            control.Items.Add(new MaxedCardBaseViewModel(card));
    }

    private void SearchBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ComboBox control = sender as ComboBox;
        if (control.SelectedItem != null)
            SelectedCard = ((MaxedCardBaseViewModel)control.SelectedItem).Card;
    }
}

问题:SelectedIndex 改变时, ComboBox 上的文本也会改变。然后文本匹配 selected 项目(在这种情况下它成为项目的 class 名称),TextChanged 再次启动,搜索没有结果,并且列表项目结束空。

如何避免在 select 编辑项目时更改文本?


更新: 我正在尝试这个 said, and it made me realize that my question somewhat suffers from the XY Problem,但我仍在解决问题中的类似代码,并将尝试使用 "Auto-Complete Combobox" 他链接的代码。

我发现这个 question 本质上和我的一样,但是 WinFormsWPF 没有 OnSelectionChangeCommitted 也没有 TextUpdate 替代方案,因为它们的工作方式不同于 OnSelectionChangeTextChanged.

使用 Sinatr 链接的内容,我已经成功地创建了一个可以按我希望的方式工作的控件。它执行以下操作:

  • 基础是 ComboBox,因此它有一个 TextBox 和一个 ListBox 集成在一个控件上。
  • ItemsSource随着Text的变化而更新,所以只显示
  • 的元素
  • 当用户通过键盘更改选择时,必须使用 Enter 或 Tab 进行确认
  • 只有 "confirmed" 项选择(Enter、Tab、鼠标)会修改 SelectedCard
  • 它只在文本最少 2 个字符时搜索(默认)

它还有一些怪癖:

  • 如果用户使用键盘 (Up/Down) 更改索引,然后在外部单击,则会选择最后突出显示的项目。这不是一个大问题,但很烦人,所以它不会成为优先事项。
  • 当使用 ItemsTemplate 并且列表大到放不下时出现视觉错误,当使用键盘滚动超过视图限制时,所选项目不会滚动到。

即使有那些最小的怪癖,我认为它已经解决了。

代码如下:

XAML:

<UserControl x:Class="MyApp.SearchCardControl"
             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:UrSimulator.View.UserControls"
             mc:Ignorable="d">
    <UserControl.Resources>
        <DataTemplate x:Key="CardTemplate">
        <!-- ... -->
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <local:SearchComboBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="Auto" 
            x:Name="SearchBox"
            ItemTemplate="{StaticResource CardTemplate}"
            IsEditable="True" 
            IsSynchronizedWithCurrentItem="False" 
            IsTextSearchEnabled="False" 
            StaysOpenOnEdit="True"
            ScrollViewer.CanContentScroll="False"
            TextBoxBase.TextChanged="TextBox_TextChanged" 
            SelectionChanged="SearchBox_SelectionChanged" 
            LostKeyboardFocus="SearchBox_LostKeyboardFocus" />
    </Grid>
</UserControl>

后面的代码:

public partial class SearchCardControl : UserControl, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public static readonly DependencyProperty SelectedCardProperty =
        DependencyProperty.Register("SelectedCard", typeof(CardBase), typeof(SearchCardControl), new FrameworkPropertyMetadata(null));

    public InMemoryManager InMemoryManager { get; set; }
    public CardBase SelectedCard
    {
        get { return (CardBase)GetValue(SelectedCardProperty); }
        set
        {
            SetValue(SelectedCardProperty, value);
            this.Notify(PropertyChanged);
        }
    }

    private int _minimumSearchChars;
    public int MinimumSearchChars
    {
        get { return _minimumSearchChars; }
        set { if (value > 0) _minimumSearchChars = value; }
    }
    public string DefaultText { get { return String.Format(Properties.UIStrings.ui_calculator_search_card, MinimumSearchChars); } }

    public SearchCardControl()
    {
        InitializeComponent();
        SearchBox.SetTextWithoutSearching(DefaultText);
        MinimumSearchChars = 2;
    }

    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        SearchComboBox control = (SearchComboBox)sender;
        if (control.IsSearchNeeded)
        {
            if (control.Text.Length >= MinimumSearchChars)
                control.ItemsSource = Search(control.Text);
            else
                control.ItemsSource = new List<object>();
        }
    }
    private void SearchBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SearchComboBox control = (SearchComboBox)sender;
        if (control.SelectedItem == null)
            control.SetTextWithoutSearching(DefaultText);
        else
        {
            SelectedCard = (CardBase)control.SelectedItem;
            control.SetTextWithoutSearching(SelectedCard.Name);
        }
    }
    private void SearchBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        if (SelectedCard == null)
            SearchBox.SetTextWithoutSearching(DefaultText);
        else
            SearchBox.Text = SelectedCard.Name;
    }

    private List<CardBase> Search(string partialName)
    {
        // Whatever floats your boat
        // MyList.FindAll(x => x.FieldToCompareForExampleCardName.IndexOf(partialName, StringComparison.OrdinalIgnoreCase) >= 0);
        // Or you could implement a delegate here
    }
}

internal class SearchComboBox : ComboBox
{
    internal bool IsSearchNeeded = true;
    internal SelectionChangedEventArgs LastOnSelectionChangedArgs;

    internal void SetTextWithoutSearching(string text)
    {
        IsSearchNeeded = false;
        Text = text;
        IsSearchNeeded = true;
    }

    protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
    {
        if (IsSearchNeeded)
        {
            Text = "";
            IsDropDownOpen = true;
        }
        base.OnGotKeyboardFocus(e);
    }
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {  
        if (SelectedIndex != -1)
            LastOnSelectionChangedArgs = e;
    }
    protected override void OnDropDownClosed(EventArgs e)
    {
        if (LastOnSelectionChangedArgs != null)
            base.OnSelectionChanged(LastOnSelectionChangedArgs);
        base.OnDropDownClosed(e);
    }
    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        if (e.Key == Key.Tab || e.Key == Key.Enter)
        {
            IsDropDownOpen = false;
        }
        else if (e.Key == Key.Escape)
        {
            SelectedIndex = -1;
            IsDropDownOpen = false;
        }
        else
        {
            if (e.Key == Key.Down)
                this.IsDropDownOpen = true;
            base.OnPreviewKeyDown(e);
        }
    }
    protected override void OnKeyUp(KeyEventArgs e)
    {
        if (!(e.Key == Key.Up || e.Key == Key.Down || e.Key == Key.Tab || e.Key == Key.Enter))
            base.OnKeyUp(e);
    }
}