0

I am trying to incorporate a progress bar in my main window after a button is pressed and the button is running its process. I know I am just missing something simple but I'm still new to WPF as I mainly use Windows Forms.

My XML is structured as follows:

<Window x:Class="Program1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Program1"
        mc:Ignorable="d"
        Title="Program1" Height="1029" Width="1300" SnapsToDevicePixels="True" BorderThickness="0" Margin="0" ResizeMode="NoResize" Closing="Window_Closing"
        x:Name="FirstWindow">
    <Grid x:Name="Grid1">
        <Button x:Name="btnPopulate" Content="Populate" HorizontalAlignment="Left" Margin="243,66,0,0" VerticalAlignment="Top" Width="118" Height="29" Click="btnPopulate_Click"/>
        <Button x:Name="btnClear" Content="Clear" HorizontalAlignment="Left" Margin="366,66,0,0" VerticalAlignment="Top" Width="118" Height="29" Click="btnClear_Click"/>
        <ProgressBar x:Name="progressBar" HorizontalAlignment="Left" Height="30" Margin="10,943,0,0" VerticalAlignment="Top" Width="351"/>
    </Grid>
</Window>

I have my populate button click method as follows:

private void btnPopulate_Click(object sender, RoutedEventArgs e)
{
    Thread backgroundThread = new Thread(
            new ThreadStart(() =>
            {
                for (int n = 0; n < 100; n++)
                {
                    Thread.Sleep(50);
                    progressBar.Value = n;
                };
            }
        ));
    backgroundThread.Start();
}

The issue I am facing is that I am getting this error:

The name 'progressBar' does not exist in the current context

and I am unsure how I can access the progressBar control from my button click method.

I know I am likely missing something simple but I'm still trying to get the hang of WPF.

Manfred Radlwimmer
  • 13,257
  • 13
  • 53
  • 62
Cogorlopez
  • 63
  • 1
  • 9
  • First, the markup is XAML (which granted, is technically XML). You should also investigate using the MVVM pattern; using WPF like WinForms is a quick way to pain. – BradleyDotNET Mar 02 '17 at 19:51
  • 1
    You cannot use WPF controls from a thread other then the one they were created on. (Usually the UI thread) So what you are doing will not work. – Brandon Kramer Mar 02 '17 at 19:52
  • 1
    While @BrandonKramer is correct; if you updated via a binding you would be fine (those automatically marshal to the UI thread). You can also marshal to the UI thread with `Dispatcher.BeginInvoke` – BradleyDotNET Mar 02 '17 at 19:54
  • All that is a little too broad for SO, but once you get 20 rep you may consider asking questions in the WPF chat room as you go down the MVVM path: http://chat.stackoverflow.com/rooms/18165/wpf – BradleyDotNET Mar 02 '17 at 19:56

4 Answers4

0

You cannot access to controls from a thread that didn't create them (old Win32 limitation). You have to use UI Sync Context to access to UI elements from background thread something like this

Somewhere in the class define field

SynchronizationContext ctx = SynchronizationContext.Current ?? new SynchronizationContext();

and then use it:

void RunOnGuiThread(Action action)
{
    this.ctx.Post(o => action(), null);
}

You can also use tasks using TaskScheduler:

private readonly TaskScheduler uiSyncContext;

then define it

this.uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext();

and use

var task = Task.Factory.StartNew(delegate
{
   /// do something
});

this.CompleteTask(task, TaskContinuationOptions.OnlyOnRanToCompletion, delegate
{
   /// do something that use UI controls
});

public void CompleteTask(Task task, TaskContinuationOptions options, Action<Task> action)
{
   task.ContinueWith(delegate
   {
      action(task);
      task.Dispose();
   }, CancellationToken.None, options, this.uiSyncContext);
}
Alexander Egorov
  • 593
  • 10
  • 14
0

You should use Dispatcher.Invoke or Dispatcher.BeginInvoke methods because progressBar belongs to another thread. In other words, instead of

progressBar.Value = n;

use

Dispatcher.Invoke(new Action(()=> { progressBar.Value = n; }));

and your code should work, unless there are some typo in the names.

Please see this post for better choices in populating a ProgressBar.

Furthermore, Grid and Margin are not a good choice. Instead use DockPanel or add RowDefinitions or ColumnDefinitions to your Grid.

