1

Often I want to open a new window from within some view. I see opening a window as view logic. So I don't want code in my viewmodel that creates/opens/changes windows or anything else that interacts with view-things directly.

When I've got a feature like a button that opens a Window when clicked, I do it as follows.

I expose a command (like OpenWindowCommand) and an event (like ActivateWindow) on the viewmodel of the main-view. The button the user can click to open the Window binds to that command. When the command is executed on the viewmodel, the viewmodel invokes the event. The view is subscribed to the event of the viewmodel. This is done in codebehind (This means the view layer knows the type of its DataContext (the viewmodel) to be able to subscribe to the event on the viewmodel). The callback in the codebehind that was triggered when the viewmodel event was invoked, creates and opens the window. The datacontext (viewmodel) of the window is gathered via the viewmodel of the main-view.

The viewmodel of the window that is created, is obtained from the viewmodel of the main-view. I'd consider this as some kind of parent-child viewmodel relation. Since the parent viewmodel (of main-view) creates the window viewmodel, the parent viewmodel must have all dependencies the window viewmodel needs in its constructor. If the window viewmodel uses some dependencies that the main viewmodel doen't use, they still need to be transported through the main viewmodel class to be able to create the child viewmodel.

I created a code example.

<!-- main view -->
<StackPanel>
    <Button Command="{Binding OpenOptionWindowCommand}">Options</Button>
            
    <!-- 
    ...... 
    -->
</StackPanel>

class MainViewModel 
{
    public ICommand OpenOptionWindowCommand { get; }
    public event Action ActivateOptionsWindow;
    private readonly IUserService _userService;
    private readonly ISettingsService _settingsService;
    private readonly INotificationService _notifService;
    
    // Constructor with dependencies
    // Imagine this class only does something with IUserService and ISettingsService, but nothing with INotificationService.
    // It still takes a INotificationService because it needs it in CreateOptionsWindowViewModel() because
    // the OptionsWindowViewModel class depends on it.
    public MainViewModel(IUserService userService, ISettingsService settingsService, INotificationService notifService) 
    {
        _userService = userService;
        _settingsService = settingsService;
        
        _notifService = notifService; 
        // This class actually doesn't need INotificationService, but it 
        // requires it in its constructor and saves it because this viewmodel class must be able
        // to create a OptionsWindowViewModel which has a dependency on INotificationService.
        
        OpenOptionWindowCommand = new RelayCommand(OnOpenOptionWindowCommandExecute);
    }
    
    private void OnOpenOptionWindowCommandExecute(object param) 
    {
        ActivateOptionsWindow?.Invoke();
    }
    
    public OptionsWindowViewModel CreateOptionsWindowViewModel() 
    {
        // Some dependencies are the same one as in MainViewModel, but some 
        // are specifically for the OptionsWindowViewModel (like INotificationService).
        return new OptionsWindowViewModel(_userService, _settingsService, _notifService);
    }
}


/// This viewmodel class needs IUserService, ISettingsService and INotificationService
class OptionsWindowViewModel 
{
    public OptionWindowViewModel(IUserService userService, ISettingsService settingsService, INotificationService notifService) 
    {
        // ...
    }
}

/// Codebehind of main view

public partial class MainView : UserControl
{
    public MainView()
    {
        InitializeComponent();

        DataContextChanged += MainView_DataContextChanged;
        Unloaded += MainView_Unloaded;
    }

    private void MainView_Unloaded(object sender, RoutedEventArgs e)
    {
        UnsubscribeFromViewModel(DataContext);
    }

    private void MainView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        UnsubscribeFromViewModel(e.OldValue);
        SubscribeToViewModel(e.NewValue);
    }

    private void SubscribeToViewModel(object dc)
    {
        if(dc is MainViewModel vm) 
        {
            vm.ActivateOptionsWindow += Vm_ActivateOptionsWindow;
        }
    }

    private void UnsubscribeFromViewModel(object dc)
    {
        if (dc is MainViewModel vm)
        {
            vm.ActivateOptionsWindow -= Vm_ActivateOptionsWindow;
        }
    }

    private void Vm_ActivateOptionsWindow(object sender, EventArgs e)
    {
        if(DataContext is MainViewModel mainVM) 
        {           
            OptionsWindow window = new OptionsWindow();
            window.DataContext = mainVM.CreateOptionsWindowViewModel();
            window.ShowDialog();
        }
    }
}

As you can see in my example, there's a traveling route of dependencies through a class hierarchy. I don't think this is good design, because constructor parameter lists are getting bigger and a 'parent' class takes constructor parameters to just store them in a field only to be able to create another class that depends on it.

Passing the child viewmodel in the constructor of the parent viewmodel is in many cases not a good option, because often the child viewmodel has multiple constructors and the parent viewmodel has to decide which constructor it wants to call.

How could I solve this design issue?

5
  • In a "full-sized" program, you would have a navigation system that does all this for you. The only thing that would be required from you is to inherit your View Model from an INavigatable interface and provide a suitable constructor; the DI container does the rest. The catch is that it's a different architecture than you may be used to, but there are many benefits: it makes your program more modular, and it provides the ability to navigate anywhere in your program using a URI. May 3 at 13:39
  • To find out more, have a look here. May 3 at 13:40
  • All that said, your instincts are correct. All these hoops to jump through, just to be semantically correct about an operation being a "View" operation and not a "View Model" operation. In the last WPF application I worked on, I created a static helper function whose sole purpose was to properly open a dialog from within a View Model, so that I could dispense with such nonsense. May 3 at 15:56
  • @RobertHarvey Do you have an example or a link to some tutorial? May 3 at 16:50
  • 1
    If you're looking for Prism tutorials, there's a wealth of information here. May 3 at 18:13

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.