1 using System; 2 using System.Collections; 3 using System.ComponentModel; 4 using System.Diagnostics; 5 using System.Windows; 6 using System.Windows.Controls; 7 using System.Windows.Data; 8 9 namespace WHQ.QA.Client.Windows.Controls 10 { 11 public class AutoFilteredComboBox : ComboBox 12 { 13 private int _silenceEvents; 14 15 /// <summary> 16 /// Creates a new instance of <see cref="AutoFilteredComboBox" />. 17 /// </summary> 18 public AutoFilteredComboBox() 19 { 20 DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty( 21 TextProperty, typeof(AutoFilteredComboBox)); 22 textProperty.AddValueChanged(this, OnTextChanged); 23 RegisterIsCaseSensitiveChangeNotification(); 24 IsEditable = true; 25 } 26 27 #region IsCaseSensitive Dependency Property 28 29 /// <summary> 30 /// The <see cref="DependencyProperty" /> object of the <see cref="IsCaseSensitive" /> dependency property. 31 /// </summary> 32 public static readonly DependencyProperty IsCaseSensitiveProperty = 33 DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), 34 new UIPropertyMetadata(false)); 35 36 /// <summary> 37 /// Gets or sets the way the combo box treats the case sensitivity of typed text. 38 /// </summary> 39 /// <value>The way the combo box treats the case sensitivity of typed text.</value> 40 [Description("The way the combo box treats the case sensitivity of typed text.")] 41 [Category("AutoFiltered ComboBox")] 42 [DefaultValue(true)] 43 public bool IsCaseSensitive 44 { 45 [DebuggerStepThrough] 46 get { return (bool)GetValue(IsCaseSensitiveProperty); } 47 [DebuggerStepThrough] 48 set { SetValue(IsCaseSensitiveProperty, value); } 49 } 50 51 protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e) 52 { 53 if (IsCaseSensitive) 54 IsTextSearchEnabled = false; 55 56 RefreshFilter(); 57 } 58 59 private void RegisterIsCaseSensitiveChangeNotification() 60 { 61 DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)) 62 .AddValueChanged( 63 this, OnIsCaseSensitiveChanged); 64 } 65 66 #endregion 67 68 #region DropDownOnFocus Dependency Property 69 70 /// <summary> 71 /// The <see cref="DependencyProperty" /> object of the <see cref="DropDownOnFocus" /> dependency property. 72 /// </summary> 73 public static readonly DependencyProperty DropDownOnFocusProperty = 74 DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), 75 new UIPropertyMetadata(true)); 76 77 /// <summary> 78 /// Gets or sets the way the combo box behaves when it receives focus. 79 /// </summary> 80 /// <value>The way the combo box behaves when it receives focus.</value> 81 [Description("The way the combo box behaves when it receives focus.")] 82 [Category("AutoFiltered ComboBox")] 83 [DefaultValue(true)] 84 public bool DropDownOnFocus 85 { 86 [DebuggerStepThrough] 87 get { return (bool)GetValue(DropDownOnFocusProperty); } 88 [DebuggerStepThrough] 89 set { SetValue(DropDownOnFocusProperty, value); } 90 } 91 92 #endregion 93 94 #region | Handle selection | 95 96 private int length; 97 private int start; 98 99 /// <summary> 100 /// Gets the text box in charge of the editable portion of the combo box. 101 /// </summary> 102 protected TextBox EditableTextBox 103 { 104 get 105 { 106 var txtBox = (TextBox)Template.FindName("PART_EditableTextBox", this); 107 return txtBox; 108 } 109 } 110 111 public int SecondToFilter { get; set; } 112 113 /// <summary> 114 /// Called when <see cref="ComboBox.ApplyTemplate()" /> is called. 115 /// </summary> 116 public override void OnApplyTemplate() 117 { 118 base.OnApplyTemplate(); 119 120 EditableTextBox.SelectionChanged += EditableTextBox_SelectionChanged; 121 } 122 123 private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e) 124 { 125 if (_silenceEvents == 0) 126 { 127 start = ((TextBox)(e.OriginalSource)).SelectionStart; 128 length = ((TextBox)(e.OriginalSource)).SelectionLength; 129 130 RefreshFilter(); 131 } 132 } 133 134 #endregion 135 136 #region | Handle focus | 137 138 /// <summary> 139 /// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event 140 /// reaches this element in its route. 141 /// </summary> 142 /// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param> 143 protected override void OnGotFocus(RoutedEventArgs e) 144 { 145 base.OnGotFocus(e); 146 147 if (ItemsSource != null && DropDownOnFocus) 148 { 149 IsDropDownOpen = true; 150 } 151 } 152 153 #endregion 154 155 #region | Handle filtering | 156 157 private void RefreshFilter() 158 { 159 if (ItemsSource != null) 160 { 161 var view = CollectionViewSource.GetDefaultView(ItemsSource); 162 view.Refresh(); 163 IsDropDownOpen = true; 164 } 165 } 166 167 168 private bool FilterPredicate(object value) 169 { 170 // We don't like nulls. 171 if (value == null) 172 return false; 173 174 // If there is no text, there's no reason to filter. 175 if (Text.Length == 0) 176 return true; 177 string thistext = GetTextBySearchPath(value); 178 string prefix = Text; 179 180 // If the end of the text is selected, do not mind it. 181 if (length > 0 && start + length == Text.Length) 182 { 183 prefix = prefix.Substring(0, start); 184 } 185 if (!IsCaseSensitive) 186 return thistext.IndexOf(prefix, StringComparison.CurrentCultureIgnoreCase) != -1; 187 return thistext.Contains(prefix); 188 } 189 190 private string GetTextBySearchPath(object value) 191 { 192 var type = value.GetType(); 193 var textPath = TextSearch.GetTextPath(this); 194 var propertyInfo = type.GetProperty(textPath); 195 var propertyValue = propertyInfo.GetValue(value); 196 return (string)propertyValue; 197 } 198 199 #endregion 200 201 /// <summary> 202 /// Called when the source of an item in a selector changes. 203 /// </summary> 204 /// <param name="oldValue">Old value of the source.</param> 205 /// <param name="newValue">New value of the source.</param> 206 protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) 207 { 208 if (newValue != null) 209 { 210 ICollectionView view = CollectionViewSource.GetDefaultView(newValue); 211 view.Filter += FilterPredicate; 212 } 213 214 if (oldValue != null) 215 { 216 ICollectionView view = CollectionViewSource.GetDefaultView(oldValue); 217 view.Filter -= FilterPredicate; 218 } 219 220 base.OnItemsSourceChanged(oldValue, newValue); 221 } 222 223 private void OnTextChanged(object sender, EventArgs e) 224 { 225 if (!IsTextSearchEnabled && _silenceEvents == 0) 226 { 227 RefreshFilter(); 228 if (Text.Length > 0) 229 { 230 var collectionView = CollectionViewSource.GetDefaultView(ItemsSource); 231 foreach (object item in collectionView) 232 { 233 int itemlength = item.ToString().Length, prefix = Text.Length; 234 SelectedItem = item; 235 236 _silenceEvents++; 237 EditableTextBox.Text = item.ToString(); 238 EditableTextBox.Select(prefix, itemlength - prefix); 239 _silenceEvents--; 240 break; 241 } 242 } 243 } 244 } 245 } 246 }