在 C# WindowsForms 中选择组合框的值后应该立即开始的进度条需要 20 秒才能开始

Progress bar taking 20 seconds to start when it should start right away after selecting a value of a combobox in C# WindowsForms

我的程序有问题。我的程序会让用户 select 一个下拉值,在他们 select 下拉值之后,datagridview 应该从 table 加载数据,然后进度条应该从百分比 1 开始到 100%。现在我程序中的所有内容都可以正确加载 datagridview 以及程序中的所有其他内容。进度条也可以正常工作并加载,但是当用户 selects 下拉组合框时,进度条需要 15 到 20 秒才能启动。我想要它马上。

你能看看我的代码,看看进度条没有立即开始的问题是什么吗?

如果您需要更多信息,请告诉我。

namespace DatagridViewProgressBar
{
    public partial class Form1 : Form
    {

        //datagridview, bindingsource, data_apapter global objects variables
        private DataGridView dataGridView = new DataGridView();
        private BindingSource bindingSource = new BindingSource();
        private SqlDataAdapter dataAdapter = new SqlDataAdapter();
        DataTable dt = new DataTable();


        //class objects
        Databases lemars = new Databases();
        Databases schuyler = new Databases();
        Databases detroitlakeskc = new Databases();


        public Form1()
        {
            InitializeComponent();
            // To report progress from the background worker we set this property
            dbWorker = new BackgroundWorker();
            dbWorker.DoWork += new DoWorkEventHandler(dbWorker_DoWork);
            dbWorker.ProgressChanged += new ProgressChangedEventHandler(dbWorker_ProgressChanged);
            dbWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(dbWorker_RunWorkerCompleted);
            dbWorker.WorkerReportsProgress = true;
            dbWorker.WorkerSupportsCancellation = true;

        }

        private void btn_Exit_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void comboBox_Database_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (comboBox_Database.SelectedItem.ToString() == "LeMars21St")
            {

                if (dbWorker.IsBusy != true)
                {
                    dbWorker.RunWorkerAsync();
                }            
            }
        }



        private void GetTableToDataGridView()
        {
            //prgBar_DataGridViewLoading
            DatabaseColumns Obj = new DatabaseColumns();
            String SqlcmdString = @"SELECT invoice, shipment, Project, invoiceDateTB, CreatedDate, typeName, exportedDate, statusName, total, import_status, Time_Completed, ERROR_DESCRIPTION FROM dbo.AllInvoicesInReadyStatus";
            SqlDataReader reader;
            int progress;

            using (SqlConnection conn = new SqlConnection(lemars._LeMarsConnectionString))
            {
                reader = null;
                SqlCommand Sqlcmd = new SqlCommand(SqlcmdString, conn);
                conn.Open();
                reader = Sqlcmd.ExecuteReader();
                if (reader.HasRows)
                {
                    try
                    {

                        dt.Load(reader);

                        for (int i = 0; i < dt.Rows.Count; i++)
                        {
                            Obj.Invoice = dt.Rows[i]["invoice"].ToString();
                            Obj.Shipment = dt.Rows[i]["shipment"].ToString();
                            Obj.Project = dt.Rows[i]["Project"].ToString();
                            Obj.InvoiceDateTB = Convert.ToDateTime(dt.Rows[i]["invoiceDateTB"]);
                            Obj.CreatedDate = Convert.ToDateTime(dt.Rows[i]["CreatedDate"]);
                            Obj.TypeName = dt.Rows[i]["typeName"].ToString();
                            Obj.ExportedDate = Convert.ToDateTime(dt.Rows[i]["exportedDate"]);
                            Obj.StatusName = dt.Rows[i]["statusName"].ToString();
                            Obj.Total = Convert.ToDecimal(dt.Rows[i]["total"]);
                            Obj.ImportStatus = dt.Rows[i]["import_status"].ToString();
                            if (!Convert.IsDBNull(dt.Rows[i]["Time_Completed"]))
                            {
                                Obj.TimeCompleted = Convert.ToDateTime(dt.Rows[i]["Time_Completed"]);
                            }
                            Obj.ErrorDescription = dt.Rows[i]["ERROR_DESCRIPTION"].ToString();

                            progress = i * 100 / dt.Rows.Count; 
                            dbWorker.ReportProgress(progress);
                            Thread.Sleep(500);
                        }                     
                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
        }

        private void dbWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            GetTableToDataGridView();
            dbWorker.ReportProgress(100);
        }

        private void dbWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar_GetTasks.Value = e.ProgressPercentage;
            // eg: Set your label text to the current value of the progress bar
            lbl_PercentageCount.Text = (progressBar_GetTasks.Value.ToString() + "%");         
        }

        private void dbWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {

            dataGridView_ShowAllData.DataSource = dt;

            if (e.Cancelled)
            {
                 MessageBox.Show("Process Cancelled.");
            }
            else if (e.Error != null)
            {
                MessageBox.Show("Error occurred: " + e.Error.Message);
            }
            else
            {
                MessageBox.Show("Successful Completion.");
            }

            //progressBar_GetTasks.Value = 0;
        }

        private void btn_CancelOperation_Click(object sender, EventArgs e)
        {
            if (dbWorker.IsBusy)
            {
                dbWorker.CancelAsync();
            }
        }
    }
}

我在做我的第一个 backgroundWorker 时遇到的问题是 GUI 锁定了。我犯了一个错误,试图分发通过报告进度找到的每个结果(从 2 到选定范围内的原始数)。如果我有足够长的操作(如果操作很短则不会发生)我最终会重载并通过写入操作锁定 GUI,看起来我从未真正添加过多线程。

现在我注意到这一行:

progress = i * 100 / dt.Rows.Count; 
你 运行 每次循环迭代的进度代码(和更新栏),即使百分比实际上没有改变。如果你处理 100 万次迭代,那就是 10000 次重绘而值实际上没有改变。随着命中变得越来越少,或者 GC 开始干扰任务的最佳性能,这可能会在以后变慢,因此 GUI 线程具有 "time to catch up".

在将值写入 GUI 之前,您应该检查该值是否实际发生了变化。 ProgressReporting 活动的最新消息。但也许可以在循环本身中做一些事情。

我制作了一些示例代码来展示我所说的 "GUI write overhead" 问题:

using System;
using System.Windows.Forms;

namespace UIWriteOverhead
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        int[] getNumbers(int upperLimit)
        {
            int[] ReturnValue = new int[upperLimit];

            for (int i = 0; i < ReturnValue.Length; i++)
                ReturnValue[i] = i;

            return ReturnValue;
        }

        void printWithBuffer(int[] Values)
        {
            textBox1.Text = "";
            string buffer = "";

            foreach (int Number in Values)
                buffer += Number.ToString() + Environment.NewLine;
            textBox1.Text = buffer;
        }

        void printDirectly(int[] Values){
            textBox1.Text = "";

            foreach (int Number in Values)
                textBox1.Text += Number.ToString() + Environment.NewLine;
        }

        private void btnPrintBuffer_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(10000);
            MessageBox.Show("Printing with buffer");
            printWithBuffer(temp);
            MessageBox.Show("Printing done");
        }

        private void btnPrintDirect_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(1000);
            MessageBox.Show("Printing directly");
            printDirectly(temp);
            MessageBox.Show("Printing done");
        }
    }
}

执行 dt.Load(reader); 等待数据完全加载后再继续,删除该行并将其替换为 while(reader.Read()) 循环。

    private void GetTableToDataGridView()
    {
        //prgBar_DataGridViewLoading
        DatabaseColumns Obj = new DatabaseColumns();
        String SqlcmdString = @"SELECT invoice, shipment, Project, invoiceDateTB, CreatedDate, typeName, exportedDate, statusName, total, import_status, Time_Completed, ERROR_DESCRIPTION FROM dbo.AllInvoicesInReadyStatus";
        String CountcmdString = @"SELECT count(*) FROM dbo.AllInvoicesInReadyStatus";
        SqlDataReader reader;
        int progress;
        int total;

        using (SqlConnection conn = new SqlConnection(lemars._LeMarsConnectionString))
        {
            reader = null;
            SqlCommand Sqlcmd = new SqlCommand(CountcmdString , conn);
            conn.Open();
            total = (int)Sqlcmd.ExecuteScalar(); //Get the total count.
            Sqlcmd.CommandText = SqlcmdString;
            using(reader = Sqlcmd.ExecuteReader()) //this should be in a using statement
            {
                while(reader.Read())
                {
                    object[] row = new object[reader.VisibleFieldCount];
                    reader.GetValues(row);
                    LoadSingleRowInToTable(dt, row); //I leave this to you to write.

                    //You can just read directly from the reader.
                    Obj.Invoice = reader["invoice"].ToString();
                    Obj.Shipment = reader["shipment"].ToString();
                    Obj.Project = reader["Project"].ToString();
                    Obj.InvoiceDateTB = Convert.ToDateTime(reader["invoiceDateTB"]);
                    Obj.CreatedDate = Convert.ToDateTime(reader["CreatedDate"]);
                    Obj.TypeName = reader["typeName"].ToString();
                    Obj.ExportedDate = Convert.ToDateTime(reader["exportedDate"]);
                    Obj.StatusName = reader["statusName"].ToString();
                    Obj.Total = Convert.ToDecimal(reader["total"]);
                    Obj.ImportStatus = reader["import_status"].ToString();
                    if (!Convert.IsDBNull(reader["Time_Completed"]))
                    {
                        Obj.TimeCompleted = Convert.ToDateTime(reader["Time_Completed"]);
                    }
                    Obj.ErrorDescription = reader["ERROR_DESCRIPTION"].ToString();

                    //Only call report progress when the progress value changes.
                    var newProgress = i * 100 / total;
                    if(progress != newProgress)
                    {
                        progress = newProgress;
                        dbWorker.ReportProgress(progress);
                    }
                    //Thread.Sleep(500); 
                }
            }
        }
    }

