WPF组合框中的AutoSuggestion

时间:2012-04-22 04:33:44

标签: c# wpf visual-studio-2010 xaml

我的组合框从s存储过程中返回一组值

private void BindCombo()
{
    DataCombo.FillCombo(ComboDS(2313001), cmbClass, 0);
    DataCombo.FillCombo(DDCombo(5007), cmbGroup, 0);
}

我设法提供了一个基本的自动完整建议IsTextSearchenabled,但无法获得我想要的自动建议框。

我已经看到大量自动填充/暗示文本框的例子,但它们似乎都不适合我。

这段代码显然适合我。

但我如何使用此处的自动建议

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace DotNetZen.AutoFilteredComboBox
{
    public class AutoFilteredComboBox : ComboBox
    {
        private int silenceEvents = 0;

    /// <summary>
    /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
    /// </summary>
    public AutoFilteredComboBox()
    {
        DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty(
            ComboBox.TextProperty, typeof(AutoFilteredComboBox));
        textProperty.AddValueChanged(this, this.OnTextChanged);

        this.RegisterIsCaseSensitiveChangeNotification();
    }

    #region IsCaseSensitive Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsCaseSensitiveProperty =
        DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));

    /// <summary>
    /// Gets or sets the way the combo box treats the case sensitivity of typed text.
    /// </summary>
    /// <value>The way the combo box treats the case sensitivity of typed text.</value>
    [System.ComponentModel.Description("The way the combo box treats the case sensitivity of typed text.")]
    [System.ComponentModel.Category("AutoFiltered ComboBox")]
    [System.ComponentModel.DefaultValue(true)]
    public bool IsCaseSensitive
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(IsCaseSensitiveProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(IsCaseSensitiveProperty, value);
        }
    }

    protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e)
    {
        if (this.IsCaseSensitive)
            this.IsTextSearchEnabled = false;

        this.RefreshFilter();
    }

    private void RegisterIsCaseSensitiveChangeNotification()
    {
        System.ComponentModel.DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)).AddValueChanged(
            this, this.OnIsCaseSensitiveChanged);
    }
    #endregion

    #region DropDownOnFocus Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty DropDownOnFocusProperty =
        DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(true));

    /// <summary>
    /// Gets or sets the way the combo box behaves when it receives focus.
    /// </summary>
    /// <value>The way the combo box behaves when it receives focus.</value>
    [System.ComponentModel.Description("The way the combo box behaves when it receives focus.")]
    [System.ComponentModel.Category("AutoFiltered ComboBox")]
    [System.ComponentModel.DefaultValue(true)]
    public bool DropDownOnFocus
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(DropDownOnFocusProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(DropDownOnFocusProperty, value);
        }
    }
    #endregion

    #region | Handle selection |
    /// <summary>
    /// Called when <see cref="ComboBox.ApplyTemplate()"/> is called.
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        this.EditableTextBox.SelectionChanged += this.EditableTextBox_SelectionChanged;
    }

    /// <summary>
    /// Gets the text box in charge of the editable portion of the combo box.
    /// </summary>
    protected TextBox EditableTextBox
    {
        get
        {
            return ((TextBox)base.GetTemplateChild("PART_EditableTextBox"));
        }
    }

    private int start = 0, length = 0;

    private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e)
    {
        if (this.silenceEvents == 0)
        {
            this.start = ((TextBox)(e.OriginalSource)).SelectionStart;
            this.length = ((TextBox)(e.OriginalSource)).SelectionLength;

            this.RefreshFilter();
        }
    }
    #endregion

    #region | Handle focus |
    /// <summary>
    /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
    /// reaches this element in its route.
    /// </summary>
    /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);

        if (this.ItemsSource != null && this.DropDownOnFocus)
        {
            this.IsDropDownOpen = true;
        }
    }
    #endregion

    #region | Handle filtering |
    private void RefreshFilter()
    {
        if (this.ItemsSource != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
            view.Refresh();
            this.IsDropDownOpen = true;
        }
    }

    private bool FilterPredicate(object value)
    {
        // We don't like nulls.
        if (value == null)
            return false;

        // If there is no text, there's no reason to filter.
        if (this.Text.Length == 0)
            return true;

        string prefix = this.Text;

        // If the end of the text is selected, do not mind it.
        if (this.length > 0 && this.start + this.length == this.Text.Length)
        {
            prefix = prefix.Substring(0, this.start);
        }

        return value.ToString()
            .StartsWith(prefix, !this.IsCaseSensitive, CultureInfo.CurrentCulture);
    }
    #endregion

    /// <summary>
    /// Called when the source of an item in a selector changes.
    /// </summary>
    /// <param name="oldValue">Old value of the source.</param>
    /// <param name="newValue">New value of the source.</param>
    protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
    {
        if (newValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
            view.Filter += this.FilterPredicate;
        }

        if (oldValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
            view.Filter -= this.FilterPredicate;
        }

        base.OnItemsSourceChanged(oldValue, newValue);
    }

    private void OnTextChanged(object sender, EventArgs e)
    {
        if (!this.IsTextSearchEnabled && this.silenceEvents == 0)
        {
            this.RefreshFilter();

            // Manually simulate the automatic selection that would have been
            // available if the IsTextSearchEnabled dependency property was set.
            if (this.Text.Length > 0)
            {
                foreach (object item in CollectionViewSource.GetDefaultView(this.ItemsSource))
                {
                    int text = item.ToString().Length, prefix = this.Text.Length;
                    this.SelectedItem = item;

                    this.silenceEvents++;
                    this.EditableTextBox.Text = item.ToString();
                    this.EditableTextBox.Select(prefix, text - prefix);
                    this.silenceEvents--;
                    break;
                }
            }
        }
    }
}
}

