Отключить кнопки WPF во время более длительной работы, путь MVVM - программирование

Отключить кнопки WPF во время более длительной работы, путь MVVM

У меня есть приложение WPF/MVVM, состоящее из одного окна с несколькими кнопками.
Каждая из кнопок вызывает вызов внешнего устройства (USB-ракета-носитель), который занимает несколько секунд.

Пока устройство работает, графический интерфейс заморожен.
( Это нормально, потому что единственная цель приложения - вызвать USB-устройство, и вы все равно ничего не сделаете, пока устройство движется!)

Единственное, что немного уродливо, заключается в том, что замороженный 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 из статьи 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)

Любые предложения?

4b9b3361

Ответ 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();

Ответ 2

О'кей, метод CanExecute не будет работать, потому что щелчок сразу поместит вас в вашу долговременную задачу.
Итак, вот как я это сделаю:

  • Сделайте свою модель модели представления INotifyPropertyChanged

  • Добавьте свойство, которое называется:

    public bool IsBusy
    {
        get
        {
            return this.isBusy;
        }
        set
        { 
            this.isBusy = value;
            RaisePropertyChanged("IsBusy");
        }
    }
    
  • Привяжите свои кнопки к этому свойству следующим образом:

    <Button IsEnabled="{Binding IsBusy}" .. />
    
  • В методах устройства ShowMessage/CallExternal добавьте строку

    IsBusy = true;
    

Должен сделать трюк

Ответ 3

Поскольку вы запускаете CallExternalDevice() в основном потоке, основной поток не будет иметь времени для обновления любого пользовательского интерфейса до тех пор, пока это задание не будет выполнено, поэтому кнопки остаются включенными. Вы можете запустить свою длительную операцию в отдельном потоке, и вы должны увидеть, что кнопки отключены, как ожидалось:

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

Ответ 4

Я думаю, что это немного более элегантно:

XAML:

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

С# (с использованием async и ожидания):

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