How to DisplayAlert in a .NET MAUI ViewModel
Asked Answered
M

4

30

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.

Millham answered 30/5, 2022 at 4:7 Comment(1)
What exact problem did you meet ? It's just a basic command binding on Button or something else, refer to the link : learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/… .Zhukov
S
39

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:

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.

Skillless answered 30/5, 2022 at 20:29 Comment(6)
Two things. Why would I want to call the "...Async" method instead of the fire and forget? I'm unfamiliar with the dispatcher stuff, can you point me somewhere to learn about that? I tried a quick google search but didn't find anything helpful.Millham
If you write your app using async/await, e.g. async Task MethodName(){ ... }, and you know that you are currently on the Window's Main thread (Dispatcher), then you could call those ...Async methods. The advantage is that you don't need "callback" method when you want to execute additional code after the Alert shows. The combination of async and await takes care of that for you. If you aren't writing async ... methods, then you won't use them.Skillless
@Skillless I wonder which aproach for using alert service is better: the one shown in answer with service as "global" property in App or injecting service in a constructor of every class where we want to use it like: public class Example { IAlertService _alertService; public Example(IAlertService alertService) { _alertService = alertService; } async Task ExampleMethod() => await _alertService.ShowAlertAsync("title", "message", "ok"); }Vermination
My opinion on such tradeoffs is to do whatever is easiest, until you hit a situation where you need a different solution. Since you are in control of your app code, its not hard to make a change when needed. The "lifetime" of the App class is identical to the lifetime of your app, so I see no "harm" in referring to static properties of App. If you add it as parameter to each class, then you'll also need to store it as a property in that class. To me, this is "unnecessary clutter"; I wouldn't do that without good reason. Other experienced programmers may disagree; avoiding statics if possible.Skillless
Hi ToolmakerSteve. I tried your solution and it works very well but I would like to use the MauiToolKit popups. I created one but I can't view it either from the ViewModel or from the code behind.. Do you have any suggestions? Thank you!Altigraph
While await Application.Current.MainPage.DisplayAlert works, but await this.DisplayAlert doesn't! Why is that? Both are called inside MainPage.xaml.cs which is a ContentPage and owns the DisplayAlert method.Hep
C
20

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.

Cleaver answered 30/5, 2022 at 14:22 Comment(5)
Then see In a ViewModel, how Get (GetService aka Resolve) a service added to builder.Services in MauiProgram? for explanation of how to define and use DialogService.Skillless
If you are using Shell you could do this await Shell.Current.DisplayAlert();Crus
@Gerald, the ACR.Dialogs GitHub mentions that it's locked and no new features will be developed. Is it still being added support to MAUI ?Lenticular
Support is already there. The latest version at the time of writing (8.0.1) supports .NET 6 and thus .NET MAUI.Cleaver
if you are worry about the furture of ACR.UserDIalog, you can try Controls.UserDialogs.Maui. github.com/Alex-Dobrynin/Controls.UserDialogs.MauiPrecision
H
15

Is this what u looking for?

bool x =  await Application.Current.MainPage.DisplayAlert("Tittle","Hello","OK","NotOK");
Hohenlinden answered 30/5, 2022 at 4:28 Comment(1)
Try to avoid asking questions in your answer, otherwise it could look like your post should be a comment. You could edit your answer saying something along the lines "You can access the Page/View accesing the Application.MainPage Property" and then adding you sample code.Sharpshooter
T
3

I am in fact using this way of implementing Alerts.

bool result = await AppShell.Current.DisplayAlert(title, message, accept, cancel);

But the way @ToolmakerSteve provided it opened my eyes a bit for unit testing, which too, many times i ignore. But if you need a quick DisplayAlert i found that AppShell.Current.DisplayAlert() is a quick way to do it.

Tinatinamou answered 18/2 at 11:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.