wpf - example to enhance ComboBox for AutoComplete

first let’s see an example of the code (the behavior code that turns a combobox to a source of autocomplete source)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interactivity;
using Nomura.Fiet.Gator.UI.Services.StaticData;
using Nomura.Fiet.Gator.UI.Views.FlexDepth;
using Nomura.GM.Presentation.Extensions;
using Nomura.GM.Presentation.Threading;
using Nomura.GM.Common.Extensions;
using log4net;

namespace Nomura.Fiet.Gator.UI.Behaviors
{
    /// <summary>
    /// Focus Behavior, control shall be focused if the IsFocus dependency property is true
    /// </summary>
    public class InstrumentComboBehavior : Behavior<ComboBox>
    {
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        public static readonly DependencyProperty IsFocusedProperty = 
            DependencyProperty.Register(
            "IsFocused", 
            typeof(bool), 
            typeof(InstrumentComboBehavior), 
            new PropertyMetadata(default(bool), IsFocusedChanged));

        public static readonly DependencyProperty HasInitialFocusProperty = 
            DependencyProperty.Register(
            "HasInitialFocus", 
            typeof(bool), 
            typeof(InstrumentComboBehavior), 
            new PropertyMetadata(default(bool)));

        public static readonly DependencyProperty MaxInstrumentInDropDownProperty =
            DependencyProperty.Register(
            "MaxInstrumentInDropDown",
            typeof(int),
            typeof(InstrumentComboBehavior),
            new PropertyMetadata(20));

        public static readonly DependencyProperty InstrumentServiceProperty =
            DependencyProperty.Register(
            "InstrumentService",
            typeof(InstrumentService),
            typeof(InstrumentComboBehavior),
            new PropertyMetadata(null));

        private readonly List<DelayTask> _delayTasks = new List<DelayTask>();
        private readonly Delegate _textChangeHandler;

        private CancellationTokenSource _searchCancellationToken;
        private IList<InstrumentVm> _instrumentSource;
        private TextBox _editableTextBox;

        public InstrumentComboBehavior()
        {
            _searchCancellationToken = new CancellationTokenSource();
            _textChangeHandler = new TextChangedEventHandler(ComboBox_TextChanged);
        }

        public bool HasInitialFocus
        {
            get
            {
                return (bool)GetValue(HasInitialFocusProperty);
            }
            set
            {
                SetValue(HasInitialFocusProperty, value);
            }
        }

        public bool IsFocused
        {
            get
            {
                return (bool)GetValue(IsFocusedProperty);
            }
            set
            {
                SetValue(IsFocusedProperty, value);
            }
        }

        public InstrumentService InstrumentService
        {
            get
            {
                return (InstrumentService)GetValue(InstrumentServiceProperty);
            }

            set
            {
                SetValue(InstrumentServiceProperty, value);
            } 
        }

        public int MaxInstrumentInDropDown
        {
            get
            {
                return (int)GetValue(MaxInstrumentInDropDownProperty);
            }
            set
            {
                SetValue(MaxInstrumentInDropDownProperty, value);
            }
        }

        protected override void OnAttached()
        {
            AssociatedObject.Loaded += AssociatedObjectLoaded;
            AssociatedObject.DropDownOpened += AssociatedObject_DropDownOpened;
            AssociatedObject.AddHandler(TextBoxBase.TextChangedEvent, _textChangeHandler);
            base.OnAttached();
        }

        protected override void OnDetaching()
        {
            AssociatedObject.DropDownOpened -= AssociatedObject_DropDownOpened;

            if (_textChangeHandler != null)
            {
                AssociatedObject.RemoveHandler(TextBoxBase.TextChangedEvent, _textChangeHandler);
            }

            base.OnDetaching();
        }

        private void AssociatedObject_DropDownOpened(object sender, EventArgs e)
        {
            var combo = (ComboBox)sender;

            // prevent the inner text box from highlighting all after drop down opened.
            if (_editableTextBox != null && combo.SelectedItem == null)
            {
                _editableTextBox.Select(_editableTextBox.Text.Length, 0);
            }
        }

        private void ComboBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var combo = (ComboBox)sender;

            var viewModel = combo.DataContext as DepthViewerViewModel;

            if (viewModel != null && !viewModel.IsInstrumentComboValid)
            {
                viewModel.IsInstrumentComboValid = true;
            }

            if (combo.SelectedItem == null)
            {
                var newCallCancellation = new CancellationTokenSource();
                Interlocked.Exchange(ref _searchCancellationToken, newCallCancellation).Cancel();

                new DelayTask(TimeSpan.FromMilliseconds(300), _delayTasks)
                     .Task.ContinueWith(
                         t => SearchInstrument(combo, combo.Text),
                         _searchCancellationToken.Token,
                         TaskContinuationOptions.OnlyOnRanToCompletion,
                         UITaskSchedulerService.Instance.GetUITaskScheduler())
                     .LogTaskExceptionIfAny(Log);
            }
        }

