web-dev-qa-db-ja.com

MVVM WPFアプリケーションでウィンドウのクローズをキャンセルする方法

[キャンセル]ボタン(または右上隅のX、またはEsc)をクリックした後、特定のフォームの終了をキャンセルするにはどうすればよいですか?

WPF:

<Window
  ...
  x:Class="MyApp.MyView"
  ...
/>
  <Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True"/>
</Window>

ViewModel:

public class MyViewModel : Screen {
  private CancelCommand cancelCommand;
  public CancelCommand CancelCommand {
    get { return cancelCommand; }
  }
  public MyViewModel() {
    cancelCommand = new CancelCommand(this);
  }
}

public class CancelCommand : ICommand {

  public CancelCommand(MyViewModel viewModel) {
    this.viewModel = viewModel;
  }

  public override void Execute(object parameter) {
    if (true) { // here is a real condition
      MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show(
        "Really close?",  "Warning", 
        System.Windows.MessageBoxButton.YesNo);
      if (messageBoxResult == MessageBoxResult.No) { return; }
    }
    viewModel.TryClose(false);
  }

  public override bool CanExecute(object parameter) {
    return true;
  }
}

現在のコードは機能しません。ポップアップダイアログで[いいえ]を選択した場合、ユーザーが現在のフォームに留まるようにしたいまた、CanExecuteをオーバーライドしても効果がありません。ボタンを無効にするだけです。ユーザーがボタンを押すことを許可しますが、データが失われることをユーザーに通知します。多分私はボタンにイベントリスナーを割り当てる必要がありますか?

編集:

[キャンセル]ボタンにポップアップを表示するようにしました。しかし、私はまだEscまたはXボタン(右上)を管理できません。 XボタンまたはEscをクリックするとExecuteメソッドが実行されるため、キャンセルボタンと混同されたようです。

EDIT2:

質問を変更しました。 「キャンセルボタンをキャンセルする方法」でした。しかし、それは私が探していたものではありませんでした。 EscまたはXボタンをキャンセルする必要があります。 「MyViewModel」に追加します。

        protected override void OnViewAttached(object view, object context) {
            base.OnViewAttached(view, context);
            (view as MyView).Closing += MyViewModel_Closing;
        }

        void MyViewModel_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
            if (true) {
                MessageBoxResult messageBoxResult = System.Windows.MessageBox.Show(
                  "Really close?",  "Warning", 
                  System.Windows.MessageBoxButton.YesNo);
                if (messageBoxResult == MessageBoxResult.No) {
                    e.Cancel = true;
                }
            }
        }

これは私の問題を解決しました。ただし、ICommandには、どのボタンがクリックされたか、保存するかキャンセルするかを理解する必要があります。イベントの使用を排除する方法はありますか?

13
Andrii Muzychuk

ViewModelクラスでビューの作業を実行しようとしています。 Viewクラスで終了リクエストを処理し、キャンセルするかどうかを決定します。

ウィンドウのクローズをキャンセルするには、ビューのClosingイベントをサブスクライブし、MessageBoxを表示した後でCancelEventArgs.Cancelをtrueに設定します。

次に例を示します。

<Window
    ...
    x:Class="MyApp.MyView"
    Closing="OnClosing"
    ...
/>
</Window>

コードビハインド:

private void OnClosing(object sender, CancelEventArgs e)
{
    MessageBoxResult result = MessageBox.Show("Really close?",  "Warning", MessageBoxButton.YesNo);
    if (result != MessageBoxResult.Yes)
    {
        e.Cancel = true;
    }

    bool shouldClose = ((MyViewModel) DataContext).TryClose(false);
    if(!shouldClose)
    {
        e.Cancel = true;
    }
}

モデルを表示する方法でこれを行う非常に良い例を見つけることができます Nish Nishantの記事 で、アタッチされたプロパティを使用して、コマンドでウィンドウイベントをフックしています。

