How Should the Viewmodel Close the Form

Close Window from ViewModel

You can pass the window to your ViewModel using the CommandParameter. See my Example below.

I've implemented an CloseWindow Method which takes a Windows as parameter and closes it. The window is passed to the ViewModel via CommandParameter. Note that you need to define an x:Name for the window which should be close. In my XAML Window i call this method via Command and pass the window itself as a parameter to the ViewModel using CommandParameter.

Command="{Binding CloseWindowCommand, Mode=OneWay}" 
CommandParameter="{Binding ElementName=TestWindow}"

ViewModel

public RelayCommand<Window> CloseWindowCommand { get; private set; }

public MainViewModel()
{
this.CloseWindowCommand = new RelayCommand<Window>(this.CloseWindow);
}

private void CloseWindow(Window window)
{
if (window != null)
{
window.Close();
}
}

View

<Window x:Class="ClientLibTestTool.ErrorView"
x:Name="TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:localization="clr-namespace:ClientLibTestTool.ViewLanguages"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="{x:Static localization:localization.HeaderErrorView}"
Height="600" Width="800"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen">
<Grid>
<Button Content="{x:Static localization:localization.ButtonClose}"
Height="30"
Width="100"
Margin="0,0,10,10"
IsCancel="True"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Command="{Binding CloseWindowCommand, Mode=OneWay}"
CommandParameter="{Binding ElementName=TestWindow}"/>
</Grid>
</Window>

Note that i'm using the MVVM light framework, but the principal applies to every wpf application.

This solution violates of the MVVM pattern, because the view-model shouldn't know anything about the UI Implementation. If you want to strictly follow the MVVM programming paradigm you have to abstract the type of the view with an interface.

MVVM conform solution (Former EDIT2)

the user Crono mentions a valid point in the comment section:

Passing the Window object to the view model breaks the MVVM pattern
IMHO, because it forces your vm to know what it's being viewed in.

You can fix this by introducing an interface containing a close method.

Interface:

public interface ICloseable
{
void Close();
}

Your refactored ViewModel will look like this:

ViewModel

public RelayCommand<ICloseable> CloseWindowCommand { get; private set; }

public MainViewModel()
{
this.CloseWindowCommand = new RelayCommand<IClosable>(this.CloseWindow);
}

private void CloseWindow(ICloseable window)
{
if (window != null)
{
window.Close();
}
}

You have to reference and implement the ICloseable interface in your view

View (Code behind)

public partial class MainWindow : Window, ICloseable
{
public MainWindow()
{
InitializeComponent();
}
}

Answer to the original question: (former EDIT1)

Your Login Button (Added CommandParameter):

<Button Name="btnLogin" IsDefault="True" Content="Login" Command="{Binding ShowLoginCommand}" CommandParameter="{Binding ElementName=LoginWindow}"/>

Your code:

 public RelayCommand<Window> CloseWindowCommand { get; private set; } // the <Window> is important for your solution!

public MainViewModel()
{
//initialize the CloseWindowCommand. Again, mind the <Window>
//you don't have to do this in your constructor but it is good practice, thought
this.CloseWindowCommand = new RelayCommand<Window>(this.CloseWindow);
}

public bool CheckLogin(Window loginWindow) //Added loginWindow Parameter
{
var user = context.Users.Where(i => i.Username == this.Username).SingleOrDefault();

if (user == null)
{
MessageBox.Show("Unable to Login, incorrect credentials.");
return false;
}
else if (this.Username == user.Username || this.Password.ToString() == user.Password)
{
MessageBox.Show("Welcome "+ user.Username + ", you have successfully logged in.");
this.CloseWindow(loginWindow); //Added call to CloseWindow Method
return true;
}
else
{
MessageBox.Show("Unable to Login, incorrect credentials.");
return false;
}
}

//Added CloseWindow Method
private void CloseWindow(Window window)
{
if (window != null)
{
window.Close();
}
}

WPF (MVVM): Closing a view from Viewmodel?

Edit: See my blog post for a more detailed explanation.

When I need to achieve that, I use a IRequestCloseViewModel interface that I created.

This interface contains only one event: RequestClose. This event is raised by the ViewModel (which inherits from a ViewModelBase class AND implement IRequestCloseViewModel) when it wants to close its associated view.

In my app, all Window inherit from an abstract class ApplicationWindow. This abstract class is notified each time the DataContext changed and in the handler checks if the DataContext support the IRequestCloseViewModel. If this is the case, an event handler is set up to close the Window when the event is fired.

Alternatively, like Kent said, you can use screen controller that handle this mecanism in an external class.

MVVM - Close a window from view model

Closing the window is the view's responsibility, the view-model should know nothing about it. No wonder you're getting tangled up. If you don't need the window to stay on screen while the "button logic" is running, then just use a CompositeCommand from Prism (I don't like it much because it uses code-behind, but no matter) or equivalent to bind two commands to the button. On the other hand, if you need to keep the window on screen while the "button logic" is running, e.g. to display progress, and if your view model reflects this, then you can add a bool IsButtonLogicComplete property to your view model (don't forget INotifyPropertyChanged) and bind the window's 'closed' state to this property via an attached property/behavior like this one:

public static class AttachedProperties
{
// in Visual Studio, the `propa` snippet inserts the boilerplate
public static DependencyProperty ForceCloseProperty =
DependencyProperty.RegisterAttached ("ForceClose",
typeof (bool), typeof (AttachedProperties), new UIPropertyMetadata (false, (d, e) =>
{
var w = d as Window ;
if (w != null && (bool) e.NewValue)
{
w.DialogResult = true ;
w.Close () ;
}
})) ;

public static bool GetForceClose (DependencyObject obj)
{
return (bool) obj.GetValue (ForceCloseProperty) ;
}

public static void SetForceClose (DependencyObject obj, bool value)
{
obj.SetValue (ForceCloseProperty, value) ;
}
}

<!-- in .xaml -->
<Window
xmlns:local="clr-namespace:YourNamespace"
local:AttachedProperties.ForceClose="{Binding IsButtonLogicComplete}" ...

This will keep your view and your view model concerns nicely separated.

Bind ViewModel method to View's Alt+F4 and Close button

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:

  • MVVM - Commands, RelayCommands and EventToCommand

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. Are you sure you want to close this form?"/>
</b:Interaction.Behaviors>
<!-- ...other markup. -->
</Window>

Do not be confused by the characters, those are newlines (\n) in XML.

Close a view from a viewmodel in MVVM

You fire the RequestClose event in the ViewModel constructor which is too early to be catched by the event registration.



Related Topics



Leave a reply



Submit