2 个答案:

答案 0 :(得分:2)

还发现AutoFilteredComboBox非常简单易用。虽然我做了一些改变:

  • 删除使用DependencyPropertyDescriptor以避免组合框对象的内存泄漏
  • 引入了FilterItem-event和FilterList-event以允许自定义过滤
  • 将默认过滤从starts-with-string更改为contains-string
  • 删除了启用IsTextSearchEnabled的支持
  • 一旦更改搜索字符串,就会显示下拉列表,因此会显示搜索结果

如何使用它的示例:

<Controls:AutoFilteredComboBox ItemsSource="{Binding ViewModel.AvailableItems}"
        SelectedValue="{Binding ViewModel.SelectedItem, Mode=TwoWay}"
        IsEditable="True" IsTextSearchEnabled="False"/>

AutoFilteredComboBox的改进版本:

public class AutoFilteredComboBox : ComboBox
{
    bool _ignoreTextChanged;
    string _currentText;

    /// <summary>
    /// Creates a new instance of <see cref="AutoFilteredComboBox" />.
    /// </summary>
    public AutoFilteredComboBox()
    {
        if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) return;
    }

    public event Func<object, string, bool> FilterItem;
    public event Action<string> FilterList;

    #region IsCaseSensitive Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsCaseSensitiveProperty =
        DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));

    /// <summary>
    /// Gets or sets the way the combo box treats the case sensitivity of typed text.
    /// </summary>
    /// <value>The way the combo box treats the case sensitivity of typed text.</value>
    [Description("The way the combo box treats the case sensitivity of typed text.")]
    [Category("AutoFiltered ComboBox")]
    [DefaultValue(true)]
    public bool IsCaseSensitive
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(IsCaseSensitiveProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(IsCaseSensitiveProperty, value);
        }
    }
    #endregion

    #region DropDownOnFocus Dependency Property
    /// <summary>
    /// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty DropDownOnFocusProperty =
        DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));

    /// <summary>
    /// Gets or sets the way the combo box behaves when it receives focus.
    /// </summary>
    /// <value>The way the combo box behaves when it receives focus.</value>
    [Description("The way the combo box behaves when it receives focus.")]
    [Category("AutoFiltered ComboBox")]
    [DefaultValue(false)]
    public bool DropDownOnFocus
    {
        [System.Diagnostics.DebuggerStepThrough]
        get
        {
            return (bool)this.GetValue(DropDownOnFocusProperty);
        }
        [System.Diagnostics.DebuggerStepThrough]
        set
        {
            this.SetValue(DropDownOnFocusProperty, value);
        }
    }
    #endregion

    #region | Handle focus |
    /// <summary>
    /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
    /// reaches this element in its route.
    /// </summary>
    /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);

        if (this.ItemsSource != null && this.DropDownOnFocus)
        {
            this.IsDropDownOpen = true;
        }
    }
    #endregion

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        AddHandler(TextBox.TextChangedEvent, new TextChangedEventHandler(OnTextChanged));
        KeyUp += AutoFilteredComboBox_KeyUp;
        this.IsTextSearchEnabled = false;
    }

    void AutoFilteredComboBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Down)
        {
            if (this.IsDropDownOpen == true)
            {
                // Ensure that focus is given to the dropdown list
                if (Keyboard.FocusedElement is TextBox)
                {
                    Keyboard.Focus(this);
                    if (this.Items.Count > 0)
                    {
                        if (this.SelectedIndex == -1 || this.SelectedIndex==0)
                            this.SelectedIndex = 0;
                    }
                }
            }
        }
        if (Keyboard.FocusedElement is TextBox)
        {
            if (e.OriginalSource is TextBox)
            {
                // Avoid the automatic selection of the first letter (As next letter will cause overwrite)
                TextBox textBox = e.OriginalSource as TextBox;
                if (textBox.Text.Length == 1 && textBox.SelectionLength == 1)
                {
                    textBox.SelectionLength = 0;
                    textBox.SelectionStart = 1;
                }
            }
        }
    }

    #region | Handle filtering |
    private void RefreshFilter()
    {
        if (this.ItemsSource != null)
        {
            Action<string> filterList = FilterList;
            if (filterList != null)
            {
                filterList(_currentText);
            }
            else
            {
                ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
                view.Refresh();
            }
            this.SelectedIndex = -1;    // Prepare so arrow down selects first
            this.IsDropDownOpen = true;
        }
    }

    private bool FilterPredicate(object value)
    {
        // We don't like nulls.
        if (value == null)
            return false;

        // If there is no text, there's no reason to filter.
        if (string.IsNullOrEmpty(_currentText))
            return true;

        Func<object, string, bool> filterItem = FilterItem;
        if (filterItem != null)
            return filterItem(value, _currentText);

        if (IsCaseSensitive)
            return value.ToString().Contains(_currentText);
        else
            return value.ToString().ToUpper().Contains(_currentText.ToUpper());                
    }
    #endregion

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        try
        {
            _ignoreTextChanged = true;  // Ignore the following TextChanged
            base.OnSelectionChanged(e);
        }
        finally
        {
            _ignoreTextChanged = false;
        }
    }

    /// <summary>
    /// Called when the source of an item in a selector changes.
    /// </summary>
    /// <param name="oldValue">Old value of the source.</param>
    /// <param name="newValue">New value of the source.</param>
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (newValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
            if (FilterList == null)
                view.Filter += this.FilterPredicate;
        }

        if (oldValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
            view.Filter -= this.FilterPredicate;
        }
        base.OnItemsSourceChanged(oldValue, newValue);
    }

    private void OnTextChanged(object sender, TextChangedEventArgs e)
    {
        if (_ignoreTextChanged)
            return;

        _currentText = Text;

        if (!this.IsTextSearchEnabled)
        {
            this.RefreshFilter();
        }
    }

答案 1 :(得分:1)

我找到了一个解决我问题的超级简单方法。

我创建了组合框的预览文本输入事件。

然后我写了

Combobox.IsDropDownOpen = true

可能不是最优雅但可以在我的情况下工作