        private void SearchInstrument(ComboBox combo, string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                combo.ItemsSource = _instrumentSource.Take(MaxInstrumentInDropDown);
            }
            else
            {
                combo.ItemsSource = _instrumentSource.Where(r => r.Alias.ToLower().Contains(key.ToLower())).OrderBy(r=>r.Alias)
                    .Concat(_instrumentSource.Where(r=>r.InstrDisplay.ToLower().Contains(key.ToLower())).OrderBy(r=>r.Alias))
                    .Distinct()
                    .Take(MaxInstrumentInDropDown);
            }

            combo.IsDropDownOpen = true;
        }


        private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Loaded -= AssociatedObjectLoaded;

            _editableTextBox = AssociatedObject.FindChild<TextBox>("PART_EditableTextBox");

            if (_editableTextBox != null)
            {
                _editableTextBox.MinWidth = 100;
            }

            _instrumentSource = new List<InstrumentVm>();

            if (InstrumentService != null)
            {
                InstrumentService.SearchInstruments("").ForEach(i => _instrumentSource.Add(GetInstrumentVm(i)));
            }

            AssociatedObject.ItemsSource = _instrumentSource.Take(MaxInstrumentInDropDown);
            if (HasInitialFocus || IsFocused)
            {
                GotFocus();
            }
        }

        private InstrumentVm GetInstrumentVm(Instrument inst)
        {
            return new InstrumentVm
                {
                    Alias = inst.Alias,
                    InstrDisplay = string.Format("{0, -12}\t {1,-30}", inst.Alias, inst.Description),
                };
        }

        private static void IsFocusedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            if ((bool)args.NewValue)
            {
                ((InstrumentComboBehavior)sender).GotFocus();
            }
            else
            {
                ((InstrumentComboBehavior)sender).ClearFocus();
            }
        }

        private void GotFocus()
        {
            new DelayTask(TimeSpan.FromMilliseconds(300), _delayTasks).Task.ContinueWith(
              t => AssociatedObject.Focus(),
              CancellationToken.None,
              TaskContinuationOptions.OnlyOnRanToCompletion,
              UITaskSchedulerService.Instance.GetUITaskScheduler());
        }

        private void ClearFocus()
        {
            AssociatedObject.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
        }
    }
}

Basically what it does is to create an internal datasource (List<InstrumentVm>) and turn that dynamically hijack the TextBox ItemsSource.

There are some thing that worth noticing now.

the default Combobox has issues that when you type first time, the text is highlighted. and this is due to some Internal problem that when the combobox ‘s drop down is opened. the Text is highlighted, we can fix that by the following code.

        private void AssociatedObject_DropDownOpened(object sender, EventArgs e)
        {
            var combo = (ComboBox)sender;

            // prevent the inner text box from highlighting all after drop down opened.
            if (_editableTextBox != null && combo.SelectedItem == null)
            {
                _editableTextBox.Select(_editableTextBox.Text.Length, 0);
            }
        }

you may wonder where the control _editableTextBoxis coming from:

        private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Loaded -= AssociatedObjectLoaded;

            _editableTextBox = AssociatedObject.FindChild<TextBox>("PART_EditableTextBox");
            // ...
        }

with the help of theVIsualTreeExtension, you can have find parent/child. the utility code is as follow.

public static T FindChild<T>(this DependencyObject parent, string childName) where T : DependencyObject
    {
      if (parent == null)
        return default (T);
      T obj = default (T);
      int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
      for (int childIndex = 0; childIndex < childrenCount; ++childIndex)
      {
        DependencyObject child = VisualTreeHelper.GetChild(parent, childIndex);
        if ((object) (child as T) == null)
        {
          obj = VisualTreeExtensions.FindChild<T>(child, childName);
          if ((object) obj != null)
            break;
        }
        else if (!string.IsNullOrEmpty(childName))
        {
          FrameworkElement frameworkElement = child as FrameworkElement;
          if (frameworkElement != null && frameworkElement.Name == childName)
          {
            obj = (T) child;
            break;
          }
        }
        else
        {
          obj = (T) child;
          break;
        }
      }
      return obj;
    }

there are also some problems with width (the ComboBox’s TextBox control has MinWidth issue), which can be resolved by the following hack.

        private void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
        {
            _editableTextBox = AssociatedObject.FindChild<TextBox>("PART_EditableTextBox");

            if (_editableTextBox != null)
            {
                _editableTextBox.MinWidth = 100;
            }
            // ...
        }

the xaml file for the combobox is as follow.

 

                <ComboBox  x:Name="cmbx" Grid.Row="1" Grid.Column="0" Style="{DynamicResource NomuraComboBox}" Margin="4" Text="{Binding SearchTxt, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding SearchTxt}" SelectedValuePath="Alias" DisplayMemberPath="InstrDisplay" TextSearch.TextPath="Alias" IsTextSearchEnabled="False" IsEditable="True" ToolTip="{Binding InstrumentInputErrorMessage, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" >
                    <ComboBox.Resources>
                        <SolidColorBrush x:Key="{x:Static SystemColors.WindowBrushKey}" Color="LightGray" />
                    </ComboBox.Resources>
          <i:Interaction.Behaviors>
            <behaviors:InstrumentComboBehavior  InstrumentService="{Binding InstrumentService}" IsFocused="{Binding DisplayInstrumentSelection, Mode=OneWay}" />
          </i:Interaction.Behaviors>
                </ComboBox>

你可能感兴趣的:(C#,WPF)