添付された動作のサンプルコード(コードの作成者: Nish Nishant

public class WindowClosingBehavior {

    public static ICommand GetClosed(DependencyObject obj) {
        return (ICommand)obj.GetValue(ClosedProperty);
    }

    public static void SetClosed(DependencyObject obj, ICommand value) {
        obj.SetValue(ClosedProperty, value);
    }

    public static readonly DependencyProperty ClosedProperty 
        = DependencyProperty.RegisterAttached(
        "Closed", typeof(ICommand), typeof(WindowClosingBehavior),
        new UIPropertyMetadata(new PropertyChangedCallback(ClosedChanged)));

    private static void ClosedChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) {

        Window window = target as Window;

        if (window != null) {

            if (e.NewValue != null) {
                window.Closed += Window_Closed;
            }
            else {
                window.Closed -= Window_Closed;
            }
        }
    }

    public static ICommand GetClosing(DependencyObject obj) {
        return (ICommand)obj.GetValue(ClosingProperty);
    }

    public static void SetClosing(DependencyObject obj, ICommand value) {
        obj.SetValue(ClosingProperty, value);
    }

    public static readonly DependencyProperty ClosingProperty 
        = DependencyProperty.RegisterAttached(
        "Closing", typeof(ICommand), typeof(WindowClosingBehavior),
        new UIPropertyMetadata(new PropertyChangedCallback(ClosingChanged)));

    private static void ClosingChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) {

        Window window = target as Window;

        if (window != null) {

            if (e.NewValue != null) {
                window.Closing += Window_Closing;
            }
            else {
                window.Closing -= Window_Closing;
            }
        }
    }

    public static ICommand GetCancelClosing(DependencyObject obj) {
        return (ICommand)obj.GetValue(CancelClosingProperty);
    }

    public static void SetCancelClosing(DependencyObject obj, ICommand value) {
        obj.SetValue(CancelClosingProperty, value);
    }

    public static readonly DependencyProperty CancelClosingProperty 
        = DependencyProperty.RegisterAttached(
        "CancelClosing", typeof(ICommand), typeof(WindowClosingBehavior));

    static void Window_Closed(object sender, EventArgs e) {

        ICommand closed = GetClosed(sender as Window);

        if (closed != null) {
            closed.Execute(null);
        }
    }

    static void Window_Closing(object sender, CancelEventArgs e) {

        ICommand closing = GetClosing(sender as Window);

        if (closing != null) {

            if (closing.CanExecute(null)) {
                closing.Execute(null);
            }
            else {

                ICommand cancelClosing = GetCancelClosing(sender as Window);

                if (cancelClosing != null) {
                    cancelClosing.Execute(null);
                }

                e.Cancel = true;
            }
        }
    }
}   

コマンドをバインドする方法の例:

<Window 
    x:Class="WindowClosingDemo.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:nsmvvm="clr-namespace:NS.MVVM"
    nsmvvm:WindowClosingBehavior.Closed="{Binding ClosedCommand}"
    nsmvvm:WindowClosingBehavior.Closing="{Binding ClosingCommand}"
    nsmvvm:WindowClosingBehavior.CancelClosing="{Binding CancelClosingCommand}">

コマンド「ClosedCommand」、「ClosingCommand」、および「CancelClosingCommand」は、別のビューモデルで定義する必要があります。

internal class MainViewModel : ViewModelBase {

    private ObservableCollection<string> log = new ObservableCollection<string>();

    public ObservableCollection<string> Log {
        get { return log; }
    }

    private DelegateCommand exitCommand;

    public ICommand ExitCommand {

        get {

            if (exitCommand == null) {
                exitCommand = new DelegateCommand(Exit);
            }

            return exitCommand;
        }
    }

    private void Exit() {
        Application.Current.Shutdown();
    }

    private DelegateCommand closedCommand;

    public ICommand ClosedCommand {

        get {

            if (closedCommand == null) {
                closedCommand = new DelegateCommand(Closed);
            }

            return closedCommand;
        }
    }

    private void Closed() {
        log.Add("You won't see this of course! Closed command executed");
        MessageBox.Show("Closed");
    }

    private DelegateCommand closingCommand;

    public ICommand ClosingCommand {

        get {

            if (closingCommand == null) {
                closingCommand = new DelegateCommand(ExecuteClosing, CanExecuteClosing);
            }

            return closingCommand;
        }
    }

    private void ExecuteClosing() {
        log.Add("Closing command executed");
        MessageBox.Show("Closing");
    }

    private bool CanExecuteClosing() {

        log.Add("Closing command execution check");

        return MessageBox.Show("OK to close?", "Confirm", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
    }

    private DelegateCommand cancelClosingCommand;

    public ICommand CancelClosingCommand {

        get {

            if (cancelClosingCommand == null) {
                cancelClosingCommand = new DelegateCommand(CancelClosing);
            }

            return cancelClosingCommand;
        }
    }

    private void CancelClosing() {
        log.Add("CancelClosing command executed");
        MessageBox.Show("CancelClosing");
    }
}
0
torpederos

私はMVVMのエキスパートではありませんが、私の意見では、Yusufsの答えはMVVMではありません。一方、Torpederosの解答は、厳密にキャンセルするだけでは少し複雑です。これが私のアプローチです。この例では、クロージングイベントをサブスクライブしましたが、常にキャンセルされます

private void OnClosing(object sender, CancelEventArgs e)
{
    e.Cancel = true;
    return;
}

XAMLでこれを追加しました

xmlns:i="http://schemas.Microsoft.com/expression/2010/interactivity"
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <i:InvokeCommandAction Command="{Binding Close}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

そして最後にビューモデルで

public ICommand Close { get; set; }
Close = new RelayCommand(CommandClose);
private void CommandClose(object sender)
{
    if (Dirty)
    {
        // Save your data here
    }
    Environment.Exit(0);
}

このアプローチでは、終了イベントが最初にトリガーされます。それは閉鎖をキャンセルします。その後、相互作用トリガーが呼び出され、RelayCommandを介してビューモデルのコードをトリガーします。ビューモデルでは、ビューではアクセスできないDirtyフラグを使用できます。

0
DaanH