El control
TextBox incluido con en el Framework de .NET incorpora la opción de mostrar sugerencias mientras se escribe estableciendo la propiedad
AutoCompleteMode al valor
Suggest. Sin embargo la posibilidad de controlar los textos sugeridos es muy limitada, se limita a mostrar los textos que comienzan con el texto introducido por el usuario en el
TextBox a partir de una colección de cadenas.
Sin embargo, ¿qué pasa si el criterio que queremos utilizar para seleccionar los textos a sugerir es otro? Por ejemplo si queremos implementar un control con sugerencias al estilo de las búsquedas de Google.
En este artículo, y los siguientes, voy a mostrar cómo podemos crear un control
TextBox que nos permita elegir el algoritmo a utilizar para mostrar los textos sugeridos.
En este artículo crearé un control que reproduzca el mecanismo de sugerencias de un control
TextBox estándar, y en los siguientes mostraré cómo podemos dar al desarrollador la posibilidad de personalizar el algoritmo para seleccionar los textos sugeridos.
Para empezar voy a crear un nuevo proyecto del tipo Biblioteca de Clases llamado
SuggestionTextBox.
A continuación le he añadido referencias a los ensamblados
System.Drawing y
System.Windows.Forms que necesitaré en el proyecto.
Para introducir el código del control he eliminado la clase
Class1 que Visual Studio crea por defecto y he añadido una nueva clase
TextSuggestion que hereda de
TextBox.
Lo primero que haremos será crear dos propiedades para configurar las sugerencias:
- MaxNumOfSuggestions: que determinará el número máximo de resultados a mostrar
- SuggestDataSource: que establecerá la colección de cadenas que utilizará el control para las sugerencias
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SuggestionTextBox
{
public class TextSuggestion: TextBox
{
#region Propiedades públicas
[DefaultValue(10)]
public int MaxNumOfSuggestions { get; set; }
public IEnumerable<string> SuggestDataSource { get; set; }
#endregion
}
}
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Drawing
Public Class TextSuggestion
Inherits TextBox
#Region "Propiedades públicas"
<DefaultValue(10)> _
Public Property MaxNumOfSuggestions As Integer
Public Property SuggestDataSource As IEnumerable(Of String)
#End Region
End Class
Para mostrar los textos sugeridos voy a utilizar un control
ListBox. Así que voy a crear dos variables privadas, una para contener el control
ListBox y otra que indicará si el control ya ha sido inicializado.
Además crearé 3 métodos para controlar el funcionamiento del
ListBox:
- ShowListBox: que se encargará de inicializar el control ListBox si todavía no lo ha sido y lo muestra.
- HideListBox: que se encargará de ocultar el control ListBox.
- UpdateListBox: que actualizará el contenido del ListBox a partir del texto introducido en el TextBox, seleccionando las cadenas de SuggestDataSource que comiencen con el texto introducido.
Por último voy a crear un controlador para el evento Click del ListBox que se encargará de establecer el texto del TextBox con el contenido del elemento seleccionado y ocultará el control ListBox.
El constructor del control se encargará de inicializar la propiedad MaxNumOfSuggestions a su valor por defecto, iniciarlizar la variable _listBox y de asociar el controlador del evento Click al control ListBox.
#region Variables privadas
private ListBox _listBox;
private bool _listBoxAddedToForm = false;
#endregion
#region Constructor
public TextSuggestion(): base()
{
MaxNumOfSuggestions = 10;
_listBox = new ListBox();
_listBox.Click += listBox_Click;
}
#endregion
#region ListBox
private void ShowListBox()
{
if (!_listBoxAddedToForm)
{
this.TopLevelControl.Controls.Add(_listBox);
Point controlLocation = this.TopLevelControl.PointToClient(this.Parent.PointToScreen(this.Location));
_listBox.Left = controlLocation.X;
_listBox.Top = controlLocation.Y + this.Height;
_listBox.Font = this.Font;
_listBox.Width = this.Width;
_listBox.MinimumSize = new Size(this.Width, _listBox.MinimumSize.Height);
_listBox.Height = _listBox.ItemHeight * (MaxNumOfSuggestions + 1);
_listBoxAddedToForm = true;
}
_listBox.Visible = true;
_listBox.BringToFront();
}
private void HideListBox() { _listBox.Visible = false; }
private void UpdateListBox()
{
if (SuggestDataSource != null && !string.IsNullOrEmpty(this.Text))
{
IEnumerable<string> result = SuggestDataSource
.Where(s => s.StartsWith(this.Text, StringComparison.OrdinalIgnoreCase)
&& !s.Equals(this.Text, StringComparison.OrdinalIgnoreCase))
.OrderBy(s => s)
.Take(MaxNumOfSuggestions);
if (result.Count() > 0)
{
_listBox.DataSource = result.ToList();
ShowListBox();
}
else
HideListBox();
}
else
HideListBox();
}
private void listBox_Click(object sender, EventArgs e)
{
if (_listBox.SelectedIndex >= 0)
Text = _listBox.SelectedItem.ToString();
HideListBox();
}
#endregion
#Region "Variables privadas"
Private _listBox As ListBox
Private _listBoxAddedToForm As Boolean = False
#End Region
#Region "Constructor"
Public Sub New()
MaxNumOfSuggestions = 10
_listBox = New ListBox()
AddHandler _listBox.Click, AddressOf listBox_Click
End Sub
#End Region
#Region "ListBox"
Private Sub ShowListBox()
If Not _listBoxAddedToForm Then
Me.TopLevelControl.Controls.Add(_listBox)
Dim controlLocation As Point _
= Me.TopLevelControl.PointToClient(Me.Parent.PointToScreen(Me.Location))
_listBox.Left = controlLocation.X
_listBox.Top = controlLocation.Y + Me.Height
_listBox.Font = Me.Font
_listBox.Width = Me.Width
_listBox.MinimumSize = New Size(Me.Width, _listBox.MinimumSize.Height)
_listBox.Height = _listBox.ItemHeight * (MaxNumOfSuggestions + 1)
_listBoxAddedToForm = True
End If
_listBox.Visible = True
_listBox.BringToFront()
End Sub
Private Sub HideListBox()
_listBox.Visible = False
End Sub
Private Sub UpdateListBox()
If SuggestDataSource IsNot Nothing AndAlso Not String.IsNullOrEmpty(Me.Text) Then
Dim result As IEnumerable(Of String) = SuggestDataSource _
.Where(Function(s) s.StartsWith(Me.Text, StringComparison.OrdinalIgnoreCase) _
AndAlso Not s.Equals(Me.Text, StringComparison.OrdinalIgnoreCase)) _
.OrderBy(Function(s) s) _
.Take(MaxNumOfSuggestions)
If result.Count() > 0 Then
_listBox.DataSource = result.ToList()
ShowListBox()
Else
HideListBox()
End If
Else
HideListBox()
End If
End Sub
Private Sub listBox_Click(sender As Object, e As EventArgs)
If _listBox.SelectedIndex >= 0 Then
Text = _listBox.SelectedItem.ToString()
End If
HideListBox()
End Sub
#End Region
La parte más interesante de este código se encuentra en la inicialización del control
ListBox. Lo que hago es añadir el
ListBox al control especificado por la propiedad
TopLevelControl (generalmente el formulario), de esta forma al mostrarse el
ListBox no se verá limitado por el control contenedor del
TextBox. A continuación establezco la posición del
ListBox en la esquina inferior izquierda del
TextBox y su ancho igual al ancho del
TextBox. Cuando se muestra el
ListBox llamo al método
BringToFront para que se muestre por encima del resto de controles.
A continuación voy a añadir dos controladores para los eventos de teclado:
- En el evento KeyDown controlo la pulsación de las teclas "Arriba" y "Abajo" para modificar el elemento seleccionado en el ListBox. También controla la pulsación de la tecla "Enter" para que, si existe un elemento del ListBox seleccionado lo establezca como texto del control.
- En el evento KeyUp control si se ha modificado el texto y, si es así, actualizo los textos sugeridos a través del método UpdateListBox.
#region Entrada de teclado
private void this_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Down:
{
if ((_listBox.Visible) && (_listBox.SelectedIndex < _listBox.Items.Count - 1))
_listBox.SelectedIndex++;
e.SuppressKeyPress = true;
break;
}
case Keys.Up:
{
if (_listBox.Visible && _listBox.SelectedIndex >= 0)
_listBox.SelectedIndex--;
e.SuppressKeyPress = true;
break;
}
case Keys.Enter:
{
if (_listBox.Visible)
{
if (_listBox.SelectedIndex >= 0)
{
Text = _listBox.SelectedItem.ToString();
SelectAll();
}
HideListBox();
e.SuppressKeyPress = true;
}
break;
}
}
}
string _lastText;
private void this_KeyUp(object sender, KeyEventArgs e)
{
if (this.Text != _lastText)
{
UpdateListBox();
_lastText = this.Text;
}
}
#endregion
#Region "Entrada de teclado"
Private Sub this_KeyDown(sender As Object, e As KeyEventArgs)
Select Case e.KeyCode
Case Keys.Down
If _listBox.Visible AndAlso _listBox.SelectedIndex < _listBox.Items.Count - 1 Then
_listBox.SelectedIndex += 1
End If
e.SuppressKeyPress = True
Case Keys.Up
If _listBox.Visible AndAlso _listBox.SelectedIndex >= 0 Then
_listBox.SelectedIndex -= 1
End If
e.SuppressKeyPress = True
Case Keys.Enter
If _listBox.Visible Then
If _listBox.SelectedIndex >= 0 Then
Text = _listBox.SelectedItem.ToString()
SelectAll()
End If
HideListBox()
e.SuppressKeyPress = True
End If
End Select
End Sub
Dim _lastText As String
Private Sub this_KeyUp(sender As Object, e As KeyEventArgs)
If Me.Text <> _lastText Then
UpdateListBox()
_lastText = Me.Text
End If
End Sub
#End Region
Por último voy a controlar el evento
LostFocus para que no se dispare cada vez que el foco pase al control
ListBox, y para ocultar el
ListBox cuando efectivamente se produzca. También modificaré el constructor para asociar los controladores de eventos recién creados.
#region Constructor
public TextSuggestion(): base()
{
MaxNumOfSuggestions = 10;
_listBox = new ListBox();
KeyDown += this_KeyDown;
KeyUp += this_KeyUp;
LostFocus += this_LostFocus;
_listBox.Click += listBox_Click;
}
#endregion
#region LostFocus
protected override void OnLostFocus(EventArgs e)
{
if (!_listBox.ContainsFocus)
base.OnLostFocus(e);
}
private void this_LostFocus(object sender, EventArgs e)
{
HideListBox();
}
#endregion
#Region "Constructor"
Public Sub New()
MaxNumOfSuggestions = 10
_listBox = New ListBox()
AddHandler KeyDown, AddressOf this_KeyDown
AddHandler KeyUp, AddressOf this_KeyUp
AddHandler LostFocus, AddressOf this_LostFocus
AddHandler _listBox.Click, AddressOf listBox_Click
End Sub
#End Region
#Region "LostFocus"
Protected Overrides Sub OnLostFocus(e As EventArgs)
If Not _listBox.ContainsFocus Then
MyBase.OnLostFocus(e)
End If
End Sub
Private Sub this_LostFocus(sender As Object, e As EventArgs)
HideListBox()
End Sub
#End Region
Ya tenemos la funcionalidad básica del control. En las próximas entradas crearemos un proyecto sobre el que poder probar el control y le añadiremos la posibilidad de poder personalizar el método de selección de sugerencias:
El código completo de este artículo y los dos siguientes, tanto en C# como en Visual Basic, está disponible en:
TextBox con sugerencias tipo Google. Ejemplos MSDN.
Asier, muchas gracias por tu aporte.
ResponderEliminarTengo una pregunta: ¿cómo puedo extraer el id del registro seleccionado?
Juan Carlos
Me imagino que ya paso mucho tiempo, pero en cuanto lo solucione yo, que tambien estoy trabajando en eso, publicare la respuesta
Eliminar