Eric
Eric

Reputation: 359

Binding based on thread state

I have a WPF application with a few dozen user panels displayed in a listbox, each panel representing a task to execute. Within each panel there will be a 'Start' and 'Stop' button for controlling a thread.

Each panel includes a private background worker for execution and only one can be executing at a time since they access a shared resource. Therefore I want to disable the buttons in every panel that isn't running a task once a task begins in any panel and, of course, re-enable them when everything completes.

So I want to enable & disable based on 2 properties: 1. Whether the private background worker instance variable is null 2. Whether a public static object has a lock (obtained using Monitor.Enter or lock)

I would like to enable/disable the buttons based on the following logic:

'Start' button: - enabled if public object is not locked (means no threads are running), else disabled (at least one thread, possibly the one from this class, is running)

'Stop' button - Enabled if private background worker is not null (thread from this class is starting/running), else disabled (no applicable threads to stop)

When a thread starts it will obtain a lock on the shared object and initialize the local background worker which will enable the single stop button and disable all other start buttons.

I'm pretty new to WPF and am looking into data binding. I could probably figure out how to bind to the background worker == or != null but I'm not sure about how to test whether a lock exists on an object and how to bind to that.

Examples: Here is some sample code following up on answers provided below

Create a userpanel with two buttons (no bindings implemented for stop button)

<StackPanel Orientation="Horizontal">
<Button Margin="2" x:Name="btnStart" Content="Start" Click="btnStart_Click" IsEnabled="{Binding CanCommandsExecute}"/>
<Button Margin="2" x:Name="btnStop" Content="Stop"/>
</StackPanel>

Place multiple instances of this into a window

<StackPanel Orientation="Vertical">
<wpfsample:TestControl/>
<wpfsample:TestControl/>
<wpfsample:TestControl/>
</StackPanel>

And here is the codebehind for TestControl

public partial class TestControl : UserControl, INotifyPropertyChanged
{
    private static bool IsLocked = false;
    private static object threadlock = new object();
    private BackgroundWorker _worker;

    public event PropertyChangedEventHandler PropertyChanged;

    private bool _canCommandsExecute = true;
    public bool CanCommandsExecute { 
        get { return _canCommandsExecute && (!IsLocked); } 
        set { _canCommandsExecute = value; OnPropertyChanged("CanCommandsExecute"); } }

    public TestControl()
    {
        DataContext = this;
        InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
        Monitor.Enter(threadlock);
        try
        {
            IsLocked = true;
            this.CanCommandsExecute = false;
            _worker = new BackgroundWorker();
            _worker.DoWork += (x, y) => { Thread.Sleep(5000); };
            _worker.RunWorkerCompleted += WorkComplete;
            _worker.RunWorkerAsync();
        }
        catch { Monitor.Exit(threadlock); }
    }

    private void WorkComplete(object sender, EventArgs e)
    {
        IsLocked = false;
        this.CanCommandsExecute = true;
        Monitor.Exit(threadlock);
    }

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }            
    }

}

This partially solves the question. When you hit start it disables the button and runs a background task. It does so using a WPF binding as requested as well.

The outstanding question is how to make ALL the start buttons disable instead of just one. I'm getting a lock on a static object (which isn't working right at the moment, looking into that)

Hopefully this example helps

Upvotes: 0

Views: 425

Answers (3)

Eric
Eric

Reputation: 359

I don't like to answer my own questions but I had a specific scenario I was trying to get.

  1. Each instance of a class was a user panel containing start & stop buttons
  2. There would be multiple instances on the screen at any given time
  3. When any one had clicked start every other start button would be disabled until the task had finished.

@Erik - Good suggestions posted here but it involved something external (a wrapper class perhaps) to manage all the instances. My goal is for all the instances to work independently of one another. It is true that there is some interdependency but this exists within the class's static members, so the class by itself still remains independent.

@Rachel - This was a bit over my head for now but I'm going to try to make a solution using your suggestions as I learn more about WPF.

Thank you both for the suggestions.


This solution uses the XAML from the example in my question (with the classname changed to Testcase) but all the work is done in code behind. There are no databindings.

There are class static events handled in each class. If any instance starts or stops each class handles this event and enables/disables its own buttons. This keeps all the logic within the class itself without a wrapper class.

This uses a background worker for threading which lacks an abort method, so it has to poll for cancel. There are better approaches but since this was a UI synchronization question and not a threading question I left it simple for an example.

public partial class Testcase : UserControl
{

    public static event EventHandler TestStarted;
    public static event EventHandler TestStopped;
    private static object lockobject = new object();

    private BackgroundWorker _worker;

    public Testcase()
    {
        InitializeComponent();

        //Register private event handlers with public static events
        Testcase.TestStarted += this.OnTestStart;
        Testcase.TestStopped += this.OnTestStop;

        //Set the default button states (start = enabled, stop = disabled)
        //Could be done in XAML, done here for clarity
        btnStart.IsEnabled = true;
        btnStop.IsEnabled = false;
    }

    private void OnTestStart(object sender, EventArgs e)
    {
        UpdateButtonStatus(sender, true);
    }

    private void OnTestStop(object sender, EventArgs e)
    {
        UpdateButtonStatus(sender, false);
    }

    private void UpdateButtonStatus(object eventCaller, bool testStarted)
    {
        Testcase testcase;
        if ((eventCaller is Testcase) && (eventCaller != null))
            testcase = (Testcase)eventCaller;
        else
            return;

        btnStart.IsEnabled = !testStarted;
        btnStop.IsEnabled = (eventCaller == this) && testStarted;
    }

