Skip to content

Instantly share code, notes, and snippets.

@AnshulKuthiala
Last active December 23, 2018 10:50
Show Gist options
  • Save AnshulKuthiala/14dd56477ee0ee025a2c3cbfc44ccfec to your computer and use it in GitHub Desktop.
Save AnshulKuthiala/14dd56477ee0ee025a2c3cbfc44ccfec to your computer and use it in GitHub Desktop.
[SuggestTextBox]
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