2

I'm developing a WPF application using Mahapps.Metro, a Nuget package that provides "modern" UI styling.

I have created a dialog that is one of those selection dialogs where you select an item on the left hand side, click the right arrow button, and the item moves to the right hand side.

One of the validation rules in my dialog is that at least one item has to be selected before you push the button, so (in the Code Behind of my view) I open a message box and notify the user if he doesn't select at least one item:

private void AddButton_Click(object sender, RoutedEventArgs e)
{
    if (!IsAnySelected(Users))
    {
        // MetroWindow call
        this.ShowMessageAsync("Permissions", "Please select a User.");
        return;

        // Call ViewModel `AddPermissions()` method here.
    }
}

bool IsAnySelected(DataGrid dataGrid)
{
    foreach(dynamic d in dataGrid.ItemsSource)
    { 
        if (d.IsSelected) return true;
    }
    return false;
}

(The DataGrid is bound to an ObservableCollection in the ViewModel)

Because ordinary message boxes in WPF are not stylable, Mahapps provides its own. It was here that I discovered that MahApps throws a null reference exception when I try to open a message box in my View. It has something to do with my setup, because their demo works just fine.

It turns out that someone provided a way to open a Mahapps message box in the View Model instead of the view. My question is, why would you want to do this?

Doesn't the View own responsibility for any visual elements (including message boxes), and isn't validation the one thing that's permissible to do in the View's Code Behind?

Note that this approach causes a new wrinkle, which is that you now need a way to fire the View Model's method or ICommand from CodeBehind:

(DataContext as SecurityDialogViewModel).AddPermissions();
Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
  • 1
    Opening a message box would be similar to issuing a `Command` and the `Command` would be handled by the ViewModel. In this case the command is for opening a dialog. Nothing wrong with that because that is the responsibility of the ViewModel. – CodingYoshi Feb 16 '18 at 23:22
  • @CodingYoshi: Sure, but I don't have access to the form controls in the ViewModel for validation purposes. Instead, I would have to rely on the values of the `IsSelected` fields in the View Model's data-bound `ObservableCollection` (a workable, if roundabout way to do it, but my instinct still says the ViewModel has no business standing up UI components). And the technique the Mahapps guy came up with for doing this in the ViewModel tightly binds Mahapps.Metro to the ViewModel, which I'm not thrilled about either. – Robert Harvey Feb 16 '18 at 23:26
  • Even if you had access to the controls, it would not be a good idea to use it in the VM because then your VM is coupled to a UI element. In other words, you would not be able to unit test it. So using the `IsSelected` is perfectly fine. VMs co-ordinate the work so I think it is fine if it is standing up UI components (you could create another item in between and call it coordinator or something but I wouldn't). With patterns the discussion could go back and forth and there will be advantages to either side of the argument. If I do it in VM, i will make sure to be consistent in all screens. – CodingYoshi Feb 16 '18 at 23:38
  • *Message boxes **are** a UI element.* How the hell are you going to unit test a View Model if it's busy opening modal message boxes? – Robert Harvey Feb 16 '18 at 23:42
  • Thats my point too in my last comment. The VM would make a decision that the something has been selected and then the parent view will get a signal to proceed and show the dialog. The vm will have no clue about the controls – CodingYoshi Feb 16 '18 at 23:56
  • 1
    Wouldn't that mean the ViewModel must now have knowledge of the View? – Robert Harvey Feb 16 '18 at 23:57
  • No. Think about it this way: the VM always decides if a button should be enabled but the VM does not know about the button. The button may be bound to an `IsValid` property of the model through a command object's CanExecute property. How is this any different? – CodingYoshi Feb 17 '18 at 00:02
  • Well, then you're going to need some kind of binding from the ViewModel to the View so that it can receive the request to open a message box. Wouldn't it be easier to just do the validation in the View, open the message box if needed, and then pass along the processing request to the ViewModel if validation passes? – Robert Harvey Feb 17 '18 at 00:03
  • In your case the `Show` or `Slide` of the dialog is bound instead of `Enabled` of a button. – CodingYoshi Feb 17 '18 at 00:06
  • It would have to be more than just a boolean. I'd need a message as well, or at least an error code. – Robert Harvey Feb 17 '18 at 00:09
  • Also, note that while `ShowMessageAsync` might actually create a window, there's nothing you can bind to it, other than the message you pass to it. It's a message box, nothing more. – Robert Harvey Feb 17 '18 at 00:11
  • I feel your pain. Message boxes being shown from the VM is a frequently encountered anti pattern (nay, violation) in MVVM. Best thing to do is raise an event or message from the VM that indicates the data is in an inconsistent state, then the view acts on that. After all, it's up to the view how to *show* that inconsistency or error state. Of course that doesn't solve your immediate problem with Mahapps, normally I would suggest you pick up a DialogService off the internet and adapt it to your needs. – slugster Feb 17 '18 at 02:49
  • There are going to be times where the view model needs input from the user. That's just fact in the real world. Obvs, MessageBoxes would wreck unit tests, which is a good hint it's a UI concern. I usually define an interface that abstracts away the UI guff from what you really need (instruction text, and the result from the user). Then I have the window implement the interface in the application and simply mock it in tests. Of course, the pattern is here to *serve you*, not the other way around, so hackery if you want to is always an option... –  Feb 19 '18 at 21:16

3 Answers3

1

I've always taken the approach of exposing user dialogs via a callback interface. OpenFileDialog, SaveFileDialog, MessageBox, FolderSelectionDialog, etc are defined by an interface:

public interface IMainViewCallbacks
{
        bool GetPathViaOpenDialog(out string filePath, string szFilter, 
                 string szDefaultExt, string szInitialDir);
        bool GetPathViaSaveDialog(out string filePath, string szFilter, 
                 string szDefaultExt, string szInitialDir);
        bool GetFolderPath(out string folderPath);
        MessageBoxResult MessageBox(string messageBoxText, string caption, 
                  MessageBoxButton button, MessageBoxImage icon);
}

Then, implement the interface in the view codebehind.

When creating the viewmodel in the view ctor, pass this:

public class MainViewMode : IMainViewCallbacks
{
   private vm = null;
   public MainWindow()
   {
      vm = new MainViewModel(this);
      this.DataContext = vm;
   }
}

Finally, add an argument to your viewmodel ctor to receive the interface:

public class MainViewModel
{
    IMainViewCallbacks Calllbacks = null;
    public MainViewModel(IMainViewCallbacks cb)
    {
       // stash the callbacks for later.
       this.Callbacks = cb;
    }

    // pseudocode for the command that consumes the callback
    public ICommand .... 
    {
        Execute() { this.Callbacks.GetPathViaOpenDialog(); }
    } 
}

This is unit-testable; the interface provided by the unit test view can fake having received user input and just return a constant value.

Lynn Crumbling
  • 12,985
  • 8
  • 57
  • 95
0

What you want to do is not that different than enabling a button and upon clicking, saving something. In your case it is enabling the dialog and showing it. The challenge is how would you do it from the ViewModel without coupling to a dialog window.

There are 2 things you would need to take care of:

  1. Is it okay to show the dialog?
  2. Show the dialog

In your ViewModel you would do this:

public class MainViewModel
{
    private IDialog dialog;
    private ICommand showCommand;
    public MainViewModel() : this(null)
    {
    }

    public MainViewModel(IDialog dialog)
    {
        this.dialog = dialog;
        this.showCommand = new ShowCommand(this.ShowCommandHandler);
    }

    private void ShowCommandHandler(object sender, EventArgs e)
    {
        this.dialog.Show();
    }

    public ICommand ShowCommand { get { return this.showCommand; } }
}

Please note the ShowCommandHandler and the constructor overload in the above which takes an IDialog. Here is IDialog:

public interface IDialog
{
    void Show();
}

In my example, the IDialog has show but in your case it will be some method which shows the sliding dialog. If it is Show, great! Otherwise change it so it matches the method in your dialog.

Here is the command:

public class ShowCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public EventHandler handler;
    public ShowCommand(EventHandler handler)
    {
        this.handler = handler;
    }

    public bool CanExecute(object parameter)
    {
        // Decide whether this command can be executed. 
        // Check if anything is selected from within your collection
        throw new NotImplementedException();
    }

    public void Execute(object parameter)
    {
        this.handler(this, EventArgs.Empty);
    }
}

