【问题标题】:Binding based on thread state基于线程状态的绑定
【发布时间】:2011-12-01 18:58:24
【问题描述】:

我有一个 WPF 应用程序,在列表框中显示了几十个用户面板,每个面板代表一个要执行的任务。在每个面板中都会有一个“开始”和“停止”按钮来控制线程。

每个面板都包含一个用于执行的私有后台工作程序,由于它们访问共享资源,因此一次只能执行一个。因此,一旦任务在任何面板中开始,我想禁用每个未运行任务的面板中的按钮,当然,当一切完成时重新启用它们。

所以我想根据 2 个属性启用和禁用: 1.私有后台worker实例变量是否为null 2.公共静态对象是否有锁(使用Monitor.Enter或lock获取)

我想根据以下逻辑启用/禁用按钮:

“开始”按钮: - 如果公共对象未锁定则启用(意味着没有线程在运行),否则禁用(至少一个线程,可能来自此类的线程正在运行)

“停止”按钮 - 如果私有后台工作者不为空(来自此类的线程正在启​​动/运行),则启用,否则禁用(没有可停止的适用线程)

当一个线程启动时,它将获得共享对象的锁并初始化本地后台工作程序,这将启用单个停止按钮并禁用所有其他启动按钮。

我对 WPF 很陌生,正在研究数据绑定。我可能会弄清楚如何绑定到后台工作者 == 或 != null 但我不确定如何测试对象上是否存在锁以及如何绑定到该对象。

示例: 以下是一些示例代码,跟进下面提供的答案

创建一个带有两个按钮的用户面板(没有为停止按钮实现绑定)

<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>

将 this 的多个实例放入一个窗口中

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

这是 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));
        }            
    }

}

这部分解决了这个问题。当您点击开始时,它会禁用按钮并运行后台任务。它也按照要求使用 WPF 绑定来执行此操作。

悬而未决的问题是如何禁用所有的开始按钮,而不仅仅是一个。我正在锁定一个静态对象(目前无法正常工作,正在调查)

希望这个例子有所帮助

【问题讨论】:

  • 没有给出完整的答案,听起来你需要使用数据转换器绑定到对象。这将允许您在代码中而不是 xaml 中进行所需的任何检查。可能与多重绑定。
  • 如果要求是在当前任务完成之前不能执行其他任务,出于好奇,您为什么要使用线程?我错过了什么吗?您布置的内容听起来像教科书同步操作 - 用户可以执行一系列任务中的一项,但一次执行一项......
  • @Erik 我正在使用线程,因为启动的任务是长时间运行的进程,不仅仅是一两秒,但在某些情况下长达半小时,我不希望它们在用户界面线程。在任务运行时,您还可以在 UI 中执行其他操作(例如查看另一个先前完成的任务的结果等)。
  • 明白了——说得通。鉴于您在那里所说的以及您对 WPF 新手的评论,我发布了回复。

标签: wpf data-binding


【解决方案1】:

我可能会将我的按钮绑定到 ViewModel 中的 RelayCommand,它的 CanExecute 绑定到 CanExecute 标志。

我还将使用诸如 PRISM 的 EventAggregator 之类的事件系统来广播有关线程是否已启动的消息,并且各个项目将订阅这些项目并基于此设置 CanExecute 标志。

由于 Button 的 Command 属性将绑定到 RelayCommand,因此当 CanExecute 参数评估为 false 时,它​​们将自动启用/禁用。

这是一个例子。我省略了其中的一些部分,以尝试将代码限制为仅相关的位。

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;
    }
}

我实际上不喜欢 PRISM 的 EventAggregator 的语法,因为我不喜欢将事件聚合器传递给我的 ViewModel,所以通常使用一个帮助器类来使其成为静态。可以在here找到代码

我通常也使用MVVM Light's 版本的RelayCommand,或者你可以自己制作。我也可以使用 PRISM 的 DelegateCommand,尽管当参数更改时,它不会自动重新运行 CanExecute()RelayCommand 的基本定义如下所示:

/// <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
}

【讨论】:

  • 当有多个不同的命令必须执行时,我看不到这将如何工作。因此,如果我有 20 个我的类的实例(派生自 UserPanel),每个实例都包含一个开始和停止按钮,那么每个类都会有一个 RelayCommand 来完成它必须做的工作,对吗?我可以为特定的中继命令绑定到 CanExecute,但我需要为类的每个实例绑定到 CanExecute,因为一次只能真正执行一个。或者,在它检查共享锁的 RelayCommand 中,也许只有一个绑定?
  • @Eric 我将拥有定义命令的基类,并继承该功能,因此您不会重写 20 次。您要做的最多的就是覆盖每个类中的 Start/Stop 方法来运行您的进程。如果您使用静态对象作为锁,您应该能够从所有类访问它。如果任何 CanExecute 参数发生变化,RelayCommand 将重新评估它的 CanExecute 方法,因此如果按钮可用或不可用,您应该能够自动启用/禁用按钮。
  • 每个类代表一个要运行的测试,基类定义一次执行方法,因此定义一次,我也可以将 RelayCommand 放在基类中。每个实例中的变化是测试名称、描述、文件路径等 - 显示的属性但不会改变测试执行方式的行为。可以从所有实例访问静态锁定对象,但处理更改事件是我感到困惑的地方。当一个对象获得/释放锁定“CanExecute”时,必须为每个其他实例重新评估。
  • (续) - 您说 CanExecute 将重新评估其任何参数是否发生变化,但只是获取/释放静态对象上的锁定会导致强制重新评估的更改?我不确定锁是否真的改变了任何东西。如果它不算作更改,那么我需要第二个参数,例如我在 Monitor 代码块中切换的 static bool IsLocked 以强制进行真正的更改。这就是我在概念上的看法,我之前没有使用过 RelayCommand,所以如果这个概念有意义,我会更具体地研究它并尝试一个模型
  • @Eric 我个人会使用类似 PRISM 的EventAggregator,并在线程启动或停止时广播一条消息。然后您的各个班级可以订阅停止/开始消息并更新他们的CanExecute 或不基于此。
