views:

174

answers:

2

I have a WPF app that simply contains a Button and a Textbox to display some output. When the user clicks the Button, a thread starts which disables the Button, prints stuff to the output Textbox, then the thread stops (at which point I want the Button to be enabled again).

The application appears to disable the Button properly, as well as update the Textbox properly. However, it always fails to re-enable the Button properly when the thread completes! Can anyone tell me what I'm doing wrong?

Here's a snippet of my xaml:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Button Grid.Row="0" HorizontalAlignment="Center" Command="{Binding ExecuteCommand}">E_xecute</Button>
    <Label Grid.Row="1" Content="Output Window:" HorizontalAlignment="Left"/>
    <TextBlock Grid.Row="2" Text="{Binding Output}"/>
</Grid>

Here's my ViewModel code (I'm using Josh Smith's MVVM design):

public class WindowViewModel : ViewModelBase
{
    private bool _threadStopped;
    private RelayCommand _executeCommand;
    private string _output;

    public WindowViewModel()
    {
        _threadStopped = true;
    }

    public string Output { get { return _output; } set { _output = value; OnPropertyChanged("Output"); } }

    public ICommand ExecuteCommand
    {
        get
        {
            if (_executeCommand == null)
            {
                _executeCommand = new RelayCommand(p => this.ExecuteThread(p), p => this.CanExecuteThread); 
            }
            return _executeCommand;
        }
    }

    public bool CanExecuteThread
    {
        get
        {
            return _threadStopped;
        }
        set
        {
            _threadStopped = value;
        }
    }

    private void ExecuteThread(object p)
    {
        ThreadStart ts = new ThreadStart(ThreadMethod);
        Thread t = new Thread(ts);
        t.Start();
    }

    private void ThreadMethod()
    {
        CanExecuteThread = false;
        Output = string.Empty;
        Output += "Thread Started:  Is the 'Execute' button disabled?\r\n";
        int countdown = 5000;

        while (countdown > 0)
        {
            Output += string.Format("Time remaining: {0}...\r\n", countdown / 1000);
            countdown -= 1000;
            Thread.Sleep(1000);
        }
        CanExecuteThread = true;
        Output += "Thread Stopped:  Is the 'Execute' button enabled?\r\n";
    }
}
+1  A: 

You'll need to help WPF know that the executable state of the command has changed. The simple way to do this is:

CommandManager.InvalidRequerySuggested()

inside CanExecuteThread:

set
{
    _threadStopped = value;
    CommandManager.InvalidRequerySuggested()
}

EDIT (now that I have time): the actual problem is likely that you're not notifying when the CanExecuteThread property changes. It should raise PropertyChanged in order for WPF to detect the change:

public bool CanExecuteThread
{
    get { return _threadStopped; }
    set
    {
        if (_threadStopped != value)
        {
            _threadStopped = value;
            this.OnPropertyChanged(() => this.CanExecuteThread);
        }
    }
}

The above assumes your ViewModel base class has an OnPropertyChanged method.

That said, I also wanted to point out that you could simplify things by simply using a BackgroundWorker:

public class WindowViewModel : ViewModel
{
    private readonly BackgroundWorker backgroundWorker;

    public WindowVieWModel()
    {
        backgroundWorker = new BackgroundWorker();
        backgroundWorker.DoWork += delegate
        {
            // do work here (what's currently in ThreadMethod)
        };
        backgroundWorker.RunWorkerCompleted += delegate
        {
            // this will all run on the UI thread after the work is done
            this.OnPropertyChanged(() => this.CanExecuteThread);
        };
    }

    ...

    public bool CanExecuteThread
    {
        get { !this.backgroundWorker.IsBusy; }
    }

    private void ExecuteThread(object p)
    {
        // this will kick off the work
        this.backgroundWorker.RunWorkerAsync();

        // this property will have changed because the worker is busy
        this.OnPropertyChanged(() => this.CanExecuteThread);
    }
}

You could refactor this further to be even nicer, but you get the idea.

HTH,
Kent

Kent Boogaart
I placed the line of code as you suggested, but the Button still appears disabled when the thread completes. It's only when I give focus to something within the Window (i.e. a mouse click, or a shortcut key) does the Button become re-enabled. (NOTE: However, I was seeing this even before your suggested solution). Any other suggestions would be greatly appreciated.
Tam Bui
Try doing it on the UI thread via a Dispatcher.Invoke call
Kent Boogaart
Thanks, Kent! That did it! I will post the answer for others to see the solution.
Tam Bui
A: 

Here is the answer, as suggested by Kent Boogaart, and it works. Basically, I had to call CommandManager.InvalidateRequerySuggested on the UI thread by placing it within a Dispatcher invoke call. Also notice that I was able to get rid of the Set accessor on the CanExecuteThread property, as it was no longer required with this solution. Thank, Kent!

public class WindowViewModel : ViewModelBase
{
    private bool _threadStopped;
    private RelayCommand _executeCommand;
    private string _output;
    private Dispatcher _currentDispatcher;
    public WindowViewModel()
    {
        _threadStopped = true;
        _currentDispatcher = Dispatcher.CurrentDispatcher;
    }

    public string Output { get { return _output; } set { _output = value; OnPropertyChanged("Output"); } }

    public ICommand ExecuteCommand
    {
        get
        {
            if (_executeCommand == null)
            {
                _executeCommand = new RelayCommand(p => this.ExecuteThread(p), p => this.CanExecuteThread); 
            }
            return _executeCommand;
        }
    }

    private delegate void ZeroArgDelegate();

    public bool CanExecuteThread
    {
        get
        {
            return _threadStopped;
        }
    }

    private void ExecuteThread(object p)
    {
        ThreadStart ts = new ThreadStart(ThreadMethod);
        Thread t = new Thread(ts);
        t.Start();
    }

    private void ThreadMethod()
    {
        _threadStopped = false;
        Output = string.Empty;
        Output += "Thread Started:  Is the 'Execute' button disabled?\r\n";
        int countdown = 5000;

        while (countdown > 0)
        {
            Output += string.Format("Time remaining: {0}...\r\n", countdown / 1000);
            countdown -= 1000;
            Thread.Sleep(1000);
        }
        _threadStopped = true;
        _currentDispatcher.BeginInvoke(new ZeroArgDelegate(resetButtonState), null);
        Output += "Thread Stopped:  Is the 'Execute' button enabled?\r\n";
    }

    private void resetButtonState()
    {
        CommandManager.InvalidateRequerySuggested();
    }
}
Tam Bui