Here’s a problem I come across more often than I would have expected. I want my buttons to use Commands in the View Model, but in response to the command, I have to interact with the user in a way that is much easier to do in the code behind.
Let’s take the example of a user pressing a button to delete a record. The button calls a command in the ViewModel, but I want to show a confirmation dialog box in the view. If the user affirms the deletion, I want to do the work back in the View Model. Oy.
I could use MessageCenter but it makes for fragile code that is hard to debug. Instead, I’ll use an event that is fired in the ViewModel and handled in the View, and I’ll call methods in the ViewModel directly from the View.
What follows is a stripped to its essentials example.
Let’s start with the main page, which will display the Discard button and, when we have discarded the record, it will display a confirmation.
<StackLayout> <Button Text="Delete" Command="{Binding DeleteCommand}" CommandParameter="{Binding Id}" /> <Label Text="{Binding Output}" /> </StackLayout>
Nothing very surprising, the button calls a command and passes the record Id as a parameter.
We create the command in the ViewModel
public ICommand DeleteCommand { get; set; }
and register it in the ViewModel’s constructor
public MainPageViewModel() { DeleteCommand = new Command<int>(HandleDelete); }
As you can see, the command parameter is bound to a property, and for this exercise, we just set that value to 2.
public int Id { get; set; } = 2;
Now, when the button is pressed, the command will fire, and will call HandleDelete, passing in the (int) parameter. At that point, the ViewModel wants the View to display a confirmation dialog box. To make that happen, we’ll declare an event in the ViewModel and fire that event in the HandleDelete method
public event Action DeleteEvent; public void HandleDelete(int Id) { DeleteEvent?.Invoke(Id); }
In our code-behind, we’ll register for that event in the OnAppearing method, which means that the DeleteEvent will not be null and thus will be invoked, and handled in the event handler in the code behind. Here is OnAppearing
protected override void OnAppearing() { base.OnAppearing(); vm = new MainPageViewModel(); BindingContext = vm; vm.DeleteEvent += DeleteEventHandler; }
The event handler pops up the dialog box. If the user confirms, then we want to call a method in the ViewModel to do the actual deletion.
private async void DeleteEventHandler(int Id) { var answer = await DisplayAlert( "Delete Record?", $"Do you want to delete record {Id}?", "Yes", "No"); if (answer==true) { vm.DeleteRecord(Id); } }
That’s it. We have a nice distribution of responsibility with the logic in the ViewModel and the dialog box in the View’s code-behind.
You can find the essential files here
Special thanks to Rodrigo Juarez
Sometimes I use the dialog service.
But sometimes I use a simpler solution.
I keep my VM without changes and isolated from the page.
In the page I create two buttons, both with a name property so I can call them from the code behind.
The first button is not visible and has a Command property binding to a VM property. The second button is visible, has a Text property and has a code behind Clicked event handler. In the handler, it calls DisplayAlert and depending on the answer, it calls (or not) the invisible button Command.Execute(param);
I’m with Rob on this – a “UserActionConfirmationService” injected into the ViewModel is the way to go here. I’ve worked on applications where ViewModels need to be reused between UI and console versions of an application, and dependency injecting the “user-facing” part so that the ViewModel can be agnostic of its implementation is invaluable. It also removes any need for code-behind in the view layer, which is also a smell to me.
This is another reason I like to keep the Views and ViewModels in different assemblies, so that if you ever find yourself having to add a reference to a UI component into a ViewModel assembly you instantly know you’re doing it wrong.
@lesscode I’m not sure where did you saw issues with reusing the viewmodel in other projects, can you give me more information?
I’m doing some testing about the service injection that you commented and so far looks good, but I want to understand what issues did you saw in Jesse’s approach
I’ve been doing this for a while but quite differently.
I create a ViewModel level Command that deals with the data level changes.
I then create a View level Command that handles the UI work and then delegates to the VM level command.
This then allows composition of your commands to enable the AreYouSure command to be reused and avoid having to write AreYouSureYouWantToDelete, AreYouSureYouWantToUndo, AreYouSureYouWantToClose etc
I started using this pattern because of navigation based UIs where you might want to create a new ViewModel and navigate to it, but you don’t want to inject the views into the constructor.
My strategy for literally the last 12 years has been to solve this with a modal dialog service that is injected into the VM. The service takes a VM to display as a modal and returns a Task that resolves to the output of the modal, in your case, a yes or no.
I’m not a fan of the fact the the code-behind calls the ViewModel’s DeleteRecord method. IMO the event should have a CancelEventArgs parameter, which would be marked as canceled if necessary in the code-behind. This way the code behind would only handle the confirmation.
I’ve been using a dialog service that every vm can access via the basevm. The same thing I always see in Laurent’s mvvmlight examples. Any reason you don’t use that?
Seems like it would be simpler to just handle the button pressed event in the code behind.