Last active
December 23, 2018 10:50
-
-
Save AnshulKuthiala/14dd56477ee0ee025a2c3cbfc44ccfec to your computer and use it in GitHub Desktop.
[SuggestTextBox]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Windows.Forms; | |
public class SuggestTextBox : TextBox | |
{ | |
#region fields and properties | |
public List<object> Items { get; set; } | |
public int MaxDropDownItems { get; set; } | |
public int SelectedIndex { get; set; } | |
public object SelectedItem { get; set; } | |
public delegate void dgEventRaiser(); | |
public event dgEventRaiser MadeSelection; | |
private readonly ListBox suggestionListBox = new ListBox { Visible = false, TabStop = false }; | |
private readonly BindingList<object> suggBindingList = new BindingList<object>(); | |
private Expression<Func<object, int, bool>> filterRule; | |
private Func<object, bool> filterRuleCompiled; | |
private Expression<Func<object, int>> suggestListOrderRule; | |
private Func<object, int> suggestListOrderRuleCompiled; | |
public int SuggestBoxHeight | |
{ | |
get => suggestionListBox.Height; | |
set { if (value > 0) suggestionListBox.Height = value; } | |
} | |
public Expression<Func<object, int, bool>> FilterRule | |
{ | |
get => filterRule; | |
set | |
{ | |
if (value == null) return; | |
filterRule = value; | |
filterRuleCompiled = item => value.Compile()(item, SuggestionScore(item, Text)); | |
} | |
} | |
///<summary> | |
/// Lambda-Expression to order the suggested items | |
/// (as Expression here because simple lamda (func) is not serializable) | |
/// <para>default: alphabetic ordering</para> | |
///</summary> | |
public Expression<Func<object, int>> SuggestListOrderRule | |
{ | |
get => suggestListOrderRule; | |
set | |
{ | |
if (value == null) return; | |
suggestListOrderRule = value; | |
suggestListOrderRuleCompiled = item => value.Compile()(SuggestionScore(item, Text)); | |
} | |
} | |
#endregion | |
/// <summary> | |
/// ctor | |
/// </summary> | |
public SuggestTextBox() | |
{ | |
Items = new List<object>(); | |
MaxDropDownItems = 5; | |
filterRuleCompiled = s => SuggestionScore(s, Text) > 0; | |
suggestListOrderRuleCompiled = s => SuggestionScore(s, Text); | |
suggestionListBox.DataSource = suggBindingList; | |
suggestionListBox.Click += SuggestionListBoxOnClick; | |
ParentChanged += OnParentChanged; | |
} | |
/// <summary> | |
/// the magic happens here ;-) | |
/// </summary> | |
/// <param name="e"></param> | |
protected override void OnTextChanged(EventArgs e) | |
{ | |
base.OnTextChanged(e); | |
if (!Focused) return; | |
suggBindingList.Clear(); | |
suggBindingList.RaiseListChangedEvents = false; | |
Items.Where(filterRuleCompiled) | |
.OrderByDescending(suggestListOrderRuleCompiled) | |
.ToList() | |
.ForEach(suggBindingList.Add); | |
suggBindingList.RaiseListChangedEvents = true; | |
suggBindingList.ResetBindings(); | |
if (suggestionListBox.Visible = suggBindingList.Any()) | |
{ | |
suggestionListBox.BringToFront(); | |
} | |
if (suggBindingList.Count == 1 && | |
suggBindingList.Single().ToString().Length == Text.Trim().Length) | |
{ | |
Text = suggBindingList.Single().ToString(); | |
Select(0, Text.Length); | |
suggestionListBox.Visible = false; | |
} | |
suggestionListBox.Height = Math.Min(suggestionListBox.PreferredHeight, MaxDropDownItems * 19); | |
suggestionListBox.Width = Math.Max(suggestionListBox.PreferredSize.Width, Width); | |
} | |
#region size and position of suggest box | |
/// <summary> | |
/// suggest-ListBox is added to parent control | |
/// (in ctor parent isn't already assigned) | |
/// </summary> | |
/// <param name="sender"></param> | |
/// <param name="e"></param> | |
private void OnParentChanged(object sender, EventArgs e) | |
{ | |
Parent.Controls.Add(suggestionListBox); | |
Parent.Controls.SetChildIndex(suggestionListBox, 0); | |
Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); | |
suggestionListBox.Top = Top + Height; | |
suggestionListBox.Left = Left; | |
suggestionListBox.Width = Width; | |
suggestionListBox.BorderStyle = BorderStyle.None; | |
suggestionListBox.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); | |
} | |
protected override void OnLocationChanged(EventArgs e) | |
{ | |
base.OnLocationChanged(e); | |
suggestionListBox.Top = Top + Height; | |
suggestionListBox.Left = Left; | |
} | |
protected override void OnSizeChanged(EventArgs e) | |
{ | |
base.OnSizeChanged(e); | |
suggestionListBox.Top = Top + Height; | |
suggestionListBox.Left = Left; | |
suggestionListBox.Width = Width; | |
} | |
#endregion | |
#region visibility of suggest box | |
protected override void OnLostFocus(EventArgs e) | |
{ | |
if (!suggestionListBox.Focused) | |
HideSuggBox(); | |
base.OnLostFocus(e); | |
} | |
private void SuggestionListBoxOnClick(object sender, EventArgs eventArgs) | |
{ | |
MarkAsSelected(); | |
Focus(); | |
} | |
private void HideSuggBox() | |
{ | |
suggestionListBox.Visible = false; | |
} | |
#endregion | |
#region keystroke events | |
private bool ProcessKeyDown(Keys keyData) | |
{ | |
if (suggestionListBox.Visible) | |
{ | |
switch (keyData) | |
{ | |
case Keys.Down: | |
if (suggestionListBox.SelectedIndex < suggBindingList.Count - 1) | |
suggestionListBox.SelectedIndex++; | |
return true; | |
case Keys.Up: | |
if (suggestionListBox.SelectedIndex > 0) | |
suggestionListBox.SelectedIndex--; | |
return true; | |
case Keys.Enter: | |
MarkAsSelected(); | |
return true; | |
case Keys.Escape: | |
HideSuggBox(); | |
return true; | |
} | |
} | |
return false; | |
} | |
protected override bool ProcessCmdKey(ref Message msg, Keys keyData) | |
{ | |
if (ProcessKeyDown(keyData)) | |
return true; | |
return base.ProcessCmdKey(ref msg, keyData); | |
} | |
#endregion | |
#region Misc | |
private static int SuggestionScore(object suggestion, string input) | |
{ | |
if (string.IsNullOrEmpty(input)) | |
{ | |
return 0; | |
} | |
if (string.IsNullOrWhiteSpace(input)) | |
{ | |
return 1; | |
} | |
string smallSuggestion = suggestion.ToString().ToLower(); | |
string smallInput = input.ToLower(); | |
int score = 0; | |
score += GetScore(smallSuggestion, smallInput); | |
string[] splitInput = smallInput.Split(' '); | |
if (splitInput.Length > 1) | |
{ | |
foreach (string inputPart in splitInput) | |
{ | |
score += GetScore(smallSuggestion, inputPart); | |
} | |
} | |
return score; | |
} | |
private static int GetScore(string smallSuggestion, string smallInput) | |
{ | |
if (string.IsNullOrWhiteSpace(smallInput)) | |
{ | |
return 0; | |
} | |
int leftMatch = 5; | |
int wordMatch = 4; | |
int wordStartMatch = 3; | |
int containsMatch = 2; | |
int score = 0; | |
if (smallSuggestion.Contains(smallInput)) | |
{ | |
score += containsMatch; | |
} | |
if ($" {smallSuggestion}".Contains($" {smallInput}")) | |
{ | |
score += wordStartMatch; | |
} | |
if ($" {smallSuggestion} ".Contains($" {smallInput} ")) | |
{ | |
score += wordMatch; | |
} | |
if (smallSuggestion.StartsWith(smallInput)) | |
{ | |
score += leftMatch; | |
} | |
return score; | |
} | |
private void MarkAsSelected() | |
{ | |
Text = suggestionListBox.Text; | |
Select(0, Text.Length); | |
SelectedIndex = suggestionListBox.SelectedIndex; | |
SelectedItem = suggestionListBox.SelectedItem; | |
HideSuggBox(); | |
MadeSelection(); | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment