How should the ViewModel close the form?
I was inspired by Thejuan's answer to write a simpler attached property. No styles, no triggers; instead, you can just do this:
<Window ... xmlns:xc="clr-namespace:ExCastle.Wpf" xc:DialogCloser.DialogResult="{Binding DialogResult}">
This is almost as clean as if the WPF team had gotten it right and made DialogResult a dependency property in the first place. Just put a bool? DialogResult
property on your ViewModel and implement INotifyPropertyChanged, and voilĂ , your ViewModel can close the Window (and set its DialogResult) just by setting a property. MVVM as it should be.
Here's the code for DialogCloser:
using System.Windows;namespace ExCastle.Wpf{ public static class DialogCloser { public static readonly DependencyProperty DialogResultProperty = DependencyProperty.RegisterAttached( "DialogResult", typeof(bool?), typeof(DialogCloser), new PropertyMetadata(DialogResultChanged)); private static void DialogResultChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { var window = d as Window; if (window != null) window.DialogResult = e.NewValue as bool?; } public static void SetDialogResult(Window target, bool? value) { target.SetValue(DialogResultProperty, value); } }}
I've also posted this on my blog.
From my perspective the question is pretty good as the same approach would be used not only for the "Login" window, but for any kind of window. I've reviewed a lot of suggestions and none are OK for me. Please review my suggestion that was taken from the MVVM design pattern article.
Each ViewModel class should inherit from WorkspaceViewModel
that has the RequestClose
event and CloseCommand
property of the ICommand
type. The default implementation of the CloseCommand
property will raise the RequestClose
event.
In order to get the window closed, the OnLoaded
method of your window should be overridden:
void CustomerWindow_Loaded(object sender, RoutedEventArgs e){ CustomerViewModel customer = CustomerViewModel.GetYourCustomer(); DataContext = customer; customer.RequestClose += () => { Close(); };}
or OnStartup
method of you app:
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); MainWindow window = new MainWindow(); var viewModel = new MainWindowViewModel(); viewModel.RequestClose += window.Close; window.DataContext = viewModel; window.Show(); }
I guess that RequestClose
event and CloseCommand
property implementation in the WorkspaceViewModel
are pretty clear, but I will show them to be consistent:
public abstract class WorkspaceViewModel : ViewModelBase// There's nothing interesting in ViewModelBase as it only implements the INotifyPropertyChanged interface{ RelayCommand _closeCommand; public ICommand CloseCommand { get { if (_closeCommand == null) { _closeCommand = new RelayCommand( param => Close(), param => CanClose() ); } return _closeCommand; } } public event Action RequestClose; public virtual void Close() { if ( RequestClose != null ) { RequestClose(); } } public virtual bool CanClose() { return true; }}
And the source code of the RelayCommand
:
public class RelayCommand : ICommand{ #region Constructors public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } #endregion // Constructors #region ICommand Members [DebuggerStepThrough] public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { _execute(parameter); } #endregion // ICommand Members #region Fields readonly Action<object> _execute; readonly Predicate<object> _canExecute; #endregion // Fields}
P.S. Don't treat me badly for those sources! If I had them yesterday that would have saved me a few hours...
P.P.S. Any comments or suggestions are welcome.
There are a lot of comments arguing the pros and cons of MVVM here. For me, I agree with Nir; it's a matter of using the pattern appropriately and MVVM doesn't always fit. People seems to have become willing to sacrifice all of the most important principles of software design JUST to get it to fit MVVM.
That said,..i think your case could be a good fit with a bit of refactoring.
In most cases I've come across, WPF enables you to get by WITHOUT multiple Window
s. Maybe you could try using Frame
s and Page
s instead of Windows with DialogResult
s.
In your case my suggestion would be have LoginFormViewModel
handle the LoginCommand
and if the login is invalid, set a property on LoginFormViewModel
to an appropriate value (false
or some enum value like UserAuthenticationStates.FailedAuthentication
). You'd do the same for a successful login (true
or some other enum value). You'd then use a DataTrigger
which responds to the various user authentication states and could use a simple Setter
to change the Source
property of the Frame
.
Having your login Window return a DialogResult
i think is where you're getting confused; that DialogResult
is really a property of your ViewModel. In my, admittedly limited experience with WPF, when something doesn't feel right it usually because I'm thinking in terms of how i would've done the same thing in WinForms.
Hope that helps.