在较长的运行过程中禁用 WPF 按钮,MVVM 方式

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14143683/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 06:52:50  来源:igfitidea点击:

Disable WPF buttons during longer running process, the MVVM way

c#wpfdata-bindingmvvm

提问by Christian Specht

I have a WPF/MVVM app, which consists of one window with a few buttons.
Each of the buttons triggers a call to an external device (an USB missile launcher), which takes a few seconds.

我有一个 WPF/MVVM 应用程序,它由一个带有几个按钮的窗口组成。
每个按钮都会触发对外部设备(USB 导弹发射器)的调用,这需要几秒钟的时间。

While the device is running, the GUI is frozen.
(This is okay, because the only purpose of the app is to call the USB device, and you can't do anything else anyway while the device is moving!)

当设备运行时,GUI 被冻结。
这没关系,因为该应用程序的唯一目的是调用 USB 设备,而在设备移动的过程中,您无论如何也不能做任何其他事情!)

The only thing that's a bit ugly is that the frozen GUI still accepts additional clicks while the device is moving.
When the device still moves and I click on the same button a second time, the device immediately starts moving again as soon as the first "run" is finished.

唯一有点难看的是,当设备移动时,冻结的 GUI 仍然接受额外的点击。
当设备仍然移动并且我第二次单击同一个按钮时,一旦第一次“运行”完成,设备就会立即再次开始移动。

So I'd like to disable all the buttons in the GUI as soon as one button is clicked, and enable them again when the button's command has finished running.

因此,我想在单击一个按钮后立即禁用 GUI 中的所有按钮,并在按钮的命令完成运行后再次启用它们。

I have found a solution for this that looks MVVM-conform.
(at least to me...note that I'm still a WPF/MVVM beginner!)

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

The problem is that this solution doesn't work (as in: the buttons are not disabled) when I call the external library that communicates with the USB device.
But the actual code to disable the GUI is correct, because it doeswork when I replace the external library call by MessageBox.Show().

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

I've constructed a minimal working example that reproduces the problem (complete demo project here):

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

This is the view:

这是视图:

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

...and this is the ViewModel (using the RelayCommandfrom Josh Smith's MSDN article):

...这是 ViewModel (使用RelayCommand来自 Josh Smith 的 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;
        }
    }
}

I'm suspecting that opening a MessageBoxtriggers some stuff in the background that does nothappen when I just call an external library.
But I'm not able to find a solution.

我怀疑打开 aMessageBox会在后台触发一些在我调用外部库时不会发生的事情。
但我无法找到解决方案。

I have also tried:

我也试过:

  • implementing INotifyPropertyChanged(and making this.disableGuia property, and calling OnPropertyChangedwhen changing it)
  • calling CommandManager.InvalidateRequerySuggested()all over the place
    (I found that in several answers to similar problems here on SO)
  • 实现INotifyPropertyChanged(并创建this.disableGui一个属性,并OnPropertyChanged在更改它时调用)
  • CommandManager.InvalidateRequerySuggested()到处打电话
    (我在 SO 上对类似问题的几个答案中发现了这一点)

Any suggestions?

有什么建议?

采纳答案by Lydéric

Try this:

尝试这个:

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

回答by Backlash

Ok the CanExecutemethod will not work because the click will immediately put you into your long-running task.
So here's how I would do it:

好的,该CanExecute方法将不起作用,因为单击会立即将您置于长期运行的任务中。
所以这是我将如何做到的:

  1. Make your view model implement INotifyPropertyChanged

  2. Add a property called something like:

    public bool IsBusy
    {
        get
        {
            return this.isBusy;
        }
        set
        { 
            this.isBusy = value;
            RaisePropertyChanged("IsBusy");
        }
    }
    
  3. Bind your buttons to this property in this manner:

    <Button IsEnabled="{Binding IsBusy}" .. />
    
  4. In your ShowMessage/CallExternal device methods add the line

    IsBusy = true;
    
  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;
    

Should do the trick

应该做的伎俩

回答by Sphinxxx

Because you run CallExternalDevice()on the main thread, the main thread won't have time to update any UI until that job is done, which is why the buttons remain enabled. You could start your long-running operation in a separate thread, and you should see that the buttons are disabled as expected:

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

回答by Stephen Oberauer

I think this is a bit more elegant:

我认为这更优雅一些:

XAML:

XAML:

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

C# (using async & await):

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