Community
  • 1
  • 1
rmojab63
  • 3,513
  • 1
  • 15
  • 28
0

Where is your btnPopulate_Click() method being declared? If in the MainWindow class, then the field containing the reference to the element should exist. Please provide a good Minimal, Complete, and Verifiable code example that reliably reproduces the compile-time error message you describe.

In the meantime…

Do note that your code is otherwise entirely wrong as well. It would be best to use MVVM and simply set the progress bar state value on a view model property, binding that property to your progress bar. You also should use some other mechanism than starting a dedicated thread for dealing with background operations. I understand the code you posted is just for practice, but it's good to get into the habit of doing things the right way.

Here are some options that would be better than what you have now, and would also be better than either of the other two answers posted so far.

If dealing with a single long-running operation that has good intermittent checkpoints where you can report progress:

First, define your view model:

class ViewModel : INotifyPropertyChanged
{
    private double _progressValue;

    public double ProgressValue
    {
        get { return _progressValue; }
        set { _UpdatePropertyField(ref _progressValue, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _UpdatePropertyField<T>(
        ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer.Default.Equals(field, value))
        {
            field = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Then in your C# code for the window:

class MainWindow : Window
{
    private readonly ViewModel _viewModel = new ViewModel();

    public MainWindow()
    {
        DataContext = _viewModel;
        InitializeComponent();
    }

    private void btnPopulate_Click(object sender, RoutedEventArgs e)
    {
        Task.Run(() =>
        {
            for (int n = 0; n < 100; n++)
            {
                // simulates some costly computation
                Thread.Sleep(50);

                // periodically, update the progress
                _viewModel.ProgressValue = n;
            }
        });
    }
}

And then in your XAML, bind the view model's ProgressValue property to the ProgressBar.Value property:

<Window x:Class="Program1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Program1"
        mc:Ignorable="d"
        Title="Program1" Height="1029" Width="1300" SnapsToDevicePixels="True"
        BorderThickness="0" Margin="0" ResizeMode="NoResize" Closing="Window_Closing"
        x:Name="FirstWindow">
  <Grid x:Name="Grid1">
    <Button x:Name="btnPopulate" Content="Populate" HorizontalAlignment="Left"
            Margin="243,66,0,0" VerticalAlignment="Top" Width="118" Height="29"
            Click="btnPopulate_Click"/>
    <Button x:Name="btnClear" Content="Clear" HorizontalAlignment="Left"
            Margin="366,66,0,0" VerticalAlignment="Top" Width="118" Height="29"
            Click="btnClear_Click"/>
    <ProgressBar HorizontalAlignment="Left" Height="30" Margin="10,943,0,0"
                 VerticalAlignment="Top" Width="351" Value="{Binding ProgressValue}"/>
  </Grid>
</Window>

If your long-running operation is actually made up of smaller, asynchronous operations, then you could do something like this instead:

private async void btnPopulate_Click(object sender, RoutedEventArgs e)
{
    for (int n = 0; n < 100; n++)
    {
        // simulates one of several (e.g. 100) asynchronous operations
        await Task.Delay(50);

        // periodically, update the progress
        _viewModel.ProgressValue = n;
    }
}

Note that in this second example, you could skip the view model altogether, because the assignment for the progress value occurs in the UI thread, and so it's safe to just assign directly to the ProgressBar.Value property there. But you should still use a view model anyway, because that's more in keeping with the standard WPF paradigm and the expectations of the WPF API (i.e. you can do it the other way, but you'll be fighting the intent of the designers of the WPF API, which will lead to more frustration and difficulty).

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
0

simplyfied version: You start another Thread that can't modify your UI-Thread-Content.

This solution solves it, but you still should learn about MVVM

 private void btnPopulate_Click(object sender, RoutedEventArgs e)
        {
            SynchronizationContext context = SynchronizationContext.Current;

            Thread backgroundThread = new Thread(
                    new ThreadStart(() =>
                    {
                        for (int n = 0; n < 100; n++)
                        {
                            Thread.Sleep(50);
                            context?.Post(new SendOrPostCallback((o) =>
                            {

                                progressBar.Value = n;
                            }), null);
                        };


                    }
                ));
            backgroundThread.Start();
        }
Peter
  • 1,655
  • 22
  • 44