Search code examples
c#wpfmultithreadingprogress-bardispatcher

Dispatcher with multi thread


I have a multi thread application with a background worker which I use to display a splash screen for processing the creation of the main window.

I want to update the progress bar in the thread 'u' in my program, so I will have to invoke the progressbar control each time I want to update it from the thread 'u'. So that means I don't have to use "backgroundWorker_DoWork" especially.

The problem I have is that I can't display the mainwindow (form2) when "backgroundWorker_RunWorkerCompleted" event is called.

I think the problem is about the dispatcher.

public partial class App : Application
{
    public BackgroundWorker backgroundWorker;
    private SplashScreenWindow splashScreen;

    public static EventWaitHandle initWaitHandle = new AutoResetEvent(false);
    public MainWindow Form2 { get; set; } 
    [DllImport("kernel32.dll")]
    private static extern bool AllocConsole();

    protected override void OnStartup(StartupEventArgs e)
    {
        backgroundWorker = new BackgroundWorker();
        backgroundWorker.WorkerReportsProgress = true;
        backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
        backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
        backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);

        splashScreen = new SplashScreenWindow();
        splashScreen.ShowInTaskbar = false;
        splashScreen.ResizeMode = ResizeMode.NoResize;
        splashScreen.WindowStyle = WindowStyle.None;
        splashScreen.Topmost = true;
        splashScreen.Width =  (SystemParameters.PrimaryScreenWidth) / 2.5;
        splashScreen.Height = (SystemParameters.PrimaryScreenHeight) / 2.5;
        splashScreen.Show();
        base.OnStartup(e);

        backgroundWorker.RunWorkerAsync();

        Thread u = new Thread(new ThreadStart(interface_process));
        u.SetApartmentState(ApartmentState.STA);
        u.Start();
    }

    public void interface_process()
    {
        MainWindow form2 = new MainWindow();
        this.Form2 = form2;
        System.Windows.Threading.Dispatcher.Run();
    }

    void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        splashScreen.Close();
        this.Form2.Invoke((Action)delegate()
        {
            this.Form2.Show(); // does not work
            System.Windows.Threading.Dispatcher.Run();
        });

    }

    void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        splashScreen.ValueProgressBar = e.ProgressPercentage;
    }

    void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        for (int i = 0; i <= 100; i += 10)
        {
            backgroundWorker.ReportProgress(i, "Chargement en cours : " + i);
            Thread.Sleep(500);
        }
    }
}

Solution

  • It's not clear to me how the code you posted would even compile, as the WPF Window class does not have an Invoke() method on it. That would cause a compile-time error in the code here:

    void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        splashScreen.Close();
        this.Form2.Invoke((Action)delegate()
        {
            this.Form2.Show(); // does not work
            System.Windows.Threading.Dispatcher.Run();
        });
    }
    

    If the above code is changed so that the second statement in the method reads this.Form2.Dispatcher.Invoke((Action)delegate() — that is, use the Dispatcher object that owns the Form2 object — not only should the code compile, but the call this.Form2.Show() should also work. Note that the second call to Dispatcher.Run() is not needed and in fact should be avoided.

    The "correct" implementation of the method thus would look like this:

    void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        splashScreen.Close();
        this.Form2.Dispatcher.Invoke((Action)delegate()
        {
            this.Form2.Show(); // works fine
        });
    }
    


    Now, that said…it seems to me that the whole approach is flawed. You really should just have the one UI thread in your program. If you want something to happen first while a splash screen is shown, then make the splash screen window the first window that's shown, run the background task, and then show the main window in the same thread when it's done.

    Here is an example of what you seem to be trying to do, but written to use just the main UI thread and the normal WPF startup mechanisms…

    Let's assume we start with a plain WPF project in Visual Studio. This will have in it already an App class and a MainWindow class. We just need to edit it to do what you want.

    First, we need a splash screen window. Most of the configuration can be done in XAML; because you want the width and height computed based on the screen size, it's easiest (for me) to just put that in the constructor. This winds up looking like this:

    SplashScreenWindow.xaml:

    <Window x:Class="TestSingleThreadSplashScreen.SplashScreenWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            DataContext="{Binding RelativeSource={RelativeSource Self}}"
            ShowInTaskbar="False"
            ResizeMode="NoResize"
            WindowStyle="None"
            Topmost="True"
            Title="SplashScreenWindow">
      <Grid>
        <ProgressBar HorizontalAlignment="Left" Height="10" Margin="10,10,0,0" 
                     VerticalAlignment="Top" Width="100"
                     Value="{Binding ValueProgressBar}"/>
      </Grid>
    </Window>
    

    SplashScreenWindow.xaml.cs:

    public partial class SplashScreenWindow : Window
    {
        public readonly static DependencyProperty ValueProgressBarProperty = DependencyProperty.Register(
            "ValueProgressBar", typeof(double), typeof(SplashScreenWindow));
    
        public double ValueProgressBar
        {
            get { return (double)GetValue(ValueProgressBarProperty); }
            set { SetValue(ValueProgressBarProperty, value); }
        }
    
        public SplashScreenWindow()
        {
            InitializeComponent();
    
            Width = SystemParameters.PrimaryScreenWidth / 2.5;
            Height = SystemParameters.PrimaryScreenHeight / 2.5;
        }
    }
    

    Now the above class is the one we want shown first. So we edit the App class's XAML to do that, by changing the StartupUri property:

    <Application x:Class="TestSingleThreadSplashScreen.App"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 StartupUri="SplashScreenWindow.xaml">
        <Application.Resources>
    
        </Application.Resources>
    </Application>
    

    Finally, we need our App class to run the BackgroundWorker, doing the appropriate things at various times:

    public partial class App : Application
    {
        private SplashScreenWindow SplashScreen { get { return (SplashScreenWindow)this.MainWindow; } }
    
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
    
            BackgroundWorker backgroundWorker = new BackgroundWorker();
    
            backgroundWorker.WorkerReportsProgress = true;
            backgroundWorker.DoWork += backgroundWorker_DoWork;
            backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
            backgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged;
            backgroundWorker.RunWorkerAsync();
        }
    
        void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            new MainWindow().Show();
            SplashScreen.Close();
        }
    
        void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            SplashScreen.ValueProgressBar = e.ProgressPercentage;
        }
    
        void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker backgroundWorker = (BackgroundWorker)sender;
    
            for (int i = 0; i <= 100; i += 10)
            {
                backgroundWorker.ReportProgress(i, "Chargement en cours : " + i);
                Thread.Sleep(500);
            }
        }
    }
    

    The only tricky thing in there is that the splash screen window must be closed after you show the main window. Otherwise, WPF will think you've closed the last window (well, technically you would have :) ) and will shut down the program. By showing the main window before closing the splash screen window, the program continues to run.

    In my opinion, this is a much better way to do things, as it works with the normal WPF mechanisms, rather than trying to subvert and/or work around them. It takes advantage of the Dispatcher that is created automatically when the program starts, doesn't require an extra UI thread, etc. Oh, and…it works. So there's that. :)