-1

In a WPF application I have an SQL query that takes some times to execute and I want to implement a BackgroundWorker with a progress bar so that the user can see when it's finished.

Here's the XAML part :

<Grid Grid.Row="2">
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <ProgressBar x:Name="BgProgBar" Margin="5"/>
    <TextBlock x:Name="Perc_TB" HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Text="{Binding Path=Value,ElementName=BgProgBar}"/>
</Grid>

and here's my BackgroundWorker in code behind :

  public partial class Supp_Stocks : Page
  {
    private string _user = Settings.Default.User;
    private DataTable dt;
    SqlConnection conn;
    SqlCommand comm;
    SqlConnectionStringBuilder connStringBuilder;
    BackgroundWorker Bg = new BackgroundWorker();
    public Supp_Stocks()
    {
        InitializeComponent();
        ConnectToDB();
        Bg.DoWork += Bg_DoWork;
        Bg.ProgressChanged += Bg_ProgressChanged;
        Bg.WorkerReportsProgress = true;

    }

    #region BackGroundWorker
    private void Bg_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        BgProgBar.Value = e.ProgressPercentage;
    }

    private void Bg_DoWork(object sender, DoWorkEventArgs e)
    {
        for(int i = 0; i < 100; i++)
        {
            try
            {
                comm = new SqlCommand("select STOCK_ENT.ENT_ID,ENT_DATE_ENT,ENT_NUMPAL,CLI_NOM,ART_LIBELLE1,ENT_PICKING,SUM(det_pnet)" +
                " as POIDS_NET from STOCK_DET, STOCK_ENT, CLIENTS, FICHES_ARTICLES, MVTS_SEQUENCE where SEQ_STATUT <> 'V' and" +
                " STOCK_ENT.ENT_ID = STOCK_DET.ENT_ID and STOCK_ENT.ENT_PROP = CLI_CODE and STOCK_ENT.ART_CODE = FICHES_ARTICLES.ART_CODE" +
                " and STOCK_ENT.ENT_ID = MVTS_SEQUENCE.ENT_ID group by STOCK_ENT.ENT_ID, ENT_DATE_ENT, ENT_NUMPAL, CLI_NOM, ART_LIBELLE1, ENT_PICKING order by ART_LIBELLE1", conn);
                SqlDataAdapter dap = new SqlDataAdapter(comm);
                dt = new DataTable();
                dap.Fill(dt);
                Stocks_DT.ItemsSource = dt.DefaultView;
                Bg.ReportProgress(i);
            }
            catch (Exception ex)
            {

                var stList = ex.StackTrace.ToString().Split('\\');
                Messages.ErrorMessages($"ERREUR : \n{ex.Message}\n\nException at : \n{stList[stList.Count() - 1]}");
            }

        }
    }
    private void GenerateStk_Btn_Click(object sender, RoutedEventArgs e)
    {
        Bg.RunWorkerAsync();
    }
    #endregion

    private void ConnectToDB()
    {
        connStringBuilder = new SqlConnectionStringBuilder
        {
            DataSource = @"VM-VISUALSTORE\SQLEXPRESS,1433",
            InitialCatalog = "GSUITE",
            Encrypt = true,
            TrustServerCertificate = true,
            ConnectTimeout = 30,
            AsynchronousProcessing = true,
            MultipleActiveResultSets = true,
            IntegratedSecurity = true,
        };
        conn = new SqlConnection(connStringBuilder.ToString());
        comm = conn.CreateCommand();
    }
}

but when executing the code its returns an error:

Error Message

Sorry it's in French but it says that "The thread cannot access this object because another thread owns it.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Possible duplicate of [How to deal with cross-thread access exceptions?](https://stackoverflow.com/questions/11923865/how-to-deal-with-cross-thread-access-exceptions) – Peter Duniho Oct 20 '19 at 05:40

1 Answers1

1

You didn't say which line in your code throws the error, but I'm pretty sure it's this one, right?

Stocks_DT.ItemsSource = dt.DefaultView;

And Stocks_DT is a control on your window, right?

That's the source of your problem. As the error says, you can only access UI elements from the UI thread (the main thread). Since you are running the above code on the background thread, it's throwing this exception.

Instead, you need to pass the results of the query back to the main thread and then assign it to Stocks_DT.ItemsSource. I would recommend using the DoWorkEventArgs.Result property. I would suggest using the DataTable as your result so the code would be e.Result = dt. You can access the Result from the BackgroundWorker.RunWorkerCompleted event.


The above will solve the exception, but there are a number of other things I feel I should go over in your code, purely in the interest of helping you out.

First of all, you seem to be looping the same exact query 100 times without any changes. This seems like it would be a waste of time since you'd just get the same result 100 times. If this is the case, you probably don't need a progress bar at all.

Secondly, it is generally considered best practice to open a new SqlConnection when one is needed and close it when it's finished, instead of keeping one connection open for the lifetime of the program/window. This is especially true as you get into multi-threading. You can find more information in this question. When creating these connections, they should be enclosed in a using statement.

Third, C# supports multi-line strings as shown in this question. Using this method will make the SQL query more readable by getting rid of all the "..." + "..."

Keith Stein
  • 6,235
  • 4
  • 17
  • 36