LINQ a hledání v DataGridView

Petr Wiedemann, 22. červenec 2009

DataGridView je třída pro zobrazení tabulkových dat. Samotná data můžou být přímo součástí tabulky v kolekcích Rows a Columns nebo mohou pocházet z jiného zdroje navázaného přes DataSource. V našem případě si ukážeme hledání dat z prvního případu, kdy jako motor hledání využijeme LINQ. Vytvoříme si malou aplikaci, která bude obsahovat tabulku DataGridView a jedno tlačítko. Po stisku tlačítka zobrazíme okno, kam může uživatel zadat hledaný text. Zároveň obsahuje volbu, jestli při hledání rozlišovat velikosti písmen.

search_demo.png

Naše tabulka bude mít 3 sloupce pro uložení kódu produktu, jeho názvu a ceny. Nejprve tedy tabulku naplníme daty v konstruktoru třídy.

// Vychozi data.
dataGridView1.Rows.Add(50);
for (int Row = 0; Row < 50; Row++)
{
    dataGridView1.Rows[Row].Cells["KodProduktu"].Value = "PN" + (Row + 1).ToString();
    dataGridView1.Rows[Row].Cells["NazevProduktu"].Value = "Bezva produkt číslo " + (Row + 1).ToString();
    dataGridView1.Rows[Row].Cells["Cena"].Value = Math.Pow(Row + 2, 2);
}

Editace dat v tabulce

V naší ukázkové aplikaci je v DataGridView povolena editace hodnot. Data ve sloupci Cena jsou uloženy jako Double. Proto musíme ošetřit hodnoty zadané uživatelem do tohoto sloupce. Pro kontrolu hodnot zachytíme událost CellParsing, ve které se zadanou hodnotu pokusíme převést na Double. V případě, že převod na Double neuspěje, uložíme do editované buňky nulu.

/// <summary>Prevede hodnotu zadanou ve sloupci Cena na typ
/// Double, ktera bude ulozena v DataGridViewCell.Value.
/// </summary>
private void dataGridView1_CellParsing(object sender, DataGridViewCellParsingEventArgs e)
{
    if (e.ColumnIndex > -1 && e.RowIndex > -1 && e.Value != null)
    {
        // Jmeno sloupce, ve kterem je editovana bunka.
        string columnName = dataGridView1.Columns[e.ColumnIndex].Name;

        if (columnName == "Cena")
        {
            try
            {
                // Prevedeni zadaneho textu na Double.
                string number = e.Value.ToString();
                number = number.Replace(',', '.');
                e.Value = Double.Parse(number);
                e.ParsingApplied = true;
            }
            catch (FormatException)
            {
                // Konverze selhala, ulozi se 0.
                e.Value = 0.0;
                e.ParsingApplied = true;
            }
        }
    }
}

Řazení dat ve sloupci

V našem případě mají sloupce v DataGridView vlastnost SortMode rovnu hodnotě Automatic. DataGridView umožňuje řadit zobrazené hodnoty po kliknutí na název sloupce. Protože s hodnotami ve sloupci Cena pracujeme jako s číslem, musíme zachytit událost SortCompare a porovnávat hodnoty ve sloupci Cena jako Double.

/// <summary>Porovna data ze sloupce Cena jako cislo.
/// Ostatni sloupce se porovnavaji jako text.
/// </summary>
private void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e)
{
    if (e.Column.Name == "Cena")
    {
        double d1, d2;
        if (!Double.TryParse(e.CellValue1 == null ? "0" : e.CellValue1.ToString(), out d1))
            d1 = 0.0;
        if (!Double.TryParse(e.CellValue2 == null ? "0" : e.CellValue2.ToString(), out d2))
            d2 = 0.0;

        if (d1 > d2)
            e.SortResult = 1;
        else if (d1 < d2)
            e.SortResult = -1;
        else
            e.SortResult = 0;

        e.Handled = true;
    }
}

Zachycení stisku kláves Ctrl + F

Protože chceme uživateli nabídnout i trochu rychlejší cestu ke zobrazení okna pro zadání hledaného textu, zachytíme stisk kláves Control + F.

