16
votes

How can I cancel exiting from particular form after Cancel button (or X at the top right corner, or Esc) was clicked?

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

Current code doesn't work. I want user to stay on current form if it chooses 'No' in popup dialog. Also, overriding CanExecute doesn't help. It just disables the button. I want to allow user to hit the button, but then notify him/her, that data will be lost. Maybe I should assign an event listener on button?

EDIT:

I managed showing popup on Cancel button. But I still can't manage Esc or X button (top right). It seems I was confused with Cancel button, because Execute method is executed when I click X button or Esc.

EDIT2:

I changed the question. It was 'how cancel Cancel button'. However, it wasn't what I was looking for. I need to cancel Esc or X button. In 'MyViewModel' I add:

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

This solved my problem. However, I need ICommand to understand, which button was clicked, Save or Cancel. Is there any way to eliminate usage of event?

4
Does your viewModel.TryClose(false) function sends an event to your view to close the dialog? If so you can remove IsCancel="true" from xaml code. That part causes the form to close.Yusuf Tarık Günaydın
@qqww2 if I remove IsCancel="true" then if I click Esc it doesn't close the window. I want the window to be closed on Esc.Andrii Muzychuk
Register a KeyBinding to your command. Here is an example.Yusuf Tarık Günaydın

4 Answers

18
votes

You are trying to do View's work in ViewModel class. Let your View class to handle the closing request and whether it should be canceled or not.

To cancel closing of a window you can subscribe to the Closing event of view and set CancelEventArgs.Cancel to true after showing a MessageBox.

Here is an example:

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

Code behind:

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

    // OR, if triggering dialog via view-model:

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

I'm not an MVVM expert, but in my opinion Yusufs' answer isn't quite MVVM. On the other hand Torpederos answer is a bit complicated for only close cancellation. Here is my approach. In this example I subscribed to the closing event, but it is always cancelled

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

In the XAML I added this

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>

And finally in the view model

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

In this approach the the closing event is triggered first. That cancels the closing. After that the interaction trigger is invoked and triggers the code in the view model via the RelayCommand. In the view model I can use the Dirty flag that is not accessible in the view.

1
votes

Very good example of doing this in the View Model way can be found in the article of Nish Nishant, where he's using attached properties to hook up window events with commands.

Sample code of attached behaviour (author of the code: 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;
            }
        }
    }
}   

Example how to bind commands:

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

Commands "ClosedCommand", "ClosingCommand" and "CancelClosingCommand" should be defined in the separate View-Model.

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");
    }
}
1
votes

This is another example of canceling the close window directly from ViewModel.

View:

<Window x:Class="WpfApplicationMvvmLight.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform"
    Title="MainWindow" Height="350" Width="525">
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding Path=ClosingCommand}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
    <TextBlock>content...</TextBlock>
</Grid>

ViewModel:

using GalaSoft.MvvmLight.CommandWpf;
using System.ComponentModel;
using System.Windows;

namespace WpfApplicationMvvmLight
{
  class SampleViewModel
  {
    public SampleViewModel() {
      _closingCommand = new RelayCommand<CancelEventArgs>(OnClosingCommand);
    }

    private RelayCommand<CancelEventArgs> _closingCommand;
    public RelayCommand<CancelEventArgs> ClosingCommand {
      get {
        return _closingCommand;
      }
    }

    private void OnClosingCommand(CancelEventArgs e) {
      //display your custom message box here..
      var result = MessageBox.Show("Do you want to close?", "", MessageBoxButton.YesNoCancel);
      //set e.Cancel to true to prevent the window from closing
      e.Cancel = result != MessageBoxResult.Yes;
    }
  }
}

Code behind:

using System.Windows;

namespace WpfApplicationMvvmLight
{
  public partial class MainWindow : Window
  {
    public MainWindow() {
      InitializeComponent();
      this.DataContext = new SampleViewModel();
    }
  }
}

This is the reference. MVVM close window event