19

我有一个 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>

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

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

我也试过:

  • 实现INotifyPropertyChanged(并创建一个属性,并在更改它时this.disableGui调用)OnPropertyChanged
  • CommandManager.InvalidateRequerySuggested()到处打电话
    (我发现在SO上对类似问题的几个答案中)

有什么建议么?

4

4 回答 4

11

尝试这个:

//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();
于 2013-01-04T10:52:08.757 回答
11

好的,该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;
    

应该做的伎俩

于 2013-01-03T17:17:30.563 回答
5

我认为这更优雅一点:

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; 
        }
    }
}
于 2013-10-21T09:04:23.337 回答
5

因为您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();
}
于 2013-01-03T23:30:47.990 回答