Search code examples
multithreadingwinformsbackgroundworkertabpage

How can I load data for multiple TabPages concurrently in a WinForms application


I have a Windows Forms application with a TabControl containing multiple TabPages. Each TabPage contains many controls whose data is read from a database via a web api. I want to be able to display at least one TabPage, with it's data, while the remaining TabPages are loading. I know I can't load a control in a thread, so I need a way to do the data retrieval in the background, concurrently, for each of the TabPages and then use the data retrieved to fill in the controls on each TabPage. So my question is: what would be the ideal method for doing this? I want to prioritize performance. I also want to have some visual indication on each TabPage header that the page is still loading. Thoughts? Ideas? Direct answers?


Solution

  • You can load data in parallel threads and just after loading data completed, then update UI (legally) by calling Invoke method to prevent cross-thread exception. You can also load data using tasks in parallel, and update data when each task is completed.

    In the following examples, the data is loading in parallel threads and as soon as each thread/task is done, the UI will be updated.

    Example 1 - Using Task to load data into UI in parallel

    private NorthwindClient client = new NorthwindClient();
    private void Form1_Load(object sender, EventArgs e)
    {
        LoadCategories();
        LoadProducts();
    }
    private void LoadProducts()
    {
        productsTabPage.Text = "Loading ...";
        var products = client.GetProductsAsync().GetAwaiter();
        products.OnCompleted(() =>
        {
            productsDataGridView.DataSource = products.GetResult();
            productsTabPage.Text = "Products";
        });
    }
    private void LoadCategories()
    {
        categoriesTabPage.Text = "Loading ...";
        var categories = client.GetCategoriesAsync().GetAwaiter();
        categories.OnCompleted(() =>
        {
            categoriesDataGridView.DataSource = categories.GetResult();
            categoriesTabPage.Text = "Categories";
        });
    }
    

    Example 2 - Using Thread to load data into UI in parallel

    private NorthwindClient client = new NorthwindClient();
    private void Form1_Load(object sender, EventArgs e)
    {
        LoadCategories();
        LoadProducts();
    }
    private void LoadProducts()
    {
        productsTabPage.Text = "Loading ...";
        new Thread(() =>
        {
            var products = client.GetProducts();
            Invoke(new Action(() =>
            {
                productsDataGridView.DataSource = products;
                productsTabPage.Text = "Products";
            }));
        }).Start();
    }
    private void LoadCategories()
    {
        categoriesTabPage.Text = "Loading ...";
        new Thread(() =>
        {
            var categories = client.GetCategories();
            Invoke(new Action(() =>
            {
                categoriesDataGridView.DataSource = categories;
                categoriesTabPage.Text = "Categories";
            }));
        }).Start();
    }
    

    NorthwindClient - Common code for both examples

    Both above examples rely on the following code to load data:

    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Threading.Tasks;
    public class NorthwindClient
    {
        static HttpClient client;
        static NorthwindClient()
        {
            client = new HttpClient();
            client.BaseAddress = new Uri("https://northwind.vercel.app/api/");
        }
        public async Task<IEnumerable<Category>> GetCategoriesAsync()
        {
            var response = await client.GetStringAsync("categories");
            return JsonConvert.DeserializeObject<IEnumerable<Category>>(response);
        }
        public async Task<IEnumerable<Product>> GetProductsAsync()
        {
            var response = await client.GetStringAsync("products");
            return JsonConvert.DeserializeObject<IEnumerable<Product>>(response);
        }
        public IEnumerable<Category> GetCategories()
        {
            var response = client.GetStringAsync("categories").Result;
            return JsonConvert.DeserializeObject<IEnumerable<Category>>(response);
        }
        public IEnumerable<Product> GetProducts()
        {
            var response = client.GetStringAsync("products").Result;
            return JsonConvert.DeserializeObject<IEnumerable<Product>>(response);
        }
    }
    
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
    }
    public class Product
    {
        public int Id { get; set; }
        public int SupplierId { get; set; }
        public int categoryId { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal UnitPrice { get; set; }
        public int UnitsInStock { get; set; }
        public int UnitsOnOrder { get; set; }
        public int ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
        public string Name { get; set; }
    }