简体   繁体   中英

Error loading custom user controls from different threads

I have a WPF project and from the main window i am creating and loading some bunch of user controls, there is some large data i am loading in background and then updating a built-in control throw the dispatcher, that works fine, the problem is that some of the user controls loads a lot of data, for example the very first thing i load in the main area of my main window, what i want is to put a loading label instead, load the main window as fast as possible so the user see this label and run in background the creation of that user control and when is done add it as a child of my main container area on my main window while i remove the loading label, if i follow the same philosophy i run into the same error like when i run a task and then try to update the window without using the dispatcher. i want to be able of create the user control asynchronous then update the main window.

Code:

User Control:

public partial class CustomUserControlGallery : UserControl
{
  public CustomUserControlGallery()
  {
    InitializeComponent();
  }
    ...
}

On the backend class of the main window:

public partial class MainWindow : Window
{
  CustomUserControlGallery _customUserControlGallery; 

  public MainWindow()
  {
    InitializeComponent();
    Task t = new Task({
    //Can't use the _customUserControlGallery's Dispatcher because object is uninitialized and this.Dispatcher not working either.
      _customUserControlGallery = new CustomUserControlGallery(); //Error Here.
      _gridContainer.Dispatcher.Invoke(new Action(() => _gridContainer.Children.Add(_customUserControlGallery)));
      _loadingLabel.Visbility = Visibility.Collapse;
    });

     t.Start();
   }
   ...
}

I don't know how to handle this situation with the thread associated to the user control and the main thread.

Error:

{"The calling thread must be STA, because many UI components require this."}

You're doing this wrong. All controls must be created & operate on the UI Thread. That said, you can use the BackgroundWorker class to load the data.

You typically do this by disabling the control whose data is being loaded in the background or hiding it & displaying a progress indicator in its place. Then, you start your BackgroundWorker . That can communicate how far along it is using the ReportProgress method. Finally, when it's finished running, the RunWorkerCompleted event is fired, and you use that to either enable the control, or to hide the progress indicator & show the control.

Some quick & dirty (untested) code:

Place this in your Initialize() or control constructor:

private BackgroundWorker loadData = new BackgroundWorker();

loadData.DoWork += loadData_DoWork;
loadData.ProgressChanged += loadData_ProgressChanged; // Only do this if you are going to report progress
loadData.WorkerReportsProgress = true;
loadData.WorkerSupportsCancellation = false; // You can set this to true if you provide a Cancel button
loadData.RunWorkerCompleted += loadData_RunWorkerCompleted;


private void DoWork( object sender, DoWorkEventArgs e ) {
    BackgroundWorker worker = sender as BackgroundWorker;

    bool done = false;
    while ( !done ) {
        // If you want to check for cancellation, include this if statement
        if ( worker.CancellationPending ) {
            e.Cancel = true;
            return;
        }

        // Your code to load the data goes here.

        // If you wish to display progress updates, compute how far along you are and call ReportProgress here.
    }
}

private void loadData_ProgressChanged( object sender, ProgressChangedEventArgs e ) {
    // You code to report the progress goes here.
}

private void loadData_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e ) {
    // Your code to do whatever is necessary to put the UI into the completed state goes here.
}

What you are essentially saying (I think) is that Your app becomes sluggish while your control renders a large amount of data.

This is a problem that needs to be solved via virtualisation. You cannot create a control on a background thread, have it render its data behind the scenes and then pop it into existence. You can create controls on separate dispatchers, but they cannot share the same visual and logical tree, so you will not be able to have one as a child of the other.

Virtualisation is what you need to focus on. Depending on the control you can use a variety of virtualisation settings. Try googleing the subject as there is a lot of information on how to achieve this effectively. Most likely you will want to use things like virtualizing stackpanels and container recycling.

You cannot create UI controls with different Dispatchers and use them with each other. It's just not allowed. What you want to do is on your Task you do the heavy lifting work without UI updates and when it is done you push it back to the Dispatcher to update the UI .

In your case, I wouldn't even use Dispatcher.Invoke . Since you are using Task , it has a TaskScheduler.FromCurrentSynchronizationContext() that you can pass in the constructor.

What is the purpose of instantiating controls in a different thread if you're just going to put it back to the Main dispatcher? It's not expensive to do that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM