-1

I have created a custom method to close a form in my view model and bound it to a button. How do I get the default close button in the title bar and Alt+F4 to run the same command?

public void Close(object parameter)
{
    // Check if any field has been edited
    if (IsDirty())
    {
        string message = "You have unsaved changes.\n\nAre you sure you want to close this form?";
        string title = "Close Window";
        MessageBoxResult result = MessageBox.Show(message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);

        if (result == MessageBoxResult.Cancel)
            return;
    }

    employeeWindow.Close();
}

I'm still new to WPF and MVVM, and I'm just not understanding any of the solutions already out there or how I can customise them to run the above code.

thatguy
  • 21,059
  • 6
  • 30
  • 40
smally
  • 453
  • 1
  • 4
  • 14
  • [WPF window close button binding](https://www.google.com/search?q=wpf+window+close+button+binding+site:stackoverflow.com&client=firefox-b-d&sa=X&ved=2ahUKEwjk-bSen9HuAhVtwIsKHd_BBXQQrQIoBHoECAUQBQ&biw=1920&bih=966) – Pavel Anikhouski Feb 04 '21 at 22:09
  • Does this answer your question? [How to bind Close command to a button](https://stackoverflow.com/questions/1065887/how-to-bind-close-command-to-a-button) – LoRdPMN Feb 05 '21 at 11:53

1 Answers1

1

What you are looking for is the Closing event on Window. Its CancelEventArgs carry a Cancel property of type bool that can be set to true, which will cancel closing the window. The closing event will both be fired for closing a window using the close button and pressing Alt+F4.

Code-Behind

In a code-behind scenario, you would add an event handler in XAML like this.

<Window Closing="OnClosing" ...>

Then, you would create this event handler in code-behind and set the Cancel property accordingly.

private void OnClosing(object sender, CancelEventArgs e)
{
   // Check if any field has been edited
   if (IsDirty())
   {
      string message = "You have unsaved changes.\n\nAre you sure you want to close this form?";
      string title = "Close Window";
      MessageBoxResult result = MessageBox.Show(message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);

      if (result == MessageBoxResult.Cancel)
         e.Cancel = true;
   }
}

Event Trigger (Non MVVM)

In MVVM you could install the Microsoft.Xaml.Behaviors.Wpf NuGet package, which is a replacement of the legacy Blend behaviors (System.Windows.Interactivity). You can use an EventTrigger to bind the event to a command in your view model. In this example, you pass the CancelEventArgs directly to the command.

<Window ...>
   <b:Interaction.Triggers>
      <b:EventTrigger EventName="Closing">
         <b:InvokeCommandAction Command="{Binding ClosingCommand}"
                                PassEventArgsToCommand="True"/>
      </b:EventTrigger>
   </b:Interaction.Triggers>
   <!-- ...other markup. -->
</Window>

This solution allows you to define a command in your view model.

public class MyViewModel
{
   public MyViewModel()
   {
      ClosingCommand = new RelayCommand<CancelEventArgs>(ExecuteClosing);
   }

   public ICommand ClosingCommand { get; }

private void ExecuteClosing(CancelEventArgs e)
   {
      string message = "You have unsaved changes.\n\nAre you sure you want to close this form?";
      string title = "Close Window";
      MessageBoxResult result = MessageBox.Show(message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
   
      if (result == MessageBoxResult.Cancel)
         e.Cancel = true;
   }
}

I do not provide an ICommand implementation here. For more information on the basics refer to:

Although this solution uses a command on the view model, it is not MVVM compliant, since the message box is a view component that must not reside in a view model. The same applies to the cancel event args.

MVVM Behavior

An MVVM compliant way could be to create a behavior to move the confirmation code out. For that create an interface for your view model that contains the IsDirty method and implement it in your view model.

public interface IStatefulViewModel
{
   bool IsDirty();
}
public class MyViewModel : IStatefulViewModel
{
   // ...your code.

   public bool IsDirty()
   {
      // ...your checks.
   }
}

Then, create a behavior using the Microsoft.Xaml.Behaviors.Wpf NuGet package. This behavior is reusable and encapsulates the closing logic decoupled from your view model. The Caption and Message dependency properties allow binding the message box contents.

public class WindowClosingBehavior : Behavior<Window>
{
   public static readonly DependencyProperty CaptionProperty = DependencyProperty.Register(
      nameof(Caption), typeof(string), typeof(WindowClosingBehavior), new PropertyMetadata(string.Empty));

   public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
      nameof(Message), typeof(string), typeof(WindowClosingBehavior), new PropertyMetadata(string.Empty));

   public string Caption
   {
      get => (string)GetValue(CaptionProperty);
      set => SetValue(CaptionProperty, value);
   }

   public string Message
   {
      get => (string)GetValue(MessageProperty);
      set => SetValue(MessageProperty, value);
   }

   protected override void OnAttached()
   {
      base.OnAttached();
      AssociatedObject.Closing += OnClosing;
   }

   protected override void OnDetaching()
   {
      base.OnDetaching();
      AssociatedObject.Closing -= OnClosing;
   }

   private void OnClosing(object sender, CancelEventArgs e)
   {
      if (!(AssociatedObject.DataContext is IStatefulViewModel statefulViewModel))
         return;

      if (!statefulViewModel.IsDirty())
         return;

      e.Cancel = ConfirmClosing();
   }

   private bool ConfirmClosing()
   {
      var result = MessageBox.Show(
         Message,
         Caption,
         MessageBoxButton.OKCancel,
         MessageBoxImage.Warning,
         MessageBoxResult.Cancel);

      return result == MessageBoxResult.Cancel;
   }
}

Attach the behavior to your window. Note that you can do this on any window.

<Window ...>
   <b:Interaction.Behaviors>
      <local:WindowClosingBehavior Caption="Close Window"
                                   Message="You have unsaved changes.&#13;&#10;&#13;&#10;Are you sure you want to close this form?"/>
   </b:Interaction.Behaviors>
   <!-- ...other markup. -->
</Window>

Do not be confused by the &#13;&#10; characters, those are newlines (\n) in XML.

thatguy
  • 21,059
  • 6
  • 30
  • 40
  • Brilliant, that's everything that I'm after, I followed your MVVM Behaviour one. And thank you for explaining them. – smally Feb 05 '21 at 21:19
  • What do you mean by 'message box is a view component that must not reside in a view model'. My view is created by the view model, because if the view model fails to load the employee from the database I don't want the view to be created or shown. Is this not MVVM compliant? – smally Feb 05 '21 at 21:20
  • @smally MVVM is about spearating your user interface from the business logic. This implies that you must not have any view component references in your view model, be it a `Window`, a `UserControl` or any other control. So no, this is not MVVM compliant. – thatguy Feb 08 '21 at 09:17