Home > Software engineering >  Opening dialogs in WPF MVVM
Opening dialogs in WPF MVVM

Time:07-30

I have started learning MVVM for a project I'm writing, and I'm sketching out some of the more complicated parts of the project beforehand to help me get a better handle on how MVVM works. One of the biggest things I'm having trouble with though is dialogs, specifically custom dialogs and message boxes. Right now, I have a list of objects, and to add a new one, a button is pressed. This button calls a command in my ViewModel which invokes a Func that returns the object I want (Pile), then adds that to the list. Here's that function

private void OnAdd()
{
    Pile? pile = GetPileToAdd?.Invoke();
    if (pile is null) return;
    Piles.Add(pile);
}

This function is set in the view when the data context gets set (I'm implementing a Model-First architecture)

private void PileScreenView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue is PileScreenViewModel psvm)
    {
        psvm.GetPileToAdd = () =>
        {
            MessageBox.Show("getting pile");
            return new Pile() { Name = "Name", Length = 0 };
        };
    }
}

The Messagebox.Show call will eventually get replaced with a custom dialog that will provide the data needed. My question then is:

Is this MVVM compliant? It feels a bit gross having to wait until the DataContext is changed to add the method to it, but I'm 99% sure that having the messagebox call in the ViewModel is a big no-no. Also not sure if I'm allowed to interact with the Model like this from the View.

Thanks for the help and for helping me with my MVVM journey :)

CodePudding user response:

Your gut feeling is absolutely right: dialogs are components of the View as they interact with the user as part of the UI. Therefore, dialogs of any kind must be handled in the View.

Your current problem is that your view depends on a particular View Model instance in order to register the callback.
Note that the callback violates MVVM as it delegates dialog handling to the View Model. The View Model must never execute or actively participate in any UI logic.

You can improve your design by making the dependency on the DataContext anonymously and independent (of the particular instance). This will also eliminate the need to observe DataContext changes.

It's best if the View only knows View Model types by their interface:
IPileScreenViewModel.cs

interface IPileScreenViewModel : INotifyPropertyChanged
{
  // Create the Pile in the View Model.
  // View Model should never wait for anything. 
  // It is invoked by the View after the required data is collected.
  void CreatePile(CreatePileViewDialogModel newPileInfo);
}

Then in the View you can show the dialog e.g. on click of a corresponding button. Dialogs an their logic should be generally designed to be triggered by the UI i.e. the user:

PileScreenView.xaml.cs

partial class PileScreenView : UserControl
{
  private void OnCreatePileButtonClicked(object sender, RoutedEventArgs e)
  {
    var dialogViewModel = new CreatePileViewDialogModel();
    var createPileDialog = new CreatePileDialog() { DataContext = dialogViewModel };
    createPileDialog.ShowDialog();
   
    (this.DataContext as IPileScreenViewModel)?.CreatePile(dialogViewModel);
  }
}

The above example is a very simple to demonstrate how the interaction and data flow could look like. There are the usual options to send the data to the View Model, like data binding.
For example, PileScreenView could expose a dependency property e.g., CreatedPileScreenInfo. The PileScreenView would then assign the dialog result to this property, to which the PileScreenViewModel can bind to. This way the DataContext is completely unimportant for the PileScreenView.

CodePudding user response:

I suggest implementing a dialog service and injecting it into the view-model.

interface IDialogService
{
    Pile? ShowPileDialog(/* any arguments that will be needed to show the dialog */);
}

class MessageBoxDialogService
{
    public Pile? ShowPileDialog(/* arguments */)
    {
        MessageBox.Show("getting pile");
        return new Pile() { Name = "Name", Length = 0 };      
    }
} 

You need to register MessageBoxDialogService as the implementation of IDialogService in your DI container.

In the view-model:

class PileScreenViewModel
{
    private IDialogService _dialogService;

    public PileScriptViewModel(IDialogService dialogService)
    {
        _dialogService = dialogService;
    }

    private void OnAdd()
    {
        Pile? pile = _dialogService.GetPile(/* pass parameters from the view model */);
        if (pile is null) return;
        Piles.Add(pile);
    }      
}

It doesn't go against the principles of MVVM. As the view-model is decoupled from the view. You can inject a fake dialog service to test the view-model in isolation. By doing so, your code will also respect the open-closed principle, i.e., if the day comes and you decide to implement your custom dialog you only need to create a new dialog service class without needing to change existing classes.

  • Related