【解决方案2】:

在不确切知道您的绑定结构是什么(代码隐藏、视图模型等)的情况下,我建议您忘记弄清楚如何让 GUI/WPF 理解您的底层对象模型并专注于让您的代码易于从 XAML 中使用。

也就是说,不要花时间弄清楚如何将 XAML 绑定到某个内容是否为 null 以及其他内容是否被锁定。相反,从绑定目标中公开一个属性,将这些属性解析为对象想要的。

Rachel 的 RelayCommand(或者可能是 DelegateCommand)是个好主意,因为这是使用按钮的好方法。但是,如果您是 WPF 的新手,那么就真正了解正在发生的事情而言,这可能有点困难。

假设您的按钮绑定到在您的代码中处理的某个点击事件:

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

现在,如果您在绑定源后面制作这段代码:

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
    }
}

然后,您在 XAML 中的按钮将绑定到此处的布尔属性以获取其 IsEnabled 属性,该属性将在您启动任务时设置为 false,然后在任务完成时设置回 true。属性设置器将为 GUI 触发 PropertyChanged,这会将按钮更新为启用状态。

需要明确的是,如果您是该框架的新手,这在概念上很容易理解,但在我看来,这不是最好的方法。这是实现最佳方法的垫脚石。一旦你了解了这里发生的事情,你就可以考虑使用视图模型进行绑定,并查看将按钮绑定到视图模型上的 RelayCommands 或 DelegateCommands,而不是使用事件处理程序和按钮 IsEnabled。无论如何,这是我在学习 WPF 时的经验。 ViewModels/Commands 很优雅,但更容易理解它们的好处以及为什么它们通常更可取,一旦你先以更容易理解的代码隐藏方式完成它。

【讨论】:

  • 我将假设“CanExecute = false”行应该是“CanCommandsExecute=false”以触发 OnPropertyChanged 事件。我同意你的说法,保持简单。雷切尔肯定知道她在说什么,我会读一读,但目前有很多东西需要吸收。绑定到单个属性更容易,但它是实现 INotifyPropertyChanged 的​​我的 Testcase 类,它是包含“开始”按钮的 UserPanel。这适用于启用/禁用此 1 面板中的开始/停止按钮,但不适用于其他类中的所有其他按钮。这就是我卡住的地方
  • 我的要求是(给定您的示例)当 ButtonClickHandler 设置 CanCommandsExecute=false 时,该类的所有其他实例也将为 false。事件处理程序是一个实例方法,它可以在测试用例类中设置一个静态布尔 IsLocked 或其他东西,但问题是如何在每个实例上强制属性更改通知。这些 UserPanel 彼此独立工作,只有共享对象锁。让每个实例对象处理在基类中定义的 OnLockChange 事件会起作用吗?切换锁定时引发事件
  • 因此,您在 XAML 中有一个集合绑定到一个对象集合。很公平。代码(不是 XAML)中是否有对集合中所有对象的引用?如果是这样,您可以侦听每个人的 CanCommandsExecute 属性更改,并在其他人中设置值(尽管如果您这样做以防止堆栈溢出,则应仅在值已更改时触发通知)。 (是的,我的意思是设置 CanCommandsExecute ——我是徒手输入的:o)
  • 哦,我刚刚看到您对原始帖子的更新代码。因此,您可以做的一件事是为控件命名: 然后,在该类背后的代码中,您可以监听它们的属性更改并通过设置其他。请注意,这是可行的——这不是你可以做的最优雅的事情,但我相信让它发挥作用而不是精炼。一旦这对你有用,我建议研究视图模型的概念以进行绑定而不是代码隐藏。
  • 技术上我确实收集了所有这些,但我希望它们能够独立工作。我同意你让它工作然后改进,在那个注释上,我可以完全在代码隐藏中正确设置启用。我正在尝试学习如何在没有它的情况下做到这一点。设计是它们都独立工作(除了它们在执行时无法访问共享资源)。我发布了一些源代码。锁还不能正常工作(研究一下,线程也是新手,天哪!)但您可以看到总体思路。
【解决方案3】:

我不喜欢回答我自己的问题,但我有一个我想要得到的特定场景。

  1. 类的每个实例都是一个包含开始和停止按钮的用户面板
  2. 在任何给定时间屏幕上都会有多个实例
  3. 当任何人单击开始时,其他所有开始按钮都将被禁用,直到任务完成。

@Erik - 这里发布了很好的建议,但它涉及到一些外部的东西(也许是一个包装类)来管理所有的实例。我的目标是让所有实例彼此独立工作。确实存在一些相互依赖关系,但这存在于类的静态成员中,因此类本身仍然保持独立。

@Rachel - 现在这让我有点不知所措,但随着我了解有关 WPF 的更多信息,我将尝试使用您的建议制定解决方案。

感谢两位的建议。


此解决方案使用我问题中示例中的 XAML(类名更改为 Testcase),但所有工作都在后面的代码中完成。没有数据绑定。

每个类都处理类静态事件。如果任何实例启动或停止,每个类都会处理此事件并启用/禁用自己的按钮。这将所有逻辑保留在类本身中,而无需包装类。

这使用后台工作线程进行缺少中止方法的线程,因此它必须轮询取消。有更好的方法,但由于这是一个 UI 同步问题,而不是一个线程问题,所以我把它作为一个简单的例子。

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();
    }




}

【讨论】:

    猜你喜欢
    • 2020-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多