更新:这是一个基于 Steve 已删除的答案的示例,它显示了不使用数据表的更好解决方案。

private void dbWorker_DoWork(object sender, DoWorkEventArgs e)
{
    List<DatabaseColumns> data = GetTableToList();
    if (data == null) //data will be null if we canceled.
    {
        e.Cancel = true;
    }
    else
    {
        e.Result = data;
    }
    dbWorker.ReportProgress(100);
}
private void dbWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{

    if (e.Cancelled)
    {
        MessageBox.Show("Process Cancelled.");
    }
    else if (e.Error != null)
    {
        MessageBox.Show("Error occurred: " + e.Error.Message);
    }
    else
    {
        dataGridView_ShowAllData.DataSource = e.Result; //use the result from thebackground worker, only use if not canceled or errored.
        MessageBox.Show("Successful Completion.");
    }

    //progressBar_GetTasks.Value = 0;
}
private List<DatabaseColumns> GetTableToList()
{
    List<DatabaseColumns> data = new List<DatabaseColumns>();
    //prgBar_DataGridViewLoading
    String SqlcmdString = @"SELECT invoice, shipment, Project, invoiceDateTB, CreatedDate, typeName, exportedDate, statusName, total, import_status, Time_Completed, ERROR_DESCRIPTION FROM dbo.AllInvoicesInReadyStatus";
    String CountcmdString = @"SELECT count(*) FROM dbo.AllInvoicesInReadyStatus";

    using (SqlConnection conn = new SqlConnection(lemars._LeMarsConnectionString))
    {
        SqlCommand Sqlcmd = new SqlCommand(CountcmdString, conn);
        conn.Open();
        var total = (int)Sqlcmd.ExecuteScalar();
        Sqlcmd.CommandText = SqlcmdString;
        int i = 0;
        int progress = 0;
        using (SqlDataReader reader = Sqlcmd.ExecuteReader()) //this should be in a using statement
        {
            while (reader.Read())
            {
                if (dbWorker.CancellationPending)
                {
                    //Exit early if operation was canceled.
                    return null;
                }
                DatabaseColumns Obj = new DatabaseColumns();

                //You can just read directly from the reader.
                Obj.Invoice = reader["invoice"].ToString();
                Obj.Shipment = reader["shipment"].ToString();
                Obj.Project = reader["Project"].ToString();
                Obj.InvoiceDateTB = Convert.ToDateTime(reader["invoiceDateTB"]);
                Obj.CreatedDate = Convert.ToDateTime(reader["CreatedDate"]);
                Obj.TypeName = reader["typeName"].ToString();
                Obj.ExportedDate = Convert.ToDateTime(reader["exportedDate"]);
                Obj.StatusName = reader["statusName"].ToString();
                Obj.Total = Convert.ToDecimal(reader["total"]);
                Obj.ImportStatus = reader["import_status"].ToString();
                if (!Convert.IsDBNull(reader["Time_Completed"]))
                {
                    Obj.TimeCompleted = Convert.ToDateTime(reader["Time_Completed"]);
                }
                Obj.ErrorDescription = reader["ERROR_DESCRIPTION"].ToString();

                //Add the object to the list.
                data.Add(Obj);

                //Only call report progress when the progress value changes.
                var newProgress = i * 100 / total;
                if (progress != newProgress)
                {
                    progress = newProgress;
                    dbWorker.ReportProgress(progress);
                }
                i++;
            }
        }
    }
    return data;
}