39

I've been toying with where to put "are you sure?" type prompts in my MVVM WPF app.

I'm leaning towards thinking that these are purely part of the View. If the ViewModel exposes a DeleteCommand, then I would expect that command to delete immediately.

To integrate such prompts into the ViewModel, it would have to expose a separate RequestDeleteCommand, a DeletePromptItem property for binding the prompt against, and which could also double as a trigger to show the prompt.

Even with this, there's nothing stopping a unit test calling DeleteCommand directly, unless I put specific logic in the ViewModel to require DeletePromptItem to match the item supplied as an argument to DeleteCommand.

However, this all just seems like noise in the ViewModel to me. The prompt is more a user interface issue to guard against misclicks etc. To me this suggests it should be in the view with a confirmed prompt calling the DeleteCommand.

Any thoughts?

GazTheDestroyer
  • 20,722
  • 9
  • 70
  • 103
  • 6
    What about "This file already exists. Do you want to overwrite it ?" kind of prompts ? – Nicolas Repiquet Apr 19 '12 at 11:40
  • Good question. I don't know. Will have to think about that. – GazTheDestroyer Apr 19 '12 at 12:23
  • as i said in my answer. put the application logic in the viewmodel and use a service to show your dialogs. – blindmeis Apr 19 '12 at 12:37
  • What about... not using the prompts? They're one of the classic UX fails; first they annoy your users, and then the users train themselves to just hit "yes." Ick. – mjfgates Apr 19 '12 at 16:21
  • @NicolasRepiquet I've had that kind of error: http://stackoverflow.com/questions/21805743/change-messagedialog-content-or-show-new-one-from-messagedialog-handler-windows/21806192#21806192 – Jakub Kuszneruk Feb 16 '14 at 02:03

11 Answers11

16

The prompts should definitely not be part of the ViewModel, but this doesn't necessarily mean that the best solution is to hardcode them in the View (even though that's a very reasonable first approach).

There are two alternatives that I know of which can reduce coupling between View and ViewModel: using an interaction service, and firing interaction requests. Both are explained very well here; you might want to take a look.

The general idea is that you abstract how asynchronous interactions are done and work with something more similar to event-based logic while at the same time allowing the ViewModel to express that it wants to interact with the user as part of an operation; the net result is that you can document this interaction and unit test it.

Edit: I should add that I have explored using Prism 4 with interaction requests in a prototype project and I was very pleased with the results (with a bit of framework code going you can even specify what's going to happen on a specific interaction request entirely in XAML!).

boop
  • 7,413
  • 13
  • 50
  • 94
Jon
  • 428,835
  • 81
  • 738
  • 806
  • +1 for link. Prism's approach seems to be the closest to pure MVVM I've found, thanks. The logic stays in the ViewModel, all interface stuff stays in the same view. – GazTheDestroyer Apr 19 '12 at 13:29
8

However, this all just seems like noise in the ViewModel to me. The prompt is more a user interface issue to guard against misclicks etc. To me this suggests it should be in the view with a confirmed prompt calling the DeleteCommand.

I agree; prompts like this should be handled in the view, as ultimately the view is what the user is seeing and interacting with, and not the view model. Once your view has obtained confirmation from the user that the DeleteCommand should be invoked, then go ahead and invoke it in your view model.

The way I see it, unit tests don't really have anything to do with user interaction, unless you're testing the view itself.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
  • I think I've changed my mind. For the limited specific example I gave the view only method works ok, but for any kind of logic based interaction such as Nicolas's comment then the ViewModel needs to be involved. – GazTheDestroyer Apr 19 '12 at 13:31
6

In my opinion, prompting the user consists of two parts:

  1. The logic that determines whether or not the prompt should be shown and what should be done with the result
  2. The code that actually shows the prompt

Part 2 clearly doesn't belong in the ViewModel.
But Part 1 does belong there.

To make this separation possible, I use a service that can be used by the ViewModel and for which I can provide an implementation specific to the environment I am in (WPF, Silverlight, WP7).

This leads to code like this:

interface IMessageBoxManager
{
    MessageBoxResult ShowMessageBox(string text, string title,
                                    MessageBoxButtons buttons);
}

class MyViewModel
{
    IMessageBoxManager _messageBoxManager;

    // ...

    public void Close()
    {
        if(HasUnsavedChanges)
        {
            var result = _messageBoxManager.ShowMessageBox(
                             "Unsaved changes, save them before close?", 
                             "Confirmation", MessageBoxButtons.YesNoCancel);
            if(result == MessageBoxResult.Yes)
                Save();
            else if(result == MessageBoxResult.Cancel)
                return; // <- Don't close window
            else if(result == MessageBoxResult.No)
                RevertUnsavedChanges();
        }

        TryClose(); // <- Infrastructure method from Caliburn Micro
    }
}

This approach can easily be used not only to show a message box but also to show other windows, as explained in this answer.

Community
  • 1
  • 1
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • 2
    I'm not sure this does separate concerns though. You now have a hardcoded messagebox prompt in the ViewModel. What if the view wanted to do confirmation some other way, like drag and drop, or even a typed response. The ViewModel should know nothing about such stuff. – GazTheDestroyer Apr 19 '12 at 12:21
  • I don't think I understand your question. The message box is an abstract construct. How it is implemented isn't defined by the ViewModel. The concrete implementation for WP7 could show the text and ask you to drag the text on one of three boxes. One of the boxes has the meaning of "Yes", one of "No" and one of "Cancel". It still has the same semantics. – Daniel Hilgarth Apr 19 '12 at 12:24
  • @GazTheDestroyer: The important points are: (1) The message box is shown conditionally - only if the ViewModel has unsaved changes - and (2) the ViewModel needs to receive one of three possible results and act accordingly. – Daniel Hilgarth Apr 19 '12 at 12:27
  • Yeah I understand that ShowMessageBox can be implemented how you want, but my point is that the main view has no control over it since it's invoked from the ViewModel, and so cannot modify it. I'm probably nitpicking since generally it will just be a MessageBox, but it feels like display choice is being removed from the view. – GazTheDestroyer Apr 19 '12 at 12:38
  • @GazTheDestroyer: But that is not the case. Again, the ViewModel isn't enforcing a specific implementation of that interface. You could even inject a different implementation to different ViewModels. – Daniel Hilgarth Apr 19 '12 at 12:40
  • Yup, I get what you're saying, but it's still divorced from the VIEW. You state that different implementation can be injected into the ViewModel, but the ViewModel should not care about visual implementations. Basically I'm saying that the main view, and the message box view are divorced from each other. The main view has no say. – GazTheDestroyer Apr 19 '12 at 12:44
  • @GazTheDestroyer: Indeed, the View has no say. It's none of its business, because the prompt doesn't belong to the View in any way. And the ViewModel DOESN'T care about visual implementations, I don't understand how you read that in my comment. – Daniel Hilgarth Apr 19 '12 at 12:48
  • We seem to be going around in circles, sorry! :) There are two "views". The main view tied to the ViewModel, and secondly the interface of the message box interaction. Although both can take any form, they are divorced from each other. The main view has no influence over the messagebox view. You state that this correct but I don't understand why. Maybe the view wants to integrate the prompt nicely somehow? Again, this is probably all academic, and indeed is what Prism seems to do, but it still feels wrong to me. – GazTheDestroyer Apr 19 '12 at 12:59
  • Actually having read some more it seems Prism actually uses Interaction Request Objects, which DO allow the main view to provide the interface to any interactions. Seems good. – GazTheDestroyer Apr 19 '12 at 13:27
