miércoles, 27 de mayo de 2015

Windows Forms. TextBox con sugerencias (III) - Personalizar las sugerencias

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;
    }
}



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();
}

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);
}

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;
}

Y ya podemos arrancar la aplicación de prueba y comprobar el resultado.

Resultado final

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.

1 comentario:

  1. 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