    private void btnStart_Click(object sender, EventArgs e)
    {
        lock (Testcase.lockobject)
        {
            try
            {
                //Raise the event starting the test while still in the UI thread
                TestStarted(this, new EventArgs());

                //Use a background worker to execute the test in a second thread
                _worker = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true };                    
                _worker.DoWork += (x, y) => 
                    {
                        for (int i = 1; i <=50; i++)
                        {
                            if (_worker.CancellationPending)
                            {
                                y.Cancel = true;
                                break;
                            }
                            //Simulate work
                            Thread.Sleep(100); 
                        }                      
                    };
                _worker.RunWorkerCompleted += WorkComplete;
                _worker.RunWorkerAsync();
            }
            catch
            {
                //Ignore handling the error for the POC but raise the stopped event
                TestStopped(this, new EventArgs());
            }
        }
    }

    private void WorkComplete(object sender, EventArgs e)
    {
        TestStopped(this,new EventArgs());
    }

    private void btnStop_Click(object sender, EventArgs e)
    {
        //Terminate the background worker
        _worker.CancelAsync();
    }




}

Upvotes: 1

Erik Dietrich
Erik Dietrich

Reputation: 6090

Without knowing exactly what your binding structure is (code behind, view models, etc), I would suggest that you forget about figuring out how to make the GUI/WPF understand your underlying object model and focus on making your code easy to use from the XAML.

To wit, don't spend time figuring out how to bind XAML to whether or not something is null and whether or not something else is locked. Instead, expose a property from your binding target that resolves these into what the object wants.

Rachel's RelayCommand (or perhaps a DelegateCommand) is a good idea, as that is a nice way to work with buttons. However, if you're new to WPF, that might be a little much to start with, in terms of really understanding what's going on.

Let's say that your buttons bind to some click event that is handled in your code behind:

public void ButtonClickHandler(/*Arguments elided*/)
{
    //Start the appropriate thread
}

Now, if you make this piece of code behind the source of your binding:

public class MyPage : INotifyPropertyChanged
{

    private bool _canCommandsExecute;
    public bool CanCommandsExecute { get { return _canCommandsExecute; } set { _canCommandsExecute = value; RaisePropertyChanged("CanCommandsExecute"); } }

    public MyPage()
    {
         DataContext = this;
         InitializeComponent();
    }

    public void ButtonClickHandler(/*Arguments elided*/)
    {
         CanExecute = false;
         //Pseudocode: Thread.OnCompleted += (sender, completeargs) => CanExecute = true;
         //Start the appropriate thread
    }
}

Your buttons in XAML then bind to the boolean property here for their IsEnabled properties, which will be set to false when you start a task and then set back to true when the task completes. The property setter will trigger PropertyChanged for the GUI, which will update the buttons back to enabled.

To be clear, this is something that is conceptually easy to understand if you're new to the framework, but not the best way to do it, in my opinion. It's a stepping stone to the best way to do it. Once you understand what's going on here, you can look into using View Models for binding and into looking at binding buttons to RelayCommands or DelegateCommands on the view model, instead of using event handlers and button IsEnabled. That was my experience, anyway, when learning WPF. ViewModels/Commands are elegant, but it's easier to understand the benefits of them and why they're often preferable once you've done it the easier-to-understand, code-behind way first.

Upvotes: 1

Rachel
Rachel

Reputation: 132548

I would probably bind my button to a RelayCommand in the ViewModel, which has it's CanExecute bound to an CanExecute flag.

I would also use an event system such as PRISM's EventAggregator to broadcast messages about if a thread has started or not, and the individual items would subscribe to these items and set the CanExecute flag based on this.

Since the Button's Command property would be bound to a RelayCommand, they will be automatically enabled/disabled when the CanExecute parameter would evaluate to false.

Here's an example. I left out some parts of it to try and keep the code limited to only the relevant bits.

public class SomeBaseClass()
{
    Public SomeBaseClass(IEventAggregator eventAggregator)
    {
        eventAggregator.GetEvent<ThreadStartedEvent>().Subscribe(DisableCanExecute);
        eventAggregator.GetEvent<ThreadStoppedEvent>().Subscribe(EnableCanExecute);
    }

    private bool _canExecute;
    private ICommand _startCommand;
    private ICommand _endCommand;

    public ICommand StartCommand
    {
        get
        {
            if (_startCommand== null)
            {
                _startCommand= new RelayCommand(
                    param => StartThread(),
                    param => this.BackgroundWorker != null && this.CanExecute
                );
            }
            return _startCommand;
        }
    }

    public ICommand EndCommand
    {
        get
        {
            if (_endCommand== null)
            {
                _endCommand = new RelayCommand(
                    param => StopThread(),
                    param => this.IsRunning == true
                );
            }
            return _endCommand;
        }
    }

    public void DisableCanExecute(ThreadStartedEvent e)
    {
       CanExecute = false;
    }

    public void EnableCanExecute(ThreadStoppedEvent e)
    {
       CanExecute = true;
    }
}

I actually don't like the syntax to PRISM's EventAggregator because I don't like to pass the event aggregator around to my ViewModels, so usually use a helper class which makes it static. The code for that can be found here

I also usually use MVVM Light's version of RelayCommand, or you can make your own. I could also use PRISM's DelegateCommand, although that one doesn't automatically re-run CanExecute() when parameters change. The basic definition of a RelayCommand looks like this:

/// <summary>
/// A command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the
/// CanExecute method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameters)
    {
        return _canExecute == null ? true : _canExecute(parameters);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameters)
    {
        _execute(parameters);
    }

    #endregion // ICommand Members
}

Upvotes: 2

Related Questions