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

时间:2013-01-03 17:09:29

标签: c# wpf data-binding mvvm

我有一个WPF / MVVM应用程序,它包含一个带有几个按钮的窗口 每个按钮都会触发对外部设备(USB missile launcher)的调用,这需要几秒钟。

当设备运行时,GUI被冻结 这没关系,因为应用程序的唯一目的是调用USB设备,而且当设备移动时你无法做任何其他事情!) < / p>

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

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

我找到了一个符合MVVM标准的解决方案 (至少对我来说......请注意我还是WPF / MVVM的初学者!)

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

我构建了一个最小的工作示例来重现问题(complete demo project here):

这是观点:

<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 from Josh Smith's MSDN article

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上找到类似问题的几个答案)

有什么建议吗?

4 个答案:

答案 0 :(得分:10)

试试这个:

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

答案 1 :(得分:9)

好的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;
    
  5. 应该做的伎俩

答案 2 :(得分: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();
}

答案 3 :(得分:5)

我认为这更优雅:

XAML:

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

C#(使用async&amp; await):

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