5

Imagine you want a Save & Close and a Cancel & Close button on your fancy WPF MVVM window?

How would you go about it? MVVM dictates that you bind the button to an ICommand and inversion of control dictates that your View may know your ViewModel but not the other way around.

Poking around the net I found a solution that has a ViewModel closing event to which the View subscribes to like this:

private void OnLoaded(Object sender
    , RoutedEventArgs e)
{
    IFilterViewModel viewModel = (IFilterViewModel)DataContext;
    viewModel.Closing += OnViewModelClosing;
}

private void OnViewModelClosing(Object sender
    , EventArgs<Result> e)
{
    IFilterViewModel viewModel = (IFilterViewModel)DataContext;
    viewModel.Closing -= OnViewModelClosing;
    DialogResult = (e.Value == Result.OK) ? true : false;
    Close();
}

But that is code-behind mixed in with my so far very well designed MVVM.

Another problem would be showing a licensing problem message box upon showing the main window. Again I could use the Window.Loaded event like I did above, but that's also breaking MVVM, is it not?

Is there a clean way or should one be pragmatical instead of pedantic in these cases?

Dee J. Doena
  • 1,631
  • 3
  • 16
  • 26
  • 4
    "MVVM dictates that you bind the button to an ICommand" Does it? You could still add a Click handler without breaking anything. Be pragmatic. – Clemens Oct 26 '16 at 08:50
  • mvvm != no codebehind. On the other hand, the pattern serves us, we do not serve the pattern. –  Oct 26 '16 at 14:18

5 Answers5

8

First, create an interface that contains only the Close method:

interface IClosable
{
    void Close();
}

Next, make your window implement IClosable:

class MyWindow : Window, IClosable
{
    public MyWindow()
    {
        InitializeComponent();
    }
}

Then let the view pass itself as IClosable as command parameter to the view model:

<Button Command="{Binding CloseCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

And lastly, the command calls Close:

CloseCommand = new DelegateCommand<IClosable>( view => view.Close() );

And what have we now?

  • we have a button that closes the window
  • we have no code in code-behind except , IClosable
  • the view model knows nothing about the view, it just gets an arbitrary object that can be closed
  • the command can easily be unit tested
Haukinger
  • 10,420
  • 2
  • 15
  • 28
  • But what if the user closes the windows in anther way, e.g. using the OS's window manger..... (X in corner of window on NT.) – Ian Ringrose Oct 26 '16 at 12:09
  • That's not part of the original question, but it would be done through an `EventTrigger` using an `InvokeCommandAction` and then the same command... – Haukinger Oct 26 '16 at 15:09
  • Or just mark the button as the close button in the markup and put a simple one line event handler in the code behind with the VM exposing a Close() method then view can call. – Ian Ringrose Oct 26 '16 at 15:18
1

There is nothing wrong or right with using code behind, this is mainly opinion based and depends on your preference.

This example shows how to close a window using an MVVM design pattern without code behind.

<Button Name="btnLogin" IsDefault="True" Content="Login" Command="{Binding ShowLoginCommand}" CommandParameter="{Binding ElementName=LoginWindow}"/> 
<!-- the CommandParameter should bind to your window, either by name or relative or what way you choose, this will allow you to hold the window object and call window.Close() -->

basically you pass the window as a parameter to the command. IMO your viewmodel shouldn't be aware of the control, so this version is not that good. I would pass a Func<object>/ some interface to the viewmodel for closing the window using dependency injection.

Community
  • 1
  • 1
gilmishal
  • 1,884
  • 1
  • 22
  • 37
0

Take a look at some toolkits e.g. MVVMLight has EventToCommand, which allows you to bind command to events. I generally try my best to limit logic in View, as it's harder to test it.

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:command="http://www.galasoft.ch/mvvmlight"

...

<i:Interaction.Triggers>
   <i:EventTrigger EventName="Loaded">
     <command:EventToCommand Command="{Binding YourCommandInVM}"/>
   </i:EventTrigger>
</i:Interaction.Triggers>
DevNewb
  • 841
  • 7
  • 19
  • Thanks, that would help with the problem of the message box. But an entire toolkit dependency for one feature is a heavy price to pay. :-( – Dee J. Doena Oct 26 '16 at 08:52
  • @Dee J. Doena Completelly understand, but 1) it's got other usefull features too 2) what it's doing is not magic, and I think it's open source so you could take a look at the source code and see what's the hard way of doing that :) – DevNewb Oct 26 '16 at 08:58
  • You have just moved hard to test code from your view.cs into your markup. The markup is just as likely (if not more) to be damaged by a merge etc then the code behind is. – Ian Ringrose Oct 26 '16 at 12:11
  • @Ian what do you mean by that? I have moved code (body of event handler) from view to view model. Are you suggesting that I should test the binding? It's strongly typed, what do you want to test there? The only thing you can mess up is event name. – DevNewb Oct 26 '16 at 12:27
  • Its not as if you would messed up the "body of event handler" without trying, and the "strong typing" in the markup is not check at compile time, so does not show an error until you use that bit of the UI. – Ian Ringrose Oct 26 '16 at 12:38
  • I have seen far too many merges of markup between branches go wrong and not get detected by unit tests or the build sever, to leave me with much trust in markup. Most merges that go wrong of cs code result in the build failing. – Ian Ringrose Oct 26 '16 at 12:39
  • @Ian can you please come up with an example of what can go wrong with my markup ? Just want to check if my current tools would detect that, because I haven't had such problems that you describe – DevNewb Oct 26 '16 at 13:22
  • Most likely you are not working with lots of other developers on a large project when the same mark-up get edited by people in different teams on different source control branches, and then the branches merged by yet anther person. – Ian Ringrose Oct 26 '16 at 13:31
0

Sometimes I use a work-around.

Assumes u have a view "MainWindow" and a viewmodel "MainWindowVM".

public class MainWindowVM
{
    private MainWindow mainWindow;
    public delegate void EventWithoudArg();
    public event EventWithoudArg Closed;

    public MainWindowVM()
    {
        mainWindow = new MainWindow();
        mainWindow.Closed += MainWindow_Closed;
        mainWindow.DataContext = this;
        mainWindow.Loaded += MainWindow_Loaded;
        mainWindow.Closing += MainWindow_Closing;
        mainWindow.Show();
    }

    private void MainWindow_Loaded()
    {
        //your code
    }

    private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        //your code
    }

    private void MainWindow_Closed()
    {
        Closed?.Invoke();
    }
}

Here I store my view in a private variable so you can access it if you need it. It breaks a bit the MVVM.
In my viewmodel, I create a new view and show it.
Here I also capture the closing event of the view an pass it to an own event.
You can also add a method to the .Loaded and .Closing events of your view.

In App.xaml.cs you just have to create a new viewmodel object.

public partial class App : Application
{
    public App()
    {
        MainWindowVM mainWindowVM = new MainWindowVM();
        mainWindowVM.Closed += Mwvm_Close;
    }

    private void Mwvm_Close()
    {
        this.Shutdown();
    }
}

I create a new viewmodel object and capture it own close-event and bind it to the shutdown method of the App.

Stef Geysels
  • 1,023
  • 11
  • 27
0

Your description indicates that the view model is some kind of a document view. If that's correct then I would leave Save, Close, etc. to be handled by the document container e.g. the application or the main window, because these commands are on a level above the document in the same way as copy/paste are on the application level. In fact ApplicationCommands has predefined commands for both Save and Close which indicates a certain approach from the authors of the framework.