在长时间运行过程中禁用WPF按键,MVVM方式(Disable WPF buttons during

2019-07-17 19:11发布

我有一个WPF / MVVM应用程序,它由一个窗口有几个按钮。
每个按钮的触发到外部装置的呼叫(一个USB导弹发射 ),它需要几秒钟。

当设备运行时,GUI被冻结。
这是正常的 ,因为应用程序的唯一目的就是调用USB设备,并在设备移动你不能做别的呢!)

这是一个有点难看的唯一的事情是,冷冻GUI仍接受额外的点击,而设备在移动。
当设备依然继续和我同一个按钮上点击第二次时,设备会立即重新开始,一旦第一个“运行”完成移动。

所以我想在GUI尽快禁用所有的按钮作为一个按钮被点击,并再次启用它们时按钮的命令已经完成运行。

我已经找到了一个解决方案,看起来MVVM-一致。
(至少对我来说...请注意,我仍然是一个WPF / MVVM初学者!)

问题是,这种解决方案不工作(如:按钮没有被禁用)当我打电话,与USB设备通信的外部库。
但实际的代码来禁用GUI是正确的,因为它工作,当我更换由外部库调用MessageBox.Show()

我已经构建了(重现问题的最小工作示例这里完整的演示项目 ):

这是视图:

<Window x:Class="WpfDatabindingQuestion.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <Button Content="MessageBox" Command="{Binding MessageCommand}" Height="50"></Button>
            <Button Content="Simulate external device" Command="{Binding DeviceCommand}" Height="50" Margin="0 10"></Button>
        </StackPanel>
    </Grid>
</Window>

......这是(使用视图模型RelayCommand从约什-史密斯的MSDN文章 ):

using System.Threading;
using System.Windows;
using System.Windows.Input;

namespace WpfDatabindingQuestion
{
    public class MainWindowViewModel
    {
        private bool disableGui;

        public ICommand MessageCommand
        {
            get
            {
                return new RelayCommand(this.ShowMessage, this.IsGuiEnabled);
            }
        }

        public ICommand DeviceCommand
        {
            get
            {
                return new RelayCommand(this.CallExternalDevice, this.IsGuiEnabled);
            }
        }

        // here, the buttons are disabled while the MessageBox is open
        private void ShowMessage(object obj)
        {
            this.disableGui = true;
            MessageBox.Show("test");
            this.disableGui = false;
        }

        // here, the buttons are NOT disabled while the app pauses
        private void CallExternalDevice(object obj)
        {
            this.disableGui = true;
            // simulate call to external device (USB missile launcher),
            // which takes a few seconds and pauses the app
            Thread.Sleep(3000);
            this.disableGui = false;
        }

        private bool IsGuiEnabled(object obj)
        {
            return !this.disableGui;
        }
    }
}

我怀疑打开一个MessageBox触发一些东西时,我只是调用外部库, 不会发生的背景。
但我不能够找到一个解决方案。

我也曾尝试:

  • 实施INotifyPropertyChanged (和制作this.disableGui的属性,并调用OnPropertyChanged时改变它)
  • 调用CommandManager.InvalidateRequerySuggested()所有的地方
    (我发现几个答案类似的问题在这里SO)

有什么建议?

Answer 1:

试试这个:

//Declare a new BackgroundWorker
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += (o, ea) =>
{
    try
    {
        // Call your device

        // If ou need to interact with the main thread
       Application.Current.Dispatcher.Invoke(new Action(() => //your action));
    }
    catch (Exception exp)
    {
    }
};

//This event is raise on DoWork complete
worker.RunWorkerCompleted += (o, ea) =>
{
    //Work to do after the long process
    disableGui = false;
};

disableGui = true;
//Launch you worker
worker.RunWorkerAsync();


Answer 2:

OK了CanExecute方法是行不通的,因为点击会立即把你带入您的长时间运行的任务。
因此,这里是我会怎么做:

  1. 让您的视图模型执行INotifyPropertyChanged

  2. 添加一个名为类似属性:

     public bool IsBusy { get { return this.isBusy; } set { this.isBusy = value; RaisePropertyChanged("IsBusy"); } } 
  3. 绑定你的按钮,该物业以这种方式:

     <Button IsEnabled="{Binding IsBusy}" .. /> 
  4. 在您的ShowMessage / CallExternal设备的方法添加一行

     IsBusy = true; 

应该做的伎俩



Answer 3:

我觉得这是一个比较优雅:

XAML:

<Button IsEnabled="{Binding IsGuiEnabled}" Content="Simulate external device" Command="{Binding DeviceCommand}" Height="50" Margin="0 10"></Button>

C#(使用异步&等待):

public class MainWindowViewModel : INotifyPropertyChanged
{
    private bool isGuiEnabled;

    /// <summary>
    /// True to enable buttons, false to disable buttons.
    /// </summary>
    public bool IsGuiEnabled 
    {
        get
        {
            return isGuiEnabled;
        }
        set
        {
            isGuiEnabled = value;
            OnPropertyChanged("IsGuiEnabled");
        }
    }

    public ICommand DeviceCommand
    {
        get
        {
            return new RelayCommand(this.CallExternalDevice, this.IsGuiEnabled);
        }
    }

    private async void CallExternalDevice(object obj)
    {
        IsGuiEnabled = false;
        try
        {
            await Task.Factory.StartNew(() => Thread.Sleep(3000));
        }
        finally
        {
            IsGuiEnabled = true; 
        }
    }
}


Answer 4:

因为你运行CallExternalDevice()在主线程,主线程没有足够的时间,直到工作已经完成,这就是为什么按钮保持启用状态更新任何UI。 你可以在一个单独的线程启动长时间运行的操作,你应该看到的是,按钮按预期被禁用:

private void CallExternalDevice(object obj)
{
    this.disableGui = true;

    ThreadStart work = () =>
    {
        // simulate call to external device (USB missile launcher),
        // which takes a few seconds and pauses the app
        Thread.Sleep(3000);

        this.disableGui = false;
        Application.Current.Dispatcher.BeginInvoke(new Action(() => CommandManager.InvalidateRequerySuggested()));
    };
    new Thread(work).Start();
}


文章来源: Disable WPF buttons during longer running process, the MVVM way