/// <summary>Zachyti stisk klaves Ctrl+F a zobrazi okno pro hledani.
/// </summary>
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == (Keys.Control | Keys.F))
    {
        ShowSearchTextForm();
        return true;
    }

    return base.ProcessCmdKey(ref msg, keyData);
}

Implementace okna pro zadání hledaného textu

Naše okno pro zadání hledaného textu obsahuje jen 1 TextBox, 1 CheckBox a 1 tlačítko.

search_form.png

Součástí naší mini třídy FormSearchText je delegát, který zavoláme při stisku tlačítka Hledat. Při uzavření okna pouze odebereme instanci třídy FormSearchText z pole vlastněných oken rodiče. Pro uzavření okna můžeme stisknout klávesu Escape, kterou zachytíme opět ve funkci ProcessCmdKey. Stisk tlačítka Hledat pouze ověří, jestli je vyplněn text k hledání a pokud ano, zavolá kód spojený s delegátem delTextSearch.

public partial class FormSearchText : Form
{
    /// <summary>Delegat volany pri stisku tlacitka Hledat.
    /// </summary>
    public Action<string, bool> delTextSearch;

    public FormSearchText()
    {
        InitializeComponent();
    }

    /// <summary>Pred zavrenim okna odebere nasi instanci z pole
    /// vlastnenych oken rodice.
    /// </summary>
    private void FormSearchText_FormClosing(object sender, FormClosingEventArgs e)
    {
        Owner.RemoveOwnedForm(this);
    }

    /// <summary>Zachyti stisk klavesy Escape a zavola funkci Close().
    /// </summary>
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == Keys.Escape)
        {
            Close();
            return true;
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }

    /// <summary>Pri stisku tlacitka Hledat zavola delegata pro hledani
    /// dat v DataGridView.
    /// </summary>
    private void buttonSearch_Click(object sender, EventArgs e)
    {
        // Uzivatel musi vyplnit hedany text.
        if (textBoxSearch.Text.Trim().Length > 0)
        {
            delTextSearch(textBoxSearch.Text, checkBoxCaseSensitive.Checked);
        }
    }
}

Zobrazení okna pro zadání textu a nastavení delegátů

Když máme hotovou třídu, která zobrazí pole pro zadání textu, můžeme se vrátit k naší hlavní třídě a zpracovat požadavek na hledání v DataGridView. Po stisku kláves Ctrl + F nebo stisku tlačítka Hledat voláme funkci ShowSearchTextForm, která přidá třídu FormSearchText mezi námi vlastněná okna a tak si zajistíme, že bude okno pro hledání vždy zobrazeno nad oknem s DataGridView. Pokud by již okno bylo mezi námi vlastněnými, pouze ho zobrazíme a předáme mu fokus.

// Projde okna ktera vlastnime
// a pokud je mezi nimi trida FormSearchText
// pouze ji zobrazi a nastavi focus.
foreach (Form ownedForm in OwnedForms)
{
    if (ownedForm is FormSearchText)
    {
        ownedForm.Show();
        ownedForm.Focus();
        return;
    }
}

V případě, že předchozí kontrola neuspěje, vytvoříme novou instanci třídy FormSearchText, přidáme jí mezi námi vlastněná okna a inicializujeme delegáta delTextSearch. Celý kód přiřazený tomuto delegátovi bude spuštěn po stisku tlačítka Hledat ve třídě FormSearchText. Po inicializaci delegáta zobrazíme novou instanci FormSearchText.

Protože je celý kód trochu delší, nejprve si vysvětlíme jeho části. Jako první věc, kterou zkontrolujeme je příznak, jestli máme při hledání textu rozlišovat malá a velká písmena. Pokud tomu tak není, převedeme hledaný text na malá písmena.

Další část kódu obsahuje definici nového delegáta CellValueMatch, který bude volán pro kontrolu textu v každé buňce v DataGridView. Tento kód kontroluje, jestli hodnota v textových buňkách obsahuje námi hledaný text. Hodnoty ve sloupci Cena naopak převádí na Double a porovnává s hledanou hodnotou převedenou také na Double. CellValueMatch vrací true, pokud najde v buňce hledaný text nebo pokud jsou číselné hodnoty shodné. V opačném případě vrací false. CellValueMatch je následně použitý v LINQ podmínce.

