I haven't worked with WPF/MVVM for a while, but now I'm back fixing some issues with one of my old apps and remembering the struggles I always had when dealing with asking for user input (eg. showing a dialog) from a view model.
I've put some thought into it and decided to try routing user input requests through a service interface so that it can be mocked/tested, while keeping the view logic out of my view model.
public interface IUserInputService
{
UserRegistration PromptUserRegistration();
}
I then wrote a generic method that creates a new instance of a view and view model, connects them together, shows the dialog and then returns the dialog view model and result (eg. accepted, cancelled) back to the caller. Finally, the caller can extract information from the returned view model and return the actual desired result. The view models have events which are called when the view model is "done", eg: Accepted and Cancelled. These are hooked up using the generic method.
public class UserInputService : IUserInputService
{
// Implementation of the service interface using the generic method below
public UserRegistration PromptUserRegistration()
{
var result = ShowDialog<UserRegistrationDialog, UserRegistrationViewModel>((v, vm) =>
{
vm.Accepted += (s, e) => v.DialogResult = true;
vm.Cancelled += (s, e) => v.DialogResult = false;
});
if (result.State != DialogState.Accepted)
{
return null;
}
return new UserRegistration
{
FirstName = result.ViewModel.FirstName,
LastName = result.ViewModel.LastName
};
}
// The generic method
private DialogResult<TViewModel> ShowDialog<TView, TViewModel>(
Action<TView, TViewModel> viewModelConnected
)
where TView : Window, new()
where TViewModel : ViewModelBase, new()
{
TViewModel viewModel = new TViewModel();
TView view = new TView();
view.DataContext = viewModel;
viewModelConnected(view, viewModel);
view.ShowDialog();
DialogState state;
switch (view.DialogResult)
{
case true:
state = DialogState.Accepted;
break;
case false:
state = DialogState.Declined;
break;
default:
state = DialogState.Cancelled;
break;
}
return new DialogResult<TViewModel>(state, viewModel);
}
}
Now I can pass around the service interface using DI/IoC and view models can ask for user input without caring how (eg. via dialog or mocking).
The other types (DialogResult and DialogState) are these:
public enum DialogState
{
Accepted,
Declined,
Cancelled
}
public class DialogResult<TViewModel>
{
public DialogResult(DialogState state, TViewModel viewModel)
{
State = state;
ViewModel = viewModel;
}
public DialogState State { get; }
public TViewModel ViewModel { get; }
}
So my questions are...
- Is this a good approach?
- What would you do differently and why?
- Any other suggestions regarding code quality?
I want to stick to the principles of MVVM as much as possible (though within reason) while keeping things testable... Let me know what you think.
accepted
anddeclined
events? Is it not enough, that you haveDialogResult
? Shouldn'tDialogResult
be in ViewModel? Can you separate in your question what belongs to view and what to viewmodel? \$\endgroup\$