基于线程状态的绑定

时间:2011-12-01 18:58:25

标签: wpf data-binding

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

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

所以我想启用&基于2个属性禁用: 1.私有后台工作器实例变量是否为空 2.公共静态对象是否具有锁(使用Monitor.Enter或lock获得)

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

'开始'按钮: - 如果公共对象未锁定(表示没有线程正在运行),则启用,否则禁用(至少一个线程,可能是此类中的一个正在运行)

'停止'按键 - 如果私有后台工作程序不为null(此类中的线程正在启​​动/运行),则启用,否则禁用(不适用于停止的线程)

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

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

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

使用两个按钮创建一个userpanel(没有为停止按钮实现绑定)

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

将多个此实例放入窗口

<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绑定。

一个突出的问题是如何使所有启动按钮禁用而不是只启用一个。我正在锁定一个静态对象(目前还没有正常工作,正在研究它)

希望这个例子有帮助

3 个答案:

答案 0 :(得分:2)

我可能会将我的按钮绑定到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

的代码

我通常也会使用RelayCommand DelegateCommand版本的CanExecute(),您也可以自己制作。我也可以使用PRISM的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 } 。 {{1}}的基本定义如下:

{{1}}

答案 1 :(得分:1)

如果不确切知道你的绑定结构是什么(代码隐藏,查看模型等),我建议你忘记弄清楚如何使GUI / WPF理解你的底层对象模型,并专注于使你的代码容易使用XAML。

也就是说,不要花时间弄清楚如何绑定XAML以确定某些内容是否为空以及是否锁定了其他内容。相反,从绑定目标公开属性,将属性解析为对象所需的属性。

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属性绑定到boolean属性,当您启动任务时,该属性将设置为false,然后在任务完成时将其设置为true。属性设置器将触发GUI的PropertyChanged,它将按钮更新回启用。

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

答案 2 :(得分:1)

我不想回答我自己的问题,但我有一个特定的情况,我试图得到。

  1. 一个类的每个实例都是一个包含start&amp; amp;的用户面板。停止按钮
  2. 在任何给定时间屏幕上都会有多个实例
  3. 当任何人点击开始时,每个其他开始按钮都将被禁用,直到任务完成。
  4. @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();
        }
    
    
    
    
    }