Search code examples
c#mvvmmaui

How to DisplayAlert in a .NET MAUI ViewModel


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.


Solution

  • 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.