Nakonec se dostáváme k celému motoru hledání, kterým je v našem případě LINQ. Poslední část delegáta tedy zavolá dotaz, který najde 1. buňku v DataGridView, která obsahuje hledaný text. Celý dotaz nejprve převede kolekci Rows na IEnumerable typu DataGridViewRow. Z tohoto seznamu odfiltruje řádek pro zadání nové hodnoty a buňky ve všech ostatních řádcích opět převede na IEnumerable typu DataGridViewCell. Hodnotu každé buňky která není null potom porovná delegátem CellValueMatch. Celý dotaz vrátí 1. buňku, pro kterou CellValueMatch vrátí true.

V případě, že LINQ nic nenajde, vyhodí volání dotazu vyjímku InvalidOperationException, ve které zobrazíme informaci o tom, že jsme nic nenalezli.

/// <summary>Zkontroluje, jestli jiz neni okno pro hledani zobrazeno
/// a pokud neni, tak ho vytvori a nastavi delegaty pro hledani
/// v DataGridView.
/// </summary>
private void ShowSearchTextForm()
{
    // Projde okna ktera vlastnime
    // a pokud je mezi nimi trida FormSearchText
    // pouze ji zobrazi a nastavi focus.
    foreach (Form ownedForm in OwnedForms)
    {
        if (ownedForm is FormSearchText)
        {
            ownedForm.Show();
            ownedForm.Focus();
            return;
        }
    }

    // Vytvoreni nove instance FormSearchText a nastaveni
    // metod, pro hledani v datech.
    FormSearchText searchForm = new FormSearchText();

    // Prida novou tridu do seznamu nami vlastnenych oken.
    AddOwnedForm(searchForm);
    searchForm.Owner = this;

    // Delegat spojeny s nasim novym oknem pro hledani.
    // Tento kod je volany pri stisku tlacitka Hledat
    // ve tride FormSearchText.
    searchForm.delTextSearch = (searchText, caseSensitive) =>
    {
        // Pokud nerozlisujeme velikost pismen, prevedeme hledany
        // text na mala pismena.
        if (!caseSensitive)
            searchText = searchText.ToLower();

        // Porovani obsahu bunky s hledanym textem. Data ve sloupci Cena
        // se prevadi na Double a porovnavaji se s hledanou hodnotou,
        // ktera je take prevedena na Double.
        // Tento kod je volany pro kazdou DataGridViewCell
        // z nasledujiciho dotazu.
        Func<DataGridViewCell, bool, string, bool> CellValueMatch = (cell, cSensitive, sText) =>
        {
            if (cell.OwningColumn.Name == "Cena")
            {
                try
                {
                    if (Convert.ToDouble(sText) == Convert.ToDouble(cell.Value))
                        return true;
                    else
                        return false;
                }
                catch (FormatException)
                {
                    return false;
                }
            }
            else
            {
                string cellValue = cell.Value.ToString();
                if (!cSensitive)
                    cellValue = cellValue.ToLower();

                if (cellValue.IndexOf(sText) > -1)
                    return true;
                else
                    return false;
            }
        };

        // Dotaz na data v DataGridView. Prohleda kazdou bunku v tabulce
        // krome radku pro novy zaznam a vrati 1. DataGridViewCell,
        // ktera obsahuje hledany text.
        // Pokud hledani nic nenajde, vyhodi vyjimku InvalidOperationException.
        try
        {
            dataGridView1.CurrentCell = (from dgRow in dataGridView1.Rows.Cast<DataGridViewRow>()
                                         where !dgRow.IsNewRow
                                         from dgCell in dgRow.Cells.Cast<DataGridViewCell>()
                                         where dgCell.Value != null
                                         && CellValueMatch(dgCell, caseSensitive, searchText)
                                         select dgCell).First();
        }
        catch (InvalidOperationException)
        {
            MessageBox.Show("Hledaný text nebyl nalezen.", Text);
        }
    };

    // Zobrazeni okna pro hledani.
    searchForm.Show();
}