3

I'd suggest doing this via a service which manages the modal windows. I've faced this problem quite a time ago, either. This blog post helped me a lot.

Even though it's a silverlight post, it shouldn't differ too much, compared to wpf.

Michael Schnerring
  • 3,584
  • 4
  • 23
  • 53
1

I think it depends upon the prompt, but generally speaking the code logic which needs to prompt the user is often in the view model anyway, for example the user has pressed a button to delete a list item, a command is fired in the VM, logic is ran and it is apparent that this may affect another entity, the user must then choose what they wish to do, at this point you should not be able ask the View to prompt the user so I could not see anyother choice but to handle it in the VM. It is something I've always been uneasy with but I simply wrote a Confirm method in my base VM which calls a dialog service for dsiplay the prompt and returns a true or false:

    /// <summary>
    /// A method to ask a confirmation question.
    /// </summary>
    /// <param name="messageText">The text to you the user.</param>
    /// <param name="showAreYouSureText">Optional Parameter which determines whether to prefix the message 
    /// text with "Are you sure you want to {0}?".</param>
    /// <returns>True if the user selected "Yes", otherwise false.</returns>
    public Boolean Confirm(String messageText, Boolean? showAreYouSureText = false)
    {
        String message;
        if (showAreYouSureText.HasValue && showAreYouSureText.Value)
            message = String.Format(Resources.AreYouSureMessage, messageText);
        else
            message = messageText;

        return DialogService.ShowMessageBox(this, message, MessageBoxType.Question) == MessageBoxResult.Yes;
    }

For me this is one of those grey crossover areas which I sometimes cannot get a firm answer for in MVVM so am interested in othere poeples approaches.

Paulie Waulie
  • 1,690
  • 13
  • 23
1

Have a look at this:

MVVM and Confirmation Dialogs

I use a similar technique in my view models because I believe that it is part of the view model to ask if it will proceed with the deletion or not and not of any visual object or view. With the described technique your model does not refer to any visual references which I don't like but to some kind of service that call a confirmation dialog or a message box or whatever else.

Dummy01
  • 1,985
  • 1
  • 16
  • 21
1

The way I've handled it in the past is putting an event in the ViewModel that's fired when the dialog needs to be displayed. The View hooks into the event and handles displaying the confirmation dialog, and returns the result to the caller via its EventArgs.

Daniel Mann
  • 57,011
  • 13
  • 100
  • 120
1

i think “Are you sure?” prompts belong to the viewmodel because its application logic and not pure ui stuff like animations and so on.

