19

私はWPF/MVVMアプリを持っています。これは、いくつかのボタンを備えた1つのウィンドウで構成されています。
各ボタンは、外部デバイス(USBミサイルランチャー)への呼び出しをトリガーします。これには数秒かかります。

デバイスの実行中、GUIはフリーズします。
(アプリの唯一の目的はUSBデバイスを呼び出すことであり、デバイスが移動している間は他に何もできないため、これは問題ありません!)

少し醜いのは、デバイスが移動している間、フリーズされたGUIが追加のクリックを受け入れることだけです。
それでもデバイスが動き、同じボタンをもう一度クリックすると、最初の「実行」が終了するとすぐにデバイスが再び動き始めます。

そのため、1つのボタンがクリックされるとすぐに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ですRelayCommandJosh 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;
        }
    }
}

トリガーを開くと、外部ライブラリを呼び出すだけでは発生しないMessageBoxバックグラウンドでのトリガーが発生するのではないかと疑っています。 しかし、私は解決策を見つけることができません。

私も試しました:

  • 実装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

メインスレッドで実行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 に答える
5

これはもう少しエレガントだと思います。

XAML:

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

C#(async&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; 
        }
    }
}
于 2013-10-21T09:04:23.337 に答える