Este artículo es continuación de:
Windows Forms. TextBox con sugerencias (I) - Creando el control
Windows Forms. TextBox con sugerencias (II) - Creando el proyecto de pruebas
En los artículos anteriores he creado un control que emula la funcionalidad de mostrar sugerencias del
TextBox estándar del Framework de .NET.
En este artículo mostraré cómo podemos modificar el control para que acepte como origen de datos para las sugerencias objetos de cualquier tipo y que el desarrollador pueda personalizar tanto el texto que se genera como sugerencia como el algoritmo para seleccionar las sugerencias a mostrar a partir del texto introducido por el usuario.
Lo primero que voy a hacer es definir las nuevas propiedades necesarias. Voy a modificar la propiedad
SuggestDataSource para que acepte cualquier tipo de objeto como elementos del origen de datos, no sólo
strings. Y definiré dos nuevas propiedades públicas:
- MatchElement: permite proporcionar una función que recibe como parámetros un elemento de SuggestDataSource y una cadena con el texto introducido por el usuario. La función devuelve un valor true o false indicando si el elemento representa una sugerencia válida para el texto introducido.
- TextFromElement: permite proporcionar una función que recibe como parámetro un elemento de SuggestDataSource y devuelve un texto que representa el elemento para mostrar como sugerencia.
Ambas funciones las almaceno en variables privadas de forma que, en caso de no haberse establecido ninguna función, las propiedades devuelven una función por defecto que establece la funcionalidad que ya teníamos implementada (un elemento es sugerencia de un texto si el texto del elemento empieza con la cadena introducida por el usuario y el texto del elemento es el propio elemento (si es un string) o el valor devuelto por el método ToString.
private Func<object, string, bool> _matchElement;
private Func<object, string> _textFromElement;
.....
public IEnumerable<object> SuggestDataSource { get; set; }
public Func<object, string ,bool> MatchElement
{
get
{
if (_matchElement == null)
{
if (SuggestDataSource.GetType().GetGenericArguments()[0].IsAssignableFrom(typeof(string)))
return delegate(object element, string text) {
return ((string)element).StartsWith(text, StringComparison.OrdinalIgnoreCase);
};
else
return delegate(object element, string text) {
return element.ToString().StartsWith(text, StringComparison.OrdinalIgnoreCase);
};
}
else
return _matchElement;
}
set
{
_matchElement = value;
}
}
public Func<object, string> TextFromElement
{
get
{
if (_textFromElement == null)
{
if (SuggestDataSource.GetType().GetGenericArguments()[0].IsAssignableFrom(typeof(string)))
return delegate(object element) { return (string)element; };
else
return delegate(object element) { return element.ToString(); };
}
else
return _textFromElement;
}
set
{
_textFromElement = value;
}
}
Private _matchElement As Func(Of Object, String, Boolean)
Private _textFromElement As Func(Of Object, String)
.....
Public Property SuggestDataSource As IEnumerable(Of Object)
Public Property MatchElement() As Func(Of Object, String, Boolean)
Get
If _matchElement Is Nothing Then
If SuggestDataSource.GetType().GetGenericArguments()(0).IsAssignableFrom(GetType(String)) Then
Return Function(element As Object, text As String)
Return CType(element, String).StartsWith(text, StringComparison.OrdinalIgnoreCase)
End Function
Else
Return Function(element As Object, text As String)
Return element.ToString().StartsWith(text, StringComparison.OrdinalIgnoreCase)
End Function
End If
Else
Return _matchElement
End If
End Get
Set(ByVal value As Func(Of Object, String, Boolean))
_matchElement = value
End Set
End Property
Public Property TextFromElement() As Func(Of Object, String)
Get
If _textFromElement Is Nothing Then
If SuggestDataSource.GetType().GetGenericArguments()(0).IsAssignableFrom(GetType(String)) Then
Return Function(element As Object)
Return CType(element, String)
End Function
Else
Return Function(element As Object)
Return element.ToString()
End Function
End If
Else
Return _textFromElement
End If
End Get
Set(ByVal value As Func(Of Object, String))
_textFromElement = value
End Set
End Property
A continuación simplemente tendremos que cambiar el método
UpdateList para que haga el filtrado de resultados utilizando la función
MatchElement y muestre el texto a partir de la función
TextFromElement.
private void UpdateListBox()
{
if (SuggestDataSource != null && !string.IsNullOrEmpty(this.Text))
{
IEnumerable<string> result = SuggestDataSource
.Where(item => MatchElement(item, this.Text)
&& !TextFromElement(item).Equals(this.Text, StringComparison.OrdinalIgnoreCase))
.Select(item => TextFromElement(item))
.OrderBy(s => s)
.Take(MaxNumOfSuggestions);
if (result.Count() > 0)
{
_listBox.DataSource = result.ToList();
ShowListBox();
}
else
HideListBox();
}
else
HideListBox();
}
Private Sub UpdateListBox()
If SuggestDataSource IsNot Nothing AndAlso Not String.IsNullOrEmpty(Me.Text) Then
Dim result As IEnumerable(Of String) = SuggestDataSource _
.Where(Function(item) MatchElement(item, Me.Text) _
AndAlso Not TextFromElement(item).Equals(Me.Text, StringComparison.OrdinalIgnoreCase)) _
.Select(Function(item) TextFromElement(item)) _
.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 UpdateListBox()
If SuggestDataSource IsNot Nothing AndAlso Not String.IsNullOrEmpty(Me.Text) Then
Dim result As IEnumerable(Of String) = SuggestDataSource _
.Where(Function(item) MatchElement(item, Me.Text) _
AndAlso Not TextFromElement(item).Equals(Me.Text, StringComparison.OrdinalIgnoreCase)) _
.Select(Function(item) TextFromElement(item)) _
.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
Para probar el control voy a modificar el formulario de prueba para que, cuando el usuario teclee un texto en el control, se muestren como sugerencias los productos que contengan las palabras tecleadas bien en el nombre, bien en el campo Color; independientemente del orden en que se introduzcan las palabras. Además en el texto sugerido se mostrará el nombre del producto junto con el color entre paréntesis, si tiene.
Para ello me voy a crear dos funciones:
- SuggestionMatch: que recibe como parámetro un DataRow de la tabla Products y el texto introducido por el usuario. La función comprueba si todas las palabras introducidas por el usuario aparecen en el nombre o en el color y devuelve el resultado de la comprobación.
- TextForSuggestion: recibe como parámetro un DataRow de la tabla Products y devuelve una cadena con el nombre del producto y el color entre paréntesis, si tiene.
private bool SuggestionMatch(object row, string userText)
{
if (string.IsNullOrEmpty(userText)) return false;
bool match = true;
string[] words = userText.Split(' ');
DataRow datarow = (DataRow)row;
foreach (string word in words)
{
if (!datarow["Name"].ToString().ToUpper().Contains(word.ToUpper())
&& (datarow["Color"]==DBNull.Value
|| !datarow["Color"].ToString().ToUpper().Contains(word.ToUpper())))
{
match = false;
break;
}
}
return match;
}
private string TextForSuggestion(object row)
{
DataRow datarow = (DataRow)row;
string color = "";
if (datarow["Color"] != DBNull.Value)
color = string.Format(" ({0})", datarow["Color"].ToString());
return string.Format("{0}{1}", datarow["Name"].ToString(), color);
}
Private Function SuggestionMatch(row As Object, userText As String)
If String.IsNullOrEmpty(userText) Then Return False
Dim match As Boolean = True
Dim words As String() = userText.Split(" "c)
Dim datarow As DataRow = CType(row, DataRow)
For Each word As String In words
If Not datarow("Name").ToString().ToUpper().Contains(word.ToUpper()) _
AndAlso (datarow("Color") Is DBNull.Value _
OrElse Not datarow("Color").ToString().ToUpper().Contains(word.ToUpper())) Then
match = False
Exit For
End If
Next
Return match
End Function
Function TextForSuggestion(row As Object)
Dim datarow As DataRow = CType(row, DataRow)
Dim color As String = ""
If (datarow("Color") IsNot DBNull.Value) Then
color = String.Format(" ({0})", datarow("Color").ToString())
End If
Return String.Format("{0}{1}", datarow("Name").ToString(), color)
End Function
Ahora simplemente tendremos que asignarle como origen de datos para sugerencias a nuestro control la colección de
DataRows de la tabla
Products. He indicarle, a través de las propiedades
MatchElement y
TextFromElement que debe utilizar estas dos funciones para obtener respectivamente las sugerencias a mostrar y el texto exacto a utilizar.
private void Form1_Load(object sender, EventArgs e)
{
DataTable dtProductos = new DataTable("Products");
dtProductos.ReadXml(Path.Combine(Application.StartupPath, @"Datos\Products.xml"));
textSuggestion1.SuggestDataSource = dtProductos.Rows.Cast<DataRow>();
textSuggestion1.MatchElement = SuggestionMatch;
textSuggestion1.TextFromElement = TextForSuggestion;
}
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim dtProductos As DataTable = New DataTable("Products")
dtProductos.ReadXml(Path.Combine(Application.StartupPath, "Datos\Products.xml"))
TextSuggestion1.SuggestDataSource = dtProductos.Rows.Cast(Of DataRow)()
TextSuggestion1.MatchElement = AddressOf SuggestionMatch
TextSuggestion1.TextFromElement = AddressOf TextForSuggestion
End Sub
Y ya podemos arrancar la aplicación de prueba y comprobar el resultado.
También podríamos ir aún más allá para hacer que la función
MatchElement, en lugar de devolver un valor booleano, devuelva un valor numérico indicando el grado de coincidencia del texto con el elemento. De esta forma podríamos ordenar el resultado de las sugerencias por este valor y mostrar primero la que mayor grado de coincidencia tenga con el texto introducido.
El código completo de este artículo y los dos anteriores, tanto en C# como en Visual Basic, está disponible en:
TextBox con sugerencias tipo Google. Ejemplos MSDN.
Muchas Gracias excelente aporte me a servido demasiado para el proyecto que actualmente estoy desarrollando espero algún día poder generar aportes tan buenos como los que proporcionas.
ResponderEliminar