Home > Software design >  Opening child window in WPF MVVM pattern - correct solution?
Opening child window in WPF MVVM pattern - correct solution?

Time:06-12

I was struggling with opening new window in MVVM pattern without violating it's rules for a month. I have read I think every post here and watch every video about this, but as a amateur programmer I did not find easy solution to understand for me.

Finally I came up with solution relying on answers I have found. There were some conditions that solution had to follow:

  1. Dependency injection friendly
  2. Not violating MVVM pattern
  3. Reusable for multiple views
  4. Easy to use (without having to type 100 lines of code for every window).
  5. No strict "DialogMVVM" etc. libraries allowed. (I am in learning phase so I wanted to understand what my code is doing.)

Disclaimer: I do not need getting result from dialog so it is not included here.

Please tell me if my solution is proper or not.

1. I made DialogWindow template with DataTemplate:

<Window x:Class="AWH.Views.DialogWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewmodels="clr-namespace:AWH.ViewModels" 
        xmlns:views="clr-namespace:AWH.Views"
        mc:Ignorable="d"
        SizeToContent="WidthAndHeight">
    <Window.Resources>
        <DataTemplate DataType="{x:Type viewmodels:ProductAddViewModel}">
            <views:ProductView/>
        </DataTemplate>
    </Window.Resources>
</Window>

2. IDialogService and implementation in DialogService

public interface IDialogService
{
    void ShowDialog(object viewModel);
}

public class DialogService : IDialogService
{
    public void ShowDialog(object viewModel)
    {
        var win = new DialogWindow();
        win.Content = viewModel;
        win.ShowDialog();
    }
}

3. Opening the window from ViewModel (in this case ProductListViewModel)

public class ProductListViewModel : ViewModelBase
{
        private IDialogService _dialogService;
        private IProductAddViewModel _productAddViewModel;
        public ICommand AddProductCommand { get; set; }

        public ProductListViewModel(IDialogService dialogService, IProductAddViewModel productAddViewModel)
        {
            _productAddViewModel = productAddViewModel;
            _dialogService = dialogService;
    
            AddProductCommand = new DelegateCommand(OpenAddProductDialog);
        }

        private void OpenAddProductDialog()
        {
        _dialogService.ShowDialog(_productAddViewModel);
        }
}

4. Injecting dependencies in App.xaml.cs (I am using IServiceCollection)

        services.AddSingleton<ProductListViewModel>();
        services.AddSingleton<IProductAddViewModel, ProductAddViewModel>();
        services.AddSingleton<IDialogService, DialogService>();

Thats it. If I thinking correctly I am not violating MVVM pattern because viewmodel is not calling view it is calling viewmodel and WPF is doing the rest via DataTemplates.

Am I right?

EDIT: Of course you need a way to open this window from some other view. So (in this case) ProductListView.xaml (this is a view corresponding to ProductListViewModel):

    <Button Content="Add product" Margin="10 15" Padding="8 5" VerticalAlignment="Stretch" Command="{Binding AddProductCommand}" />

CodePudding user response:

In the context of MVVM a dialog is a module of the View component. Following the rules of MVVM, the View Model component is not allowed to handle controls or implement UI related logic. This means, a class of the View Model is also not allowed to use another class that does handle controls or implements UI logic, as such a class would be part of the View component.

Controls must always be instantiated and handled in the View component.

The definition of a design pattern requires that a pattern must be independent of any language, compiler or platform in order to qualify.
Since code-behind is a pure language feature (and therefore a compiler feature), code-behind can't violate any design pattern.

Code-behind i.e. a partial class, is a crucial part of WPF: not everything can be implemented in XAML. The conclusion that if you can't implement it in XAML, dependency properties or complex logic in general for example, it must be in the View Model is very wrong.
In fact most view related framework code is written in C#. XAML is primarily meant to layout the UI: it visually reflects the tree structure of the UI and excels C# in terms of readability in this context. Also some tasks are easier using XAML, like creating a DataTemplate for example. This are only reasons to prefer XAML over C# when writing UI related code. XAML can never replace C#.

The solution to your problem is to show the dialog from code-behind e.g. by implementing a click handler.

To add more flexibility, the following example makes use of a RoutedCommand to replace a click handler.
This way, the dialog can be shown from any control that is child of the control that defines the corresponding CommandBinding (in this case the MainWindow):

MainWindow.xaml.cs

public partial class MainWindow : Window
{
  public static RoutedUICommand ShowAddProductDialogCommand { get; } = new RoutedUICommand(
    "Show the product dialog", 
    nameof(ShowAddProductDialogCommand), 
    typeof(MainWindow));

  private IDialogService DialogService { get; }

  public MainWindow(IDialogService dialogService)
  {
    InitializeComponent();

    this.DialogService = dialogService;
    var dialogCommandBinding = new CommandBinding(ShowDialogCommand, ExecuteShowDialogCommand, CanExecuteShowDialogCommand);
    this.CommandBindings.Add(dialogCommandBinding);
  }

  private void ExecuteShowDialogCommand(object sender, ExecutedRoutedEventArgs e) 
    => this.DialogService.ShowAddProductDialog();

  private void CanExecuteShowDialogCommand(object sender, CanExecuteRoutedEventArgs e) 
    => e.CanExecute = true;
}

MainWindow.xaml

<Window>
  <local:ProductListView />
</Window>

ProductListView.xaml

<UserControl>
  <Button Content="Show Dialog"
          Command="{x:Static local:MainWindow.ShowAddProductDialogCommand}" />
</UserControl>

IDialogService.cs

public interface IDialogService
{
  void ShowAddProductDialog();

  // Add more methods - one for each dialog.
  // Each method knows how to configure and show the dialog window.
}

DialogService.cs

public class DialogService : IDialogService
{
  private Func<IProductAddViewModel> AddProductDialogViewModelFactory { get; }

  // Inject a factory for each dialog/ContentTemplate to create new view models for each dialog.
  // Add more factories - one for each dialog/dialog view model.
  public DialogService(Func<IProductAddViewModel> addProductDialogViewModelFactory)
  {
    this.AddProductDialogViewModelFactory = addProductDialogViewModelFactory;
  }

  public void ShowAddProductDialog()
  {
    IProductAddViewModel dialogDataContext = this.AddProductDialogViewModelFactory.Invoke();
    var dialog = new DialogWindow() 
    {    
      DataContext = dialogDataContext,
      Content = dialogDataContext
    };
    
    dialog.ShowDialog();
  }
}

App.xaml.cs

// Register the factory for the DialogService
services.AddSingleton<Func<IProductAddViewModel>>(serviceProvider => serviceProvider.GetService<IProductAddViewModel>);
  • Related