Finally in your main view, do this:

public partial class MainView : Window
{
    public MainView()
    {
        this.InitializeComponent();
        this.DataContext = new MainViewModel(/*pass your dialog here*/);
    }
}

Please note this is no different than if you had a save Button on your view and the button's Enabled property will be bound to IsEnabled in the command. Then once the button is enabled, the user clicks it and the object is saved. In this case, it sliding ability (or showing ability) is bound but instead of saving, it simply calls Show on the IDialog.

CodingYoshi
  • 25,467
  • 4
  • 62
  • 64
0

Everything you're trying to accomplish can be done from the view, with no need to consider ViewModel, unless you have more advanced requirements. The ShowMessageAsync method is async yet you do not have your method signature as an async and you are not awaiting the return.

You can also get rid of the IsAnySelected method altogether and use just Linq. Below:

private async void AddButton_Click(object sender, RoutedEventArgs e)
{

var IsAnySelectedUsers = dataGrid.ItemsSource.Cast<User>().Any(p => p.IsSelected);

if (!IsAnySelectedUsers)
{
    await this.ShowMessageAsync("Permissions", "Please select a User.");
}

}

I am using Linq to query the Datagrid ItemsSource(My datagrid is using a collection of User, so if that's not what your underlying class is, you would need to replace it with whatever underlying class is used for your datagrid)

Darthchai
  • 757
  • 8
  • 17
  • Well, almost. As I described in my question, once the validation succeeds, I intend to pass control to a method in my ViewModel. – Robert Harvey Feb 17 '18 at 05:26