Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to DisplayAlert in a .NET MAUI ViewModel

Tags:

c#

mvvm

maui

I went through the "Build mobile and desktop apps with .NET MAUI" path on Microsoft Learn. Now that I have a simple working MAUI app, I'm trying to make it MVVM using CommunityToolkit.MVVM.

The course has a click event for called OnCall which looks like this

private async void OnCall(object sender, EventArgs e)
{
   var confirmCall = DisplayAlert(
      "Dial a Number",
      $"Would you like to call {translatedNumber}?",
      "Yes",
      "No"
   );

   if (await confirmCall)
   {
      try
      {
         PhoneDialer.Open(translatedNumber);
      }
      catch (ArgumentNullException)
      {
         await DisplayAlert("Unable to dial", "Phone number was not valid.", "OK");
      }
      catch (FeatureNotSupportedException)
      {
         await DisplayAlert("Unable to dial", "Phone dialing not supported.", "OK");
      }
      catch (Exception)
      {
         await DisplayAlert("Unable to dial", "Phone dialing failed.", "OK");
      }
   }
}

So I moved that to my ViewModel and made it a command, like this

[ICommand]
public async void OnCall ()
{
   var confirmCall = DisplayAlert(
      "Dial a Number",
      $"Would you like to call {translatedNumber}?",
      "Yes",
      "No"
   );

   if (await confirmCall)
   {
      try
      {
         PhoneDialer.Open(translatedNumber);
      }
      catch (ArgumentNullException)
      {
         await DisplayAlert("Unable to dial", "Phone number was not valid.", "OK");
      }
      catch (FeatureNotSupportedException)
      {
         await DisplayAlert("Unable to dial", "Phone dialing not supported.", "OK");
      }
      catch (Exception)
      {
         await DisplayAlert("Unable to dial", "Phone dialing failed.", "OK");
      }
   }
}

My problem is how do I call DisplayAlert from a command in the ViewModel.

like image 376
master_ruko Avatar asked Sep 01 '25 05:09

master_ruko


2 Answers

While Adarsh's answer shows the essential call, a direct reference to that UI method means your viewmodel "knows" about that UI method. That works fine (IF code is on the Main (Dispatcher) thread; if it is not, you'll get "wrong thread" exception), but will interfere with testability, if you later want to add "unit tests". Its also considered good practice to keep viewmodel independent of UI code.

This can be avoided, by accessing via an interface to a registered Service.

I use the following variation on Gerald's answer.

MauiProgram.cs:

    ...
    public static MauiApp CreateMauiApp()
    {
        ...
        builder.Services.AddSingleton<IAlertService, AlertService>();
        ...

App.xaml.cs (the cross-platform one, where MainPage is set):

    ...
    public static IServiceProvider Services;
    public static IAlertService AlertSvc;

    public App(IServiceProvider provider)
    {
        InitializeComponent();

        Services = provider;
        AlertSvc = Services.GetService<IAlertService>();

        MainPage = ...
    }

Declarations of interface and class in other files:
NOTE: The ! after Application.Current and MainPage are optional. Omit those if using a C# version less than 8. Those are to suppress "nullable" warnings. OR replace them with ? to have the method call skipped if MainPage is not set.

public interface IAlertService
{
    // ----- async calls (use with "await" - MUST BE ON DISPATCHER THREAD) -----
    Task ShowAlertAsync(string title, string message, string cancel = "OK");
    Task<bool> ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No");

    // ----- "Fire and forget" calls -----
    void ShowAlert(string title, string message, string cancel = "OK");
    /// <param name="callback">Action to perform afterwards.</param>
    void ShowConfirmation(string title, string message, Action<bool> callback,
                          string accept = "Yes", string cancel = "No");
}

internal class AlertService : IAlertService
{
    // ----- async calls (use with "await" - MUST BE ON DISPATCHER THREAD) -----

    public Task ShowAlertAsync(string title, string message, string cancel = "OK")
    {
        return Application.Current!.MainPage!.DisplayAlert(title, message, cancel);
    }

    public Task<bool> ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No")
    {
        return Application.Current!.MainPage!.DisplayAlert(title, message, accept, cancel);
    }


    // ----- "Fire and forget" calls -----

    /// <summary>
    /// "Fire and forget". Method returns BEFORE showing alert.
    /// </summary>
    public void ShowAlert(string title, string message, string cancel = "OK")
    {
        Application.Current!.MainPage!.Dispatcher.Dispatch(async () =>
            await ShowAlertAsync(title, message, cancel)
        );
    }

    /// <summary>
    /// "Fire and forget". Method returns BEFORE showing alert.
    /// </summary>
    /// <param name="callback">Action to perform afterwards.</param>
    public void ShowConfirmation(string title, string message, Action<bool> callback,
                                 string accept="Yes", string cancel = "No")
    {
        Application.Current!.MainPage!.Dispatcher.Dispatch(async () =>
        {
            bool answer = await ShowConfirmationAsync(title, message, accept, cancel);
            callback(answer);
        });
    }
}

Here is test, showing that the "fire and forget" methods can be called from anywhere:

Task.Run(async () =>
{
    await Task.Delay(2000);
    App.AlertSvc.ShowConfirmation("Title", "Confirmation message.", (result =>
    {
        App.AlertSvc.ShowAlert("Result", $"{result}");
    }));
});

NOTE: If instead you use the "...Async" methods, but aren't on the window's Dispatcher thread (Main thread), at runtime you'll get a wrong thread exception.

CREDIT: Gerald's answer to a different question shows how to get at Maui's IServiceProvider.

like image 153
ToolmakerSteve Avatar answered Sep 02 '25 18:09

ToolmakerSteve


There is multiple ways to do it. The easiest one being this:

if (await confirmCall)
{
   try
   {
      PhoneDialer.Open(translatedNumber);
   }
   catch (ArgumentNullException)
   {
      await Application.Current.MainPage.DisplayAlert("Unable to dial", "Phone number was not valid.", "OK");
   }
   catch (FeatureNotSupportedException)
   {
      await Application.Current.MainPage.DisplayAlert("Unable to dial", "Phone dialing not supported.", "OK");
   }
   catch (Exception)
   {
      await Application.Current.MainPage.DisplayAlert("Unable to dial", "Phone dialing failed.", "OK");
   }
}

What this does is go through the Application object to find the current page and call the DisplayAlert on that.

To make it a bit more maintainable (and potentially dependency injection friendly) you could wrap it in a service, for example as simple as this:

public class DialogService : IDialogService
{
    public async Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons)
    {
        return await Application.Current.MainPage.DisplayActionSheet(title, cancel, destruction, buttons);
    }

    public async Task<bool> DisplayConfirm(string title, string message, string accept, string cancel)
    {
        return await Application.Current.MainPage.DisplayAlert(title, message, accept, cancel);
    }
}

Now you can create an instance of that service and if at some point you want to show your dialogs another way, you can just swap out the implementation here.

If you decide to add the interface as well and register it in your dependency injection container, you can also let the service be injected and swap out the implementation even easier or depending on other potential variables.

The third option would be to look at a plugin like ACR.UserDialogs (Supports .NET MAUI as of version 8). Basically what this does is create its own implementation of showing a dialog on the currently visible page and give you the service for that out of the box for usage with MVVM scenarios.

like image 31
Gerald Versluis Avatar answered Sep 02 '25 17:09

Gerald Versluis