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, 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 get all dependencies for itself and the window viewmodel via its constructor.
In other words, there's a hierarchy of viewmodels, and dependencies are passed from the parent to the child viewmodel. This results in a parent viewmodel getting things in its constructor just for propagating it down to the child viewmodel.
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 have 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 INotificationService, but nothing with ISettingsService.
// It still takes an ISettingsService because it needs it in CreateOptionsWindowViewModel() because
// the OptionsWindowViewModel class depends on it.
public MainViewModel(IUserService userService, INotificationService notifService, ISettingsService settingsService)
{
_userService = userService;
_notifService = notifService;
_settingsService = settingsService;
// This class (MainViewModel) actually doesn't need ISettingsService, but it
// requires it in its constructor and saves it because this viewmodel must be able
// to create an OptionsWindowViewModel which has a dependency on ISettingsService.
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 ISettingsService).
return new OptionsWindowViewModel(_userService, _notifService, _settingsService);
}
}
// This viewmodel class needs IUserService, INotificationService and ISettingsService.
class OptionsWindowViewModel
{
public OptionWindowViewModel(IUserService userService, INotificationService notifService, ISettingsService settingsService)
{
// ...
}
}
/// 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.
To solve this I almost get the urge to use the service locator anti pattern. But I think that makes it worse.
How could I solve this design issue?