Technical
Technical

Reputation: 154

MVVM DialogService alternatives

While learning how to program using MVVM pattern I've ran into a common problem - display various dialogs from ViewModels.

At first it looked simple for me. I created an IWindowService interface, implemented it in a WindowService class. I used this class to launch new View windows.

But then I needed MessageBox style dialogs. So I created an IDialogService and a DialogService class. I did same for Open/Save file dialogs.

After all that I've noticed that creating ViewModel instances became quite complicated:

ViewModel VM = new ViewModel(Data, AnotherData, MoreData, WindowService, DialogService, FileDialogService, AnotherService);

I tried to merge all services into a single FrontendService class but it just made it quite hard to maintain and the IFrontendService interface became really "bloated".

Now I'm looking for alternative ways. The best case for me would be something that won't require passing instances to the ViewModel constructors.

Upvotes: 3

Views: 1643

Answers (1)

BionicCode
BionicCode

Reputation: 29028

Dialogs or Window in general are view related. If you want to implement MVVM then you have to separate View from your View Model. MVVM will help you to accomplish this.

MVVM dependency graph
MVVM dependency graph and responsibilities overview

The dependency graph shows that the View depends on the View Model. This dependency is unidirectional for the purpose of decoupling. This is possible only because of the data binding mechanism.

It is important to emphasize that MVVM is an application architectural design pattern. It views applications from a component perspoective and not class perspective. The wide spread practice to name the source class of a data binding "ViewModel" is quite misleading, obviously. In fact, as the View is composed of many classes (e.g. controls), so is the View Model. It's a component.

Since the dialog is part of the View, a Window.Show() invoked by the View Model would add an illegal arrow, that points from View Model to View. This would mean the View Model now depends on the View.
Now, that you have created an explicit dependency to the View, you will encounter new problems (that MVVM originally was trying to solve): if you decide to show a different window type or replace the dialog with a popup (or in other words anytime you modify the View), then you would have to modify the View Model. This is exactly what MVVM is designed for to avoid.

The solution is to let the View handle itself. When the View needs to show a dialog, it must do it on its own.

A dialog is a way to provide user interaction. User interaction is not business of the View Model.

If you need a dedicated service to handle displaying GUI, then it must operate entirely in the View - as such it can't be referenced by the View Model.
Since the View Model is data related (presentation of data) it could only flag data related states that the View can trigger on (e.g. data validation errors, where the recommended way is to implement INotifyDataErrorInfo on the View Model).

My recommendation is to keep your View Model free from View related responsibilities and keep its business focused on the model data presentation only - or let go MVVM and return to the initial decoupling problem.

Solution 1

The simplest way is to show the dialog from code-behind or a routed event handler. It can be triggered by an exception or event thrown or raised by the View Model. For example if writing to a file fails the View Model can raise as FileError event that the View listens to and reacts to e.g., by displaying a dialog to the user.
Then pass the collected data (if this is an input dialog) to the View Model (if this is required) by using an ICommand or by updating a data binding.

Code-behind does not violate MVVM as MVVM is component based, while code-behind is a C# language that is unknown to the concept of MVVM. A requirement of a design pattern is that it must be language independent. In the definition of MVVM code-behind does not play any role - its not mentioned.

Solution 2

Alternatively design your own dialog by implementing a ContentControl (or UserControl). Such a control blends perfectly into the WPF framework and allows to write MVVM compliant code. You can now make use of data binding and data triggers to show and hide the control/dialog.

Native Windows dialogs do not integrate well into the WPF framework. A Window can't be shown using a trigger. We must call the Show() or DialogShow() method. That's where the original problem "How to show a dialog in an MVVM compliant way" stems from.

This is an example of a modal dialog, that can be shown and hidden using XAML only. No C# involved. It uses Event Triggers to animate the Visibility of the dialog grid (or alternatively animate the Opacity). The event is triggered by a Button. For different scenarios the Button can be simply replaced with a DataTrigger or a binding using the BooloeanToVisibilityConverter:

<Window>
  <Window.Triggers>
    <EventTrigger RoutedEvent="Button.Click" SourceName="OpenDialogButton">
      <BeginStoryboard>
        <Storyboard>
          <ObjectAnimationUsingKeyFrames 
                          Storyboard.TargetName="ExampleDialog"
                          Storyboard.TargetProperty="Visibility"
                          Duration="0">
            <DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}"/>
          </ObjectAnimationUsingKeyFrames>
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Window.Triggers>

  <Grid SnapsToDevicePixels="True" x:Name="Root">

    <!-- The example dialog -->
    <Grid x:Name="ExampleDialog" Visibility="Hidden"  Panel.ZIndex="100" VerticalAlignment="Top">

      <!-- The Rectangle stretches over the whole window area --> 
      <!-- and covers all window child elements except the dialog -->
      <!-- This prevents user interaction with the covered elements -->
      <!-- and adds modal behavior to the dialog -->
      <Rectangle
        Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=ActualWidth}"
        Height="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window}, Path=ActualHeight}"
        Fill="Gray" Opacity="0.7" />
      <Grid Width="400" Height="200" >
        <Grid.RowDefinitions>
          <RowDefinition Height="*"/>
          <RowDefinition Height="100"/>
        </Grid.RowDefinitions>
        <Border Grid.RowSpan="2" Background="LightGray" BorderBrush="Black" BorderThickness="1">
          <Border.Effect>
            <DropShadowEffect BlurRadius="5" Color="Black" Opacity="0.6" />
          </Border.Effect>
        </Border>
        <TextBlock Grid.Row="0" TextWrapping="Wrap"
                   Margin="30"
                   Text="I am a modal dialog and my Visibility or Opacity property can be easily modified by a trigger or a nice animation" />
        <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right" Height="50" >
          <Button x:Name="OkButton"
                  Content="Ok" Width="80" />
          <Button x:Name="CancelButton" Margin="30,0,30,0"
                  Content="Cancel" Width="80" />
        </StackPanel>
      </Grid>

      <Grid.Triggers>
        <EventTrigger RoutedEvent="Button.Click">
          <BeginStoryboard>
            <Storyboard>
              <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ExampleDialog"
                                             Storyboard.TargetProperty="Visibility"
                                             Duration="0">
                <DiscreteObjectKeyFrame Value="{x:Static Visibility.Hidden}" />
              </ObjectAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </Grid.Triggers>
    </Grid>

    <! The actual control or page content -->
    <StackPanel>
      <TextBlock Text="This is some page content" />

      <!-- The button to open the dialog. This can be replaced by a DataTrigger -->
      <Button x:Name="OpenDialogButton" Content="ShowDialog" Width="100" Height="50" />
    </StackPanel>
  </Grid>
</Window>

This is the dialog:

enter image description here

You can encapsulate the dialog and move the implementation into a dedicated Control e.g. DialogControl, which is easier to use throughout the application (no duplicate code, improved handling). You can add the common window chrome to the dialog like title bar, icon and the chrome buttons to control the state of the dialog.

Edit to show the wrong and why a "dialog service" violates/eliminates MVVM

Everything that the View Model component references is either part of the View Model too or part of the Model. The above MVVM dependency diagramm shows very well that the View component is completely unknown to the View Model component. Now, how can a dialog service, that is known by the View Model and that shows modules of the View like a dialog, not violate the MVVM pattern? Apparently, either the View Model component has knowledge of a View component's module or the View Model contains illegal responsibilities. Either way, a dialog service obviously does not solve the problem. Moving code from the class named ...ViewModel to a class named ...Service, where the original class still has a reference to the new class, is doing nothing in terms of architecture. The code is still in the same component, referenced by a View Model class. Nothing has changed except the name of the class that shows the dialog. And giving a class a name does not change its nature. For example naming my data binding source MainView instead of MainViewModel does not make MainView part of the View.
The class naming or naming conventions in general are absolutely irrelevant in terms of architecture, in terms of MVVM. The responsibilities and dependencies are the matter of interest.

Here are the dependencies introduced by a dialog service which is operated by the View Model:

enter image description here

As you can see we now have an arrow (dependency) that points from the View Model towards the View. Now changes in the View will reflect to the View Model and impact implementations. It is because the View Model is now involved in the View logic - in this special case the user interaction logic. User interaction logic is GUI, is View. Controlling this logic from the View Model screams "MVVM violation"...
It's fine if you can accept this violation. But it is a violation of the MVVM design pattern and can't be sold as a "MVVM way of showing dialogs". At least we should not buy it.

Upvotes: 4

Related Questions