so the best option would be in the deletecommand execute method to call a "Are you sure" service dialog.

EDIT: ViewModel Code

    IMessageBox _dialogService;//come to the viewmodel with DI

    public ICommand DeleteCommand
    {
        get
        {
            return this._cmdDelete ?? (this._cmdDelete = new DelegateCommand(this.DeleteCommandExecute, this.CanDeleteCommandExecute));
        }
    }

put the logic in the execute method

    private void DeleteCommandExecute()
    {
      if (!this.CanDeleteCommandExecute())
         return;

        var result = this.dialogService.ShowDialog("Are you sure prompt window?", YesNo);

        //check result
        //go on with delete when yes
     } 

the dialog service can be anything you want, but the application logic to check before delete is in your viewmodel.

           

Community
  • 1
  • 1
blindmeis
  • 22,175
  • 7
  • 55
  • 74
0

Personally, I think it's just part of the View as there is no data

Neil Thompson
  • 6,356
  • 2
  • 30
  • 53
0

I solve this kind of problem by using the EventAggregator pattern.

You can see it explained here

Nicolas Repiquet
  • 9,097
  • 2
  • 31
  • 53
0

Ran into this while porting an old WinForms app to WPF. I think the important thing to keep in mind is that WPF accomplishes at lot of what it does under the hood by signaling between the view model and the view with events (i.e. INotifyPropertyChanged.PropertyChanged, INotifyDataErrorInfo.ErrorsChanged, etc). My solution to the problem was to take that example and run with it. In my view model:

/// <summary>
/// Occurs before the record is deleted
/// </summary>
public event CancelEventHandler DeletingRecord;

/// <summary>
/// Occurs before record changes are discarded (i.e. by a New or Close operation)
/// </summary>
public event DiscardingChangesEvent DiscardingChanges;

The view can then listen for these events, prompt the user if need be and cancel the event if directed to do so.

Note that CancelEventHandler is defined for you by the framework. For DiscardingChanges, however, you need a tri-state result to indicate how you want to handle the operation (i.e. save changes, discard changes or cancel what you are doing). For this, I just made my own:

public delegate void DiscardingChangesEvent(object sender, DiscardingChangesEventArgs e);

public class DiscardingChangesEventArgs
    {
        public DiscardingChangesOperation Operation { get; set; } = DiscardingChangesOperation.Cancel;
    }

public enum DiscardingChangesOperation
    {
        Save,
        Discard,
        Cancel
    }

I tried to come up with a better naming convention, but this was the best I could think of.

So, to putting it in action looks something like this:

ViewModel (this is actually a base class for my CRUD-based view models):

protected virtual void New()
{
    // handle case when model is dirty
    if (ModelIsDirty)
    {
        var args = new DiscardingChangesEventArgs();    // defaults to cancel, so someone will need to handle the event to signal discard/save
        DiscardingChanges?.Invoke(this, args);
        switch (args.Operation)
        {
            case DiscardingChangesOperation.Save:
                if (!SaveInternal()) 
                    return;
                break;
            case DiscardingChangesOperation.Cancel:
                return;
        }
    }

    // continue with New operation
}

protected virtual void Delete()
{
    var args = new CancelEventArgs();
    DeletingRecord?.Invoke(this, args);
    if (args.Cancel)
        return;

    // continue delete operation
}

View:

<UserControl.DataContext>
    <vm:CompanyViewModel DeletingRecord="CompanyViewModel_DeletingRecord" DiscardingChanges="CompanyViewModel_DiscardingChanges"></vm:CompanyViewModel>
</UserControl.DataContext>

View code-behind:

private void CompanyViewModel_DeletingRecord(object sender, System.ComponentModel.CancelEventArgs e)
{
    App.HandleRecordDeleting(sender, e);
}

private void CompanyViewModel_DiscardingChanges(object sender, DiscardingChangesEventArgs e)
{
    App.HandleDiscardingChanges(sender, e);
}

And a couple of static methods that are part of the App class that every view can use:

public static void HandleDiscardingChanges(object sender, DiscardingChangesEventArgs e)
{
    switch (MessageBox.Show("Save changes?", "Save", MessageBoxButton.YesNoCancel))
    {
        case MessageBoxResult.Yes:
            e.Operation = DiscardingChangesOperation.Save;
            break;
        case MessageBoxResult.No:
            e.Operation = DiscardingChangesOperation.Discard;
            break;
        case MessageBoxResult.Cancel:
            e.Operation = DiscardingChangesOperation.Cancel;
            break;
        default:
            throw new InvalidEnumArgumentException("Invalid MessageBoxResult returned from MessageBox.Show");
    }
}

public static void HandleRecordDeleting(object sender, CancelEventArgs e)
{
    e.Cancel = MessageBox.Show("Delete current record?", "Delete", MessageBoxButton.YesNo) == MessageBoxResult.No;
}

Centralizing the dialog box in these static methods lets us easily swap them out for custom dialogs later.

Blake
  • 200
  • 3
  • 10