Jak asynchronním způsobem naplnit DataTable

22. května 2007

O autorovi

Portrét

Jan Aubrecht
.NET vývojář a konzultant IS/IT
honza@intellisoft.cz

Honza se zabývá vývojem webových aplikací od roku 2000. Rád používá ASP.NET 2.0 a je přímo posedlý neustálým zlepšováním svých aplikací.

Ve volném čase je jeho vášní dobré jídlo a pití. Nejraději se baví přípravou středomořských specialit a báječnými víny z Francie.

Soubory ke stažení

Zdrojové kódy použité v článku a další související soubory si můžete stáhnout zde:

Další články

Článek reaguje na dotaz ze serveru forum.builder.cz a pokouší se nalézt způsob, jakým je možné naplnit DataTable daty z databáze, tak aby celá operace probíhala na pozadí a uživatel měl možnost ji kdykoliv přerušit.

Co je naším cílem?

Jistě znáte nějakou databázovou aplikaci, která získává data z databáze na pozadí a zobrazuje je uživateli postupně. Díky tomu je pak možné s aplikací pohodlně pracovat, i když zrovna nahrává data nebo je připravuje k zobrazení. Stejně tak je uživateli umožněno, aby si mohl např. kdykoliv změnit podmínky pro výběr dat, aniž by musel čekat, až se z databáze dotáhnou všechna data, která odpovídala jeho předchozímu zadání.

Jak to má fungovat?

Pojďme se podívat, co všechno by měla taková aplikace splňovat, jak z pohledu uživatele, tak i z hlediska její architektury.

  • Data by mělo nahrávat samostatné vlákno aplikace.
  • Proces nahrávání dat musí jít kdykoliv přerušit.
  • Získaná data musí být uloženy v DataTable.
  • Tabulka s daty by měla být zobrazena v UI vlákně aplikace.

Vytváříme nové vlákno

Nejdříve si ukážeme, jak vytvořit nové vlákno, ve kterém budeme získávat data z databáze.

Začneme deklarací členských proměnných, které použijeme pro komunikaci mezi aplikací a vláknem pro nahrávání dat. Zároveň si deklarujeme i proměnou určenou pro synchronizaci, tak aby předávaná data mezi novým vláknem a aplikací byly konzistentní.

private bool _aborted;
private bool _running;
private readonly object _syncLock = new object();

Nyní si můžeme vytvořit proceduru, která vytvoří nové vlákno a předá mu SQL dotaz, který má vlákno vykonat. V proceduře také nastavíme proměnné, které indikují aktuální stav zpracování.

public void StartWorker(string query)
{
    if (query == null)
        throw new ArgumentNullException("query");
 
    lock (_syncLock)
    {
        if (_running)
            return;
 
        _aborted = false;
        _running = true;
    }
 
    Thread thread = new Thread(new ParameterizedThreadStart(this.DatabaseWorker));
 
    thread.IsBackground = true;
    thread.Start(query);
}

Nahráváme data

Uvnitř procedury spuštěné prostřednictvím nově vytvořeného vlákna se nejdříve připojíme k databázi pomocí třídy SqlConnection. Po navázání spojení vytvoříme objekt SqlCommand, kterému předáme SQL dotaz předaný do vlákna při jeho startu (pomocí parametru v Thread.Start).

Pak nám již nic nebrání vytvořit novou DataTable. Do té pak pomocí třídy SqlDataAdapter nahrajeme schéma, které odpovídá struktuře dat nahrávaných z databáze.

Poté se můžeme pustit do nahrávání dat do DataTable. Data budeme nahrávat po jednotlivých řádcích pomocí třídy SqlDataReader. Na konci každého cyklu zkontrolujeme, zda-li není požadováno ukončit nahrávání. Pokud je, přerušíme provádění SQL dotazu pomocí metody Cancel objektu SqlCommand.

Nakonec potvrdíme změny provedené v tabulce a můžeme začít se zobrazením tabulky uživateli.

Kompletní kód procedury pro nahrání dat vypadá takto:

private void DatabaseWorker(object state)
{
    try
    {
        //Create a new connection.
        using (SqlConnection connection = new SqlConnection(ConnectionString))
        {
            string query = (string) state;
 
            connection.Open();
 
            using (SqlCommand command = new SqlCommand(query, connection))
            {
                DataTable table = new DataTable();
 
                //Append the schema.
                using (SqlDataAdapter adapter = new SqlDataAdapter(command))
                {
                    adapter.FillSchema(table, SchemaType.Mapped);
                }
 
                //Append table rows.
                using (SqlDataReader reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        //Get the row values.
                        object[] values = new object[reader.FieldCount];
 
                        //Append a single row.
                        if (reader.GetValues(values) > 0)
                            table.Rows.Add(values);
 
                        //Abort processing if needed.
                        if (this.IsAborted)
                        {
                            command.Cancel();
                            break;
                        }
                    }
                }
 
                if (!this.IsAborted)
                    table.AcceptChanges();
                else
                    table.RejectChanges();
 
                //Display table in the DataGridView.
                this.DisplayResults(table);
            }
        }
    }
    finally
    {
        lock (_syncLock)
        {
            _running = false;
        }
    }
}

Zobrazujeme data

Protože jsme tabulku vytvořili ve svém vlastním vlákně, tak se musíme postarat, aby došlo k jejímu zobrazení prostřednictvím vlákna, ve kterém běží user interface naší aplikace. Zobrazení tedy zařídíme pomocí metody Invoke ovládacího prvku, který chceme použít pro zobrazení dat.

Procedura použitá pro zobrazení dat vypadá následovně:

private void DisplayResults(DataTable table)
{
    //Check if the caller thread is different from the UI thread.
    if (this.dataGridView1.InvokeRequired)
    {
        //Invoke method on the UI thread.
        this.dataGridView1.Invoke(new DisplayCallback(this.DisplayResults), table);
    }
    else
    {
        //Display the table data.
        this.dataGridView1.DataSource = table;
 
        //Free previously used memory.
        GC.Collect();
    }
}

Pár slov závěrem

Jak sami vidíte, tak získání dat asynchronním způsobem není zrovna nejjednodušší. Naše aplikace musí používat zamykání pro synchronizaci dat mezi vlákny a i plnění DataTable probíhá složitějším způsobem. Odměnou nám je ale aplikace, kterou může uživatel používat, i když aplikace zrovna pracuje s databází. Navíc má uživatel možnost, nahrávání dat kdykoliv přerušit.

Kompletní zdrojové kódy příkladu si můžete stáhnout zde.