19

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.

master_ruko
  • 629
  • 1
  • 4
  • 22
  • What exact problem did you meet ? It's just a basic command binding on Button or something else, refer to the link : https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/button#use-the-command-interface . – ColeX May 30 '22 at 08:15

3 Answers3

23

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.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • 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. – master_ruko May 31 '22 at 19:50
  • If you write your app [using async/await,](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/#dont-block-await-instead) 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. – ToolmakerSteve May 31 '22 at 20:43
  • @ToolmakerSteve 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"); }` – Artur Wyszomirski Jan 24 '23 at 12:31
  • 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. – ToolmakerSteve Jan 24 '23 at 19:15
12

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.

Gerald Versluis
  • 30,492
  • 6
  • 73
  • 100
  • Then see [In a ViewModel, how Get (GetService aka Resolve) a service added to builder.Services in MauiProgram?](https://stackoverflow.com/q/72438903/199364) for explanation of how to define and use DialogService. – ToolmakerSteve May 30 '22 at 19:20
  • 8
    If you are using Shell you could do this await Shell.Current.DisplayAlert(); – Chris Parker Jul 26 '22 at 17:00
  • @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 ? – Stargazer Nov 24 '22 at 10:26
  • Support is already there. The latest version at the time of writing (8.0.1) supports .NET 6 and thus .NET MAUI. – Gerald Versluis Nov 24 '22 at 11:55
9

Is this what u looking for?

bool x =  await Application.Current.MainPage.DisplayAlert("Tittle","Hello","OK","NotOK");
Adarsh s
  • 128
  • 11
  • 5
    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](https://learn.microsoft.com/en-us/dotnet/api/xamarin.forms.application.mainpage?view=xamarin-forms)" and then adding you sample code. – Cleptus May 30 '22 at 07:18