3

Opening dialogs while adhering to the MVVM pattern seems to be one of the regular questions (here and elsewhere). I know there are frameworks like MVVM light who have answers to that problem, but I'm currently working on a very small personal project where I try to do most of the stuff myself for learning purposes.

To "force" myself to pay attention to the references, I decided to extract the view models from the UI project and put them into a separate assembly. The UI project references UI.ViewModels, but not the other way round. This led to me having problems with opening (modal) dialog windows.

Many people seem to be using a DialogService which does something along the lines of:

internal class DialogService : IDialogService
{
  public bool? OpenDialog(ViewModelBase vm)
  {
    Window window = new Window();
    window.Content = vm;
    return window.ShowDialog();
  }
}

The corresponding window content can be inferred from the view model type by using a DataTemplate.

In my scenario though, this doesn't work since the DialogService needs to be in the UI project, but I have to call it from a view model. I could of course abuse DI/IoC to inject an IDialogService implementation into my view models, but that's not what I want to do.

Is there any rigorous way to get this to work?

As an alternative, I have added the following code to my ViewModelBase:

public abstract class ViewModelBase : INotifyPropertyChanged
{
  ...

  public event Action<ViewModelBase, Action<bool?>> Navigate;

  protected virtual void OnNavigate(ViewModelBase vm, Action<bool?> callback)
  {
    Navigate?.Invoke(vm, callback);
  }
}

Obviously, there could be more overloads, other parameters, EventArgs and so on; and I should probably put this into an interface as well. But it's just a 'thought' so far.

When creating a view instance (either via resolving or e.g. in the NavigationService, see below), I can let a NavigationService subscribe to that event and invoke the callback. Is this a problematic/bad idea? What are the cons of this? So far (without testing much), one thing I don't like is that I cannot continue in the next line after opening the dialog, but have to continue inside the callback code. That also makes it harder to follow the program flow by reading the code.

Any input is greatly appreciated! This is a very interesting topic, and since many many answers to these questions on SO are quite dated, I hope to learn more about the current best practices :)

InvisiblePanda
  • 1,589
  • 2
  • 16
  • 39
  • You may want to watch my recent video on working with dialogs in MVVM (https://www.youtube.com/watch?v=OqKaV4d4PXg). It is a common problem, but can be solved relatively easy. You will still need to decide how you want to inject the services (or not), because you can also use a service locator pattern or other methods. Injection isn't the only way. – David Anderson Jan 24 '17 at 23:08

2 Answers2

2

In my scenario though, this doesn't work since the DialogService needs to be in the UI project, but I have to call it from a view model.

The IDialogService interface should be defined in the view model project. The view models only know about this interface and you can easily provide a mock implementation of it in your unit tests that simply returns a bool? without actually creating a dialog.

The concrete implementation of the IDialogService, i.e. the DialogService class that creates the window, should on the other hand be implemented in the UI project.

I could of course abuse DI/IoC to inject an IDialogService implementation into my view models, but that's not what I want to do.

This is not an abuse of the depenedency injection pattern. It's rather a typical and correct use of it. I use it myself and it works wonders.

Your alternative solution of extending the view model base class to provide some kind of dialog functionality seems just messy and wrong to me. I would certainly stick with injecting the IDialogService interface into all view models that actually require a dialog service. This is the same as injecting them with any other service.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • 1
    Thank you, the more I thought of it today, the more it cleared up! And yes, I really like mocking to work. The only "problem" I was still seeing (see my 2nd comment above) was how to create new VM instances in another VM. But since the view models already have `IDialogService` objects, they can just pass them on. – InvisiblePanda Jan 22 '17 at 13:05
1

Why injecting class implementing IDialogService into the viewmodels abuses IoC? I think it's a good way to do this.

Don't overthink this, separating views and logic projects is a good idea, just use whatever works for you.

In my last project I have used Interaction class from ReactiveUI framework. It works well with dialogs and file pickers. I think it closely resembles your alternative, you can check the code and examples.

It's using Reactive Extensions, but you should get the idea.

Paweł Słowik
  • 308
  • 2
  • 11
  • Given the reference structure, I cannot make an interface `IDialogService` known in my view models without putting it in there or in some shared/common project. But an implementation of this interface would *have* to be in the UI project, because it shows a window. This just doesn't seem 'right' to me, but rather like a way to circumvent the missing project reference. I'll have a look at the links you provided tomorrow, thanks! – InvisiblePanda Jan 21 '17 at 23:51
  • Why not just put IDialogService interface in the viewmodels project? Your viewmodels should be able to be used to run the program with(out) any UI, so it's natural to put it there. Of course the implementation have to be in the UI project, that's the point of the separation - removing UI dependency from viewmodels and providing an interface to interact with UI. – Paweł Słowik Jan 22 '17 at 01:07
  • You're convincing me, point by point ;) It still feels 'strange' to me to register an implementation from the UI project with an interface from UI.ViewModels, but on the other hand it also seems correct... I can also still create new view model instances from a VM by passing the `IDialogService` the top-level VM got initially. Do you have an opinion about the event driven idea above? – InvisiblePanda Jan 22 '17 at 08:22
  • I think events are good when viewmodel doesn't need answer from the view (one-way), however using callbacks in events seems not very C#-ish way. There are better options - involving async/awaiting for result. The example I given (Interaction) is internally using RX, but using the class requires just simple await - I think it works much better without obfuscating code with callbacks. – Paweł Słowik Jan 22 '17 at 18:21