Through this article series, you've created several .NET MAUI pages, performed navigation, used data binding, and worked with the MVVM and DI design patterns. As you created your view models, you've set information and exception message properties. In this article, you'll build reusable components to display information, error, and validation messages on your pages. To validate user input, you're going to use data annotation attributes such as [Required] and [Range]. With just a little generic code, you can add validation to your .NET MAUI applications and display the error messages from these data annotation attributes. Sometimes you require a pop-up dialog to ask the user a question, get a little piece of data, or maybe just provide some information to the user. There are a few different dialogs you can use in .NET MAUI and you'll start this article by exploring these.
Previous articles in the series:
- Exploring .NET MAUI: Getting Started
- Exploring .NET MAUI: Styles, Navigation, and Reusable UI
- Exploring .NET MAUI: Data Entry Controls and Data Binding
- Exploring .NET MAUI: MVVM, DI, and Commanding
- Exploring .NET MAUI: Working with Lists of Data
Display Your Own Page as a Modal Dialog
Any page you create may be used as a modal dialog by calling the PushModalAsync()
method on the Navigation object. For this application, before a user can log in, they must read and accept a privacy policy, as shown in Figure 1. This policy page is displayed as a modal dialog. After the user accepts the terms, the Login button becomes enabled so the user can continue using the application. If they don't accept the policies, they're never allowed to log in. To build this functionality, create a privacy policy page and the corresponding view model, as well as a log in view model, and an application settings class to hold the Boolean of whether or not the user accepted the policies.

Create the Privacy Policy View Model
You're not going to add all the functionality for enabling or disabling the Login button yet. First, let's learn how to display a modal dialog and return to the calling page by clicking on a button. Right mouse-click on the MauiViewModelClasses
folder and add a new class named PrivacyPolicyViewModel. Replace the entire contents of this new file with the code shown in Listing 1. At this point, you don't need a corresponding view model class in the ViewModelLayer
project as you are just creating the functionality to call pages in the front-end application.
Listing 1: Create a privacy policy view model class to execute commands.
using Common.Library;
using System.Windows.Input;
namespace AdventureWorks.MAUI.MauiViewModelClasses
{
public class PrivacyPolicyViewModel : ViewModelBase
{
#region Commands
public ICommand? AccceptCommand { get; private set; }
public ICommand? DontAcceptCommand { get; private set; }
#endregion
#region Init Method
public override void Init()
{
base.Init();
// Create commands for this view
AccceptCommand = new Command(async () => await AcceptPolicyAsync());
DontAcceptCommand = new Command(async () => await
DontAcceptPolicyAsync());
}
#endregion
#region AcceptPolicyAsync Method
public async Task AcceptPolicyAsync()
{
await Shell.Current.Navigation.PopModalAsync();
}
#endregion
#region DontAcceptPolicyAsync Method
protected async Task DontAcceptPolicyAsync()
{
await Shell.Current.Navigation.PopModalAsync();
}
#endregion
}
}
This view model contains two Command objects; AcceptCommand
and DontAcceptCommand
, which will be mapped to the two buttons you saw on the privacy policy page in Figure 1. To dismiss a modal dialog call the PopModalAsync()
method, as shown in the two methods created in this view model; AcceptPolicyAsync()
and DontAcceptPolicyAsync()
.
Create the Privacy Policy Page
Right mouse-click on the Views
folder and select Add > New Item… from the menu. Click on the .NET MAUI tab, select the .NET MAUI ContentPage (XAML) template, set the Name
to PrivacyPolicyView
, and click on the Add button. Set the Title
attribute to Privacy Policy
. Add an XML namespace to reference the partial views in this project, and another to reference the view model you just created.
xmlns:partial="clr-namespace: AdventureWorks.MAUI.ViewsPartial"
xmlns:vm="clr-namespace: AdventureWorks.MAUI.MauiViewModelClasses"
Add the x:DataType
attribute to the ContentPage
starting tag so you can connect the buttons to the commands created in the view model class.
x:DataType="vm:PrivacyPolicyViewModel"
Replace the <VerticalStackLayout>
element with the XAML shown in Listing 2. Due to space limitations in this publication, I did not include all the text shown in Figure 2. Feel free to add additional text if you wish to mimic Figure 2.
Listing 2: Add a normal content page and add some text to display a privacy policy.
<Border Style="{StaticResource Border.Page}">
<ScrollView>
<Grid Style="{StaticResource Grid.Page}"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<partial:HeaderView
ViewTitle="Privacy Policy"
ViewDescription="Please read and accept the privacy policies
outlined here." />
<Label Grid.Row="1"
Text="This privacy policy..." />
<Label Grid.Row="2"
Text="We reserve the right..." />
<Label Grid.Row="3"
FontSize="Large"
Text="What User Data We Collect" />
<Label Grid.Row="4"
Text="We don't currently collect any data from you on our
app/website." />
<Label Grid.Row="5"
FontSize="Large"
Text="Links to Other Apps/Websites" />
<Label Grid.Row="6"
Text="Our app/website contains..." />
<HorizontalStackLayout Grid.Row="7">
<Button Text="I ACCEPT"
Command="{Binding AccceptCommand}" />
<Button Text="I DO NOT ACCEPT"
Command="{Binding DontAcceptCommand}" />
</HorizontalStackLayout>
</Grid>
</ScrollView>
</Border>

Open the Views\PrivacyPolicy.xaml.cs
file and replace the entire contents of this file with the code shown in Listing 3. This code should look familiar as it's typical of most pages you have developed in this article series. The PrivacyPolicyViewModel
is injected into the constructor and is assigned to the _ViewModel
variable. In the OnAppearing()
method, you set the BindingContext
of the page to the instance of the view model injected.
Listing 3: Set the privacy policy view model into the BindingContext of the page.
using AdventureWorks.MAUI.MauiViewModelClasses;
namespace AdventureWorks.MAUI.Views;
public partial class PrivacyPolicyView : ContentPage
{
public PrivacyPolicyView(PrivacyPolicyViewModel viewModel)
{
InitializeComponent();
_ViewModel = viewModel;
}
private readonly PrivacyPolicyViewModel _ViewModel;
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = _ViewModel;
}
}
To be injected into the privacy policy page, the PrivacyPolicyViewModel
needs to be added to the DI container. Open the ExtensionClasses\ServiceExtensions.cs
file and add the following line of code to the AddViewModelClasses()
method:
services.AddScoped<MauiViewModelClasses.PrivacyPolicyViewModel>();
The privacy policy page also needs to be a part of the DI process, so add the page to the AddViewClasses()
method in this same class, as shown in the following code:
services.AddScoped<PrivacyPolicyView>();
Create the Login View Model
The log in page needs a view model to hold the log in ID and password, and to call the privacy policy page. Right mouse-click on the MauiViewModelClasses
folder and add a new class named LoginViewModel. Replace the entire contents of this new file with the code shown in Listing 4. At this point, you don't need a corresponding view model class in the ViewModelLayer
project as you're just creating the functionality to call pages in the front-end application.
Listing 4: Create a view model for the login page.
using Common.Library;
using System.Windows.Input;
namespace AdventureWorks.MAUI.MauiViewModelClasses;
public class LoginViewModel : ViewModelBase
{
public LoginViewModel(PrivacyPolicyViewModel pviewModel)
{
_PrivacyViewModel = pviewModel;
}
#region Private Variables
private readonly PrivacyPolicyViewModel _PrivacyViewModel;
#endregion
#region Commands
public ICommand? PrivacyPolicyDisplay { get; private set; }
#endregion
#region Init Method
public override void Init()
{
base.Init();
// Create commands for this view
PrivacyPolicyDisplay = new Command(async () => await
PrivacyPolicyDisplayAsync());
}
#endregion
#region PrivacyPolicyDisplayAsync Method
public async Task PrivacyPolicyDisplayAsync()
{
await Shell.Current.Navigation.PushModalAsync(
new Views.PrivacyPolicyView(_PrivacyViewModel));
}
#endregion
}
The code in this front-end view model follows the same design pattern you've been using with the other view models created in this article series. However, the log in view model is going to accept an instance of the PrivacyPolicyViewModel
as it's needed to create a modal instance of the privacy policy page. Create a Command property named PrivacyPolicyDisplay
from which you can call the method PrivacyPolicyDisplayAsync()
to display the privacy policy page. The page is displayed as a modal dialog by passing a new instance of the privacy policy page to the PushModalAsync()
method of the Navigation
object.
You must now include the log in view model and the log in page in dependency injection. Open the ExtensionClasses\ServiceExtensions.cs
file and add the following line of code to the AddViewModelClasses()
method:
services.AddScoped<MauiViewModelClasses.LoginViewModel>();
The log in page also needs to be a part of the DI process, so add the page to the AddViewClasses()
method in this same class, as shown in the following code:
services.AddScoped<LoginView>();
Modify the Login View Page
In the second article of this series, you created the UI for the log in page, but didn't really hook up the view model. Let's do that now by opening the Views\LoginView.xaml
file and adding an XML namespace to reference the login view model class.
xmlns:vm="clr-namespace: AdventureWorks.MAUI.MauiViewModelClasses"
Add an x:DataType
attribute to the ContentPage starting tag so you can reference any data and commands in the log in view model class.
x:DataType="vm:LoginViewModel"
You're going to add a third button to this log in page, as shown in Figure 1, because, on a mobile device, this can cause the buttons to be cropped on the right side of the screen. Replace the entire <HorizontalStackLayout>
that contains the Login and Cancel buttons with a FlexLayout, as shown in the code below.
<FlexLayout Grid.Row="3" Grid.Column="1" Wrap="Wrap" Direction="Row">
<Button Text="Read/Accept Privacy Policy" Style="{StaticResource flexButton}"
Command="{Binding PrivacyPolicyDisplay}" />
<Button Text="Login" Style="{StaticResource flexButton}" Margin="5" />
<Button Text="Cancel" Style="{StaticResource flexButton}" Margin="5" />
</FlexLayout>
To ensure the buttons within the FlexLayout control have a little separation in case they should wrap to another line, add a <ContentPage.Resources>
element just after the <ContentPage>
starting tag. Add a margin to each button with a spacing of five, as shown in the code below:
<ContentPage.Resources>
<Style TargetType="Button" x:Key="flexButton">
<Setter Property="Margin" Value="5" />
</Style>
</ContentPage.Resources>
The log in page needs to access the log in view model to be able to call the PrivacyPolicyDisplay command. Open the Views\LoginView.xaml.cs
file and replace the entire contents of this file with the code shown in Listing 5. This code injects the LoginViewModel
into the constructor and assigns it to the private variable named _ViewModel
.
Listing 5: Add the view model to the login page.
using AdventureWorks.MAUI.MauiViewModelClasses;
namespace AdventureWorks.MAUI.Views;
public partial class LoginView : ContentPage
{
public LoginView(LoginViewModel viewModel)
{
InitializeComponent();
_ViewModel = viewModel;
}
private readonly LoginViewModel _ViewModel;
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = _ViewModel;
}
}
Try It Out
Run the application and click on the Login menu. Click on the Read Privacy Policy button to display the privacy policy page shown in Figure 2. Click on either of the buttons to return to the log in page.
Note in Figure 2 that the menus and all other navigation is removed from the shell. No other keys or clicking anywhere else removes this modal dialog. You must click on one of the buttons presented.
Returning Value(s) from a Modal Page
To determine which button was pressed when the modal page was closed, you need some way to communicate that information to the calling page. You can do this using the [QueryProperty]
attribute and modifying the OnAppearing()
event procedure. Open the Views\LoginView.xaml.cs
file and add two public properties.
public string? LastPage
{
get;
set;
}
public bool IsPrivacyPolicyAccepted
{
get;
set;
}
Add two [QueryProperty]
attributes to map the values passed when navigating to each property.
[QueryProperty(nameof(LastPage), "lastPage")]
[QueryProperty(nameof(IsPrivacyPolicyAccepted), "isPrivacyPolicyAccepted")]
In the OnAppearing()
event procedure, shown in Listing 6, check to ensure that the LastPage
property is not null. If it isn't, check to see if the LastPage
is set to the value PrivacyPolicyView
. If both conditions are true, you can check the IsPrivacyPolicyAccepted
property to determine which button was pressed.
Listing 6: Add code to determine what page called this page.
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = _ViewModel;
if (LastPage != null && LastPage == nameof(Views.PrivacyPolicyView))
{
if (IsPrivacyPolicyAccepted)
{
Console.WriteLine("Privacy Policy was Accepted");
}
else
{
Console.WriteLine("Privacy Policy was NOT Accepted");
}
}
LastPage = null;
}
Before you use the PopModalAsync()
method in the privacy view model to return to the login page after clicking on a button. Let's change that to use the GoToAsync()
method passing in "..", which is the same as popping a page off the navigation stack. Onto the "..", add two query parameters: lastPage
and isPrivacyPolicyAccepted
. Open the MauiViewModelClasses\PrivacyPolicyViewModel.cs
file and locate the AcceptPolicyAsync()
method and modify it as shown in the following code:
public async Task AcceptPolicyAsync()
{
string ret = "..?lastPage=";
ret += $"{nameof(Views.PrivacyPolicyView)}";
ret += "&isPrivacyPolicyAccepted=true";
await Shell.Current.GoToAsync(ret);
}
You need to do the same procedures in the DontAcceptPolicyAsync()
method as shown in the following code. Be sure to pass a false
value to the isPrivacyPolicyAccepted
parameter.
protected async Task DontAcceptPolicyAsync()
{
string ret = "..?lastPage=";
ret += $"{nameof(Views.PrivacyPolicyView)}";
ret += "&isPrivacyPolicyAccepted=false";
await Shell.Current.GoToAsync(ret);
}
Try It Out
Set a breakpoint on the line of code if (IsPrivacyPolicyAccepted) in the OnAppearing()
method of the log in page. Run the application and click on the Login menu. Click on either button and you should hit the breakpoint in the OnAppearing()
event.
Add Application Settings Class
You can now display the privacy page as a modal dialog from the log in page and get the value back from whichever button was pressed. You now need someplace to record that the rest of the application knows whether the terms were accepted. In most applications, you have a global settings class to hold information about the entire application. One of those settings is whether the user has accepted the privacy policies. This property is set when the value is returned from the privacy policy page. Let's create that global settings class. Right mouse-click on the AdventureWorks.MAUI project and add a new folder named ConfigurationClasses
. Right mouse-click on the ConfigurationClasses
folder and add a new class named AppSettings
. Replace the entire contents of this new file with the code shown in Listing 7.
Listing 7: Add a new class
using Common.Library;
namespace AdventureWorks.MAUI.ConfigurationClasses;
public class AppSettings : CommonBase
{
#region Private Variables
private bool _IsPrivacyPolicyAccepted;
#endregion
#region Public Properties
public bool IsPrivacyPolicyAccepted
{
get { return _IsPrivacyPolicyAccepted; }
set
{
_IsPrivacyPolicyAccepted = value;
RaisePropertyChanged(nameof(IsPrivacyPolicyAccepted));
}
}
#endregion
}
This AppSettings
class needs to participate in DI so you must register it with the DI container. Open the ExtensionClasses\ServiceExtensions.cs
file and add a new method named AddOtherClasses(),
as shown in the following code. Only a single instance of this class should be available throughout the entire application, so register it with DI using the AddSingleton()
method.
private static void AddOtherClasses(IServiceCollection services)
{
// Add Other Classes
services.AddSingleton<ConfigurationClasses.AppSettings>();
}
This new method needs to be called by the public method on the ServiceExtensions
class. Locate and modify the AddServicesToDIContainer()
method and call the AddOtherClasses()
method, as shown in the code below:
public static void AddServicesToDIContainer(this IServiceCollection services)
{
// Add Repository Classes
AddRepositoryClasses(services);
// Add View Model Classes
AddViewModelClasses(services);
// Add View Classes
AddViewClasses(services);
// Add Other Classes
AddOtherClasses(services);
}
Add Settings Class to Privacy Policy View Model
Open the MauiViewModelClasses\LoginViewModel.cs
file and add a Using
statement at the top of the file. This namespace is where the AppSettings
class is located.
using AdventureWorks.MAUI.ConfigurationClasses;
private AppSettings _Settings;
Add a public property to expose the AppSettings
object so you can bind the IsPrivacyPolicyAccepted
property to the Login button on the page.
#region Public Properties
public AppSettings Settings
{
get { return _Settings; }
set { _Settings = value; }
}
#endregion
Add a constructor to the LoginViewModel
class and inject an instance of the AppSettings
class, as shown in the following code snippet:
public LoginViewModel(PrivacyPolicyViewModel pviewModel, AppSettings settings)
{
_PrivacyViewModel = pviewModel;
_Settings = settings;
}
Open the Views\LoginView.xaml.cs
file and modify the OnAppearing()
method to look like the following. Once you determine if the last page was the privacy policy page, set the Settings.IsPrivacyPolicyAccepted
property to the value returned from the that page.
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = _ViewModel;
if (LastPage != null && LastPage == nameof(Views.PrivacyPolicyView))
{
_ViewModel.Settings.IsPrivacyPolicyAccepted = IsPrivacyPolicyAccepted;
}
LastPage = null;
}
Open the Views\LoginView.xaml
file and locate the Login button. Add the IsEnabled
attribute to bind to the Settings.IsPrivacyPolicyAccepted
property in the LoginViewModel
class.
<Button Text="Login" IsEnabled="{Binding Settings.IsPrivacyPolicyAccepted}" />
Try It Out
Run the application and click on the Login menu. Notice that the Login button is disabled, as shown in Figure 3. Click on the Read/Accept Privacy Policy button and click the I ACCEPT button. Upon returning to the log in page, you should see that the Login button is enabled. Click on the Read/Accept Privacy Policy button one more time, but this time click the I DON'T ACCEPT button. You should see that the Login button is now disabled on the log in page.

Working with Pop-Up Dialogs
Sometimes in your business applications you want to display a simple pop-up message to a user or maybe ask them a Yes/No question. Instead of creating your own page to do this, .NET MAUI supplies you with a few different pop-up dialogs to use. .NET MAUI has three classes for displaying different prompts. These methods, DisplayAlert()
, DisplayPromptAsync()
, and DisplayActionSheet()
are available on the Page
class. The pop-up rendered will be different based on the operating system you're running your application upon.
The ContentPage inherits from the Page
class, so these methods are available on all your pages. This means you can call these methods in the code-behind of any page. When called from within the code-behind, the dialogs are displayed on the UI thread. This is important, because if you tried calling these methods from a view model in your ViewModelLayer project, there's no guarantee that you're on the UI thread and thus an exception may be raised. Also, your ViewModelLayer project would no longer be reusable as this would make your view models dependent on .NET MAUI and thus could not be reused.
You may call these dialogs from the view model classes you created within the .NET MAUI application. Because these view models are injected into the code-behind of a ContentPage class they are running on the UI thread. Use the Shell.Current.CurrentPage
object to invoke each of the popup methods to display these dialogs.
Display an Alert Dialog
The DisplayAlert()
method on the ContentPage object might be used to provide confirmation that an action was successful. The user must click on the button of this dialog to continue. The first three parameters you pass to the DisplayAlert()
method are a title, a message, and the text to display on the button, as shown in Figure 4. The title is displayed in a large, bold font. The message is displayed immediately under the title in a smaller, non-bold font. Open the MauiViewModelClasses\UserViewModel.cs
file and locate the SaveAsync()
method and immediately after the if(ret != null) statement, add the following code:
await Shell.Current.CurrentPage.DisplayAlert(
"Saved", "Data Has Been Saved", "OK"
);

Try It Out
Run the application, click on the Users menu, then click on the edit button on a specific user. Press the Save button and you should see a dialog appear as shown in Figure 4. To clear this dialog on Windows, click the OK button or press the escape key. On mobile platforms you may click on the OK button, or just press anywhere outside of the button to clear it.
I wouldn't use a dialog in this manner. I don't think you need to provide confirmation when an operation succeeds. I like to only inform a user when something goes wrong. When you eventually write the SaveAsync()
method in the UserViewModel
declared in the ViewModelLayer set the InfoMessage
property to some text to inform the user that the record was saved successfully. This message will appear briefly in the information message area. This is confirmation enough to the user that their operation was successful. This makes using this dialog unnecessary. Stop the application and remove this line of code to display this dialog.
Alert With Yes/No for Deleting Data
You may also pass four string parameters to the DisplayAlert()
method. The fourth parameter becomes the cancel button, and the third parameter becomes the accept button. When the accept button is pressed, true is returned from this method. When the Cancel button is pressed, false is returned from this method.
On the user and product lists, you have a delete button represented by a trash can icon. When a user clicks on that button, before deleting the items, you should ask the user if they really want to perform this operation. Use the same DisplayAlert()
method but add two buttons, Yes and No, to the dialog. Before calling the DisplayAlert()
method, let's add the appropriate commanding infrastructure. Open the MauiViewModelClasses\UserViewModel.cs
file and add a new ICommand, as shown in the following code:
public ICommand? DeleteCommand
{
get;
private set;
}
In the Init()
method create an instance of the Command class and assign it to this DeleteCommand
property.
DeleteCommand = new Command(
async () => await DeleteAsync()
);
Add the DeleteAsync()
method referenced by the DeleteCommand
property, as shown in Listing 8. This method declares a variable named ret
to return a true
or false
value. The DisplayAlert()
method is invoked passing in “Delete?” for the title, and “Delete This User?” for the message. The accept button is displayed with the text “Yes”, while the cancel button is displayed with the text “No”. If the accept button is clicked, a true
value is returned and assigned to the perform
variable. In the if statement is call the DeleteAsync()
in the UserViewModel
contained in the ViewModelLayer project. This method returns an instance of the user object that was deleted. If it isn't deleted, a null value is returned. If the item is deleted, invoke the GetAsync()
method again to refresh the list with all the values from the data store. This part is optional, you could just delete the user object returned from the list. Feel free to code this either way you wish.
Listing 8: Use the DisplayAlert() method to ask the user a yes/no question.
#region DeleteAsync Method
public async Task<bool> DeleteAsync()
{
bool ret = false;
bool perform = await Shell.Current.CurrentPage.DisplayAlert(
"Delete?", "Delete This User?", "Yes", "No");
if (perform)
{
// TODO: Perform delete here
User? response = new();
if (response != null)
{
// Redisplay List
await GetAsync();
}
}
return ret;
}
#endregion
Open the Views\UserListView.xaml
file and locate the existing delete button within the ListView. Add the CommandParameter
and Command
attributes to call the DeleteCommand
property you added to your view model.
<ImageButton
Source="trash.png"
CommandParameter="{Binding UserId}"
Command="{Binding Source={x:Reference UserListPage},
Path=BindingContext.DeleteCommand}"
ToolTipProperties.Text="Delete User"
/>
Try It Out
Run the application and click on the Users menu. Click on one of the users' delete button and view the alert with the yes/no buttons, as shown in Figure 5. You haven't written any delete functionality in your view model class, so this is just an example of how to make the call to the method that would delete data from your data store.

Ask User to Input a Single Value
To ask the user for a single piece of input, call the DisplayPromptAsync()
method, as shown in the code snippet below, to display a dialog that looks like Figure 6.
string response = await Shell.Current.CurrentPage.DisplayPromptAsync(
"Name",
"What's your first name?");
Console.WriteLine(response);

If you click the Cancel button or press the escape key on Windows, a null value is returned into the response
variable. On mobile platforms, you may click on the Cancel button, or just press anywhere outside of the button to return a null value into the response
variable.
Add a Placeholder
The previous example of DisplayPromptAsync()
is just one of several overloads and optional parameters you may set on this dialog. For example, if you wish to have placeholder text in the input area (Figure 7) pass a string to the placeholder
argument when calling DisplayPromptAsync(),
as shown in the following code:
string response = await DisplayPromptAsync(
{
"Name",
"What's your first name?",
placeholder: "Enter Name"
};
Console.WriteLine(response);

Add an Initial Value
To display an initial value when the pop-up is first displayed, pass a string to the placeholder
argument when calling DisplayPromptAsync(),
as shown in Figure 8. The following code shows passing arguments to both the placeholder
and the initialValue
parameters. If you delete the initial value, the placeholder is once again displayed within the input area.
string response = await DisplayPromptAsync(
"Name",
"What's your first name?",
placeholder: "Enter Name",
initialValue: "John"
);
Console.WriteLine(response);

Set MaxLength and Change the Keyboard
You may restrict the number of characters the user is allowed to enter by passing in a numeric value to the maxLength
parameter. If you are asking a user for numeric input, you may wish to only display the numeric keyboard on mobile devices. The following code passes arguments to both the maxLength
and the keyboard
parameters to display the pop-up shown in Figure 9.
string response = await Shell.Current.CurrentPage.DisplayPromptAsync(
"Age",
"What's your age?",
initialValue: "1",
maxLength: 3,
keyboard: Keyboard.Numeric
);
Console.WriteLine(response);

Display an Action Sheet
The DisplayActionSheet()
method presents one or more options to a user from which they may select a single value. The following code displays a dialog that looks like Figure 10.
string action = await Shell.Current.CurrentPage.DisplayActionSheet(
"Post Text To...",
"Cancel",
null,
"LinkedIn",
"X (formerly Twitter)",
"Facebook"
);
Console.WriteLine(action);

When the user clicks on one of the values presented, that value is returned and placed into the variable action
. The action
variable will be “LinkedIn”, "X (formerly Twitter)", or “Facebook”. If the Cancel button is pressed, the action
variable is set to “Cancel”. On Windows, you may press the escape key, which is the same as pressing the Cancel button. On mobile devices, you may press anywhere outside the dialog and that's also the same as pressing the Cancel button.
Display a Second Button on Action Sheet
If you pass a string to the third parameter instead of a null
value, that string is presented as a second button (see Figure 11) as shown in the following code snippet. This third parameter is known as a destruction button as it's generally used to let the user dispose of something instead of saving something. If this button is pressed, the text of the button is returned in the action
variable.
string action = await Shell.Current.CurrentPage.DisplayActionSheet(
"Save Product Photo?",
"Cancel",
"Delete Photo",
"To Photo Roll",
"To File",
"As Email Attachment"
);
Console.WriteLine(action);

Action sheets can be dismissed on any touch platform and MacCatalyst by tapping on the page outside of the action sheet. On a Windows desktop, action sheets can be cleared by pressing the escape key, clicking the Cancel button, or by clicking on the page outside the action sheet.
Displaying Informational Messages
Throughout the last couple of articles, you've set the InfoMessage
and the LastErrorMessage
properties to various strings to display. However, these properties aren't bound anywhere on your pages, so let's add some reusable components to display these different messages. On most pages, you're going to want to have areas where you can display informational, exception, and validation messages. Informational messages are used to inform users about something that's currently happening, or the result of some operation. Exception messages inform the user that some error occurred while attempting to process data. Validation messages inform the user of incorrect data entry on their part.
Add Properties to the View Model Base Class
Before you learn about validation messages, let's first tackle the information and exception messages. Create a couple of Boolean properties you can bind to make the information and exception message areas visible or not. Open the BaseClasses\ViewModelBase.cs
file in the Common.Library project and add two new private variables, as shown in the following code snippet:
private bool _IsInfoAreaVisible;
private bool _IsExceptionAreaVisible;
Create two public properties to expose these two private variables, as shown in Listing 9. Each of the setters raises the PropertyChanged
event so when they are set, the different message areas become visible.
Listing 9: Create two properties to display messages on your pages.
public bool IsInfoAreaVisible
{
get { return _IsInfoAreaVisible; }
set
{
_IsInfoAreaVisible = value;
RaisePropertyChanged(nameof(IsInfoAreaVisible));
}
}
public bool IsExceptionAreaVisible
{
get { return _IsExceptionAreaVisible; }
set
{
_IsExceptionAreaVisible = value;
RaisePropertyChanged(nameof(IsExceptionAreaVisible));
}
}
In the GetAsync()
and GetAsync(id)
methods in your view models call the repository class to attempt to get data. Just before this call, invoke a method in the ViewModelBase
class called BeginProcessing()
. Just before the end of each method, call another method named EndProcessing()
. Within each of these methods, set the view model properties to reflect the appropriate state at that moment in time. In the BeginProcessing()
method, shown below, set the properties to a valid start state just before you call the repository
method to load or save data. Add the following method to the ViewModelBase
class:
#region BeginProcessing Method
protected virtual void BeginProcessing()
{
InfoMessage = string.Empty;
LastErrorMessage = string.Empty;
LastException = null;
RowsAffected = 0;
IsInfoAreaVisible = true;
IsExceptionAreaVisible = false;
}
#endregion
The EndProcessing()
method is called after the processing has succeeded or has failed with an exception. This method checks to see if the LastErrorMessage
is not null or empty, or that the LastException
property is not null. If either of these conditions are true, then the IsExceptionAreaVisible
property is set to true. Add the following method to the ViewModelBase
class:
#region EndProcessing Methods
protected virtual void EndProcessing()
{
if (!string.IsNullOrEmpty(LastErrorMessage) || LastException != null)
{
IsExceptionAreaVisible = true;
}
}
#endregion
Modify the User View Model Class
Open the ViewModelClasses\UserViewModel.cs
file. In the GetAsync()
method, remove the line of code RowsAffected = 0;
at the beginning of the method. Call the BeginProcessing()
method just before the try block. Call the EndProcessing()
method just after the closing brace for the catch block, as shown in the following code snippet:
public async Task<ObservableCollection<User>> GetAsync()
{
BeginProcessing();
try
{
// REST OF THE CODE HERE
}
catch (Exception ex)
{
PublishException(ex);
}
EndProcessing();
return Users;
}
In the GetAsync(id)
method, call the BeginProcessing()
method just before the try block. Call the EndProcessing()
method just after the closing brace for the catch block. Within the catch block, remove the RowsAffected = 0;
line of code, as shown in the following code snippet:
public async Task<User?> GetAsync(int id)
{
BeginProcessing();
try
{
// REST OF THE CODE HERE
}
catch (Exception ex)
{
PublishException(ex);
}
EndProcessing();
return CurrentEntity;
}
Modify the Product View Model
Modify the product view model class the same as you did in the user view model class. Open the ViewModelClasses\ProductViewModel.cs
file. In the GetAsync()
method, remove the line of code RowsAffected = 0; at the beginning of the method. Call the BeginProcessing()
method just before the try block. Call the EndProcessing()
method just after the closing brace for the catch block.
In the GetAsync(id)
method, call the BeginProcessing()
method just before the try block. Call the EndProcessing()
method just after the closing brace for the catch block. Within the catch block, remove the RowsAffected = 0; line of code. Feel free to update the ColorViewModel
class with these changes as well.
Partial Message Pages
To provide the most reusable code, create a set of partial pages to display information, exception, and validation messages. Even though the information and exception partial pages are just a simple Label control, this gives you the flexibility to change the look of the message display on all pages by just changing these in a single location.
Create the Information Message Page
Right mouse-click on the ViewsPartial
folder and select Add > New Item… > .NET MAUI > Content View (XAML), set the name to InfoMessageArea, and click on the Add button. In the <ContentView>
starting tag, add a new XML namespace to reference the Common.Library
.
xmlns:common="clr-namespace: Common.Library;assembly=Common.Library"
Add an x:DataType
attribute to the <ContentView>
starting tag to bind properties from the ViewModelBase class to this content view control.
x:DataType="common:ViewModelBase"
Replace the <VerticalStackLayout>
element with the following XAML. This label control is bound to the InfoMessage
property. All information messages are displayed by this label.
<Label IsVisible="{Binding IsInfoAreaVisible}"
Text="{Binding Path=InfoMessage}"
Style="{StaticResource InfoMessageArea}" />
Open the Resources\Styles\CommonStyles.xaml
file in the Common.Library.MAUI project and add a new keyed style, as shown in the following XAML.
<Style TargetType="Label"
x:Key="InfoMessageArea">
<Setter Property="FontSize"
Value="18" />
<Setter Property="Margin"
Value="4,4" />
</Style>
Create the Exception Message Page
Right mouse-click on the ViewsPartial
folder and select Add > New Item… > .NET MAUI > Content View (XAML), set the name to ExceptionMessageArea
, and click on the Add button. In the <ContentView>starting
tag, add a new XML namespace to reference the Common.Library
.
xmlns:common="clr-namespace: Common.Library;assembly=Common.Library"
Add an x:DataType
attribute to the <ContentView>
starting tag to bind properties from the ViewModelBase
class to this content view control.
x:DataType="common:ViewModelBase"
Replace the <VerticalStackLayout>
element with the following XAML. This label control is bound to the LastErrorMessage
property. All exception messages are displayed by this label.
<Label
IsVisible="{Binding IsExceptionAreaVisible}"
Text="{Binding LastErrorMessage}"
Style="{StaticResource ErrorMessage}" />
Open the Resources\Styles\CommonStyles.xaml
file in the Common.Library.MAUI project and add a new keyed style, as shown in the following XAML:
<Style TargetType="Label" x:Key="ErrorMessage">
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="TextColor" Value="Red" />
</Style>
Modify the List Views to Display Informational Messages
It's now time to add these two new partial views to the pages you've created so far. The information message area is going to be displayed just below the reusable header view. Open the Views\UserListView.xaml
file and add a new row definition to the RowDefinitions
attribute so it looks like the XAML shown below:
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
In between the <partial:HeaderView …>
and the <ListView>
, add the Information Message Partial Page.
<!-- ************************ -->
<!-- Information Message Area -->
<!-- ************************ -->
<partial:InfoMessageArea Grid.Row="1" Grid.ColumnSpan="2" />
Because you inserted the information message control before the ListView control, modify the Grid.Row
property on the ListView control to “2”, as shown in the following code snippet:
<ListView Grid.Row="2" ...>
Modify the Product List View
Open the Views\ProductListView.xaml
file and add a new row definition so the RowDefinitions
looks like the XAML shown below.
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
In between the <partial:HeaderView …>
and the <CollectionView>
, add the Information Message Partial Page.
<!-- ************************ -->
<!-- Information Message Area -->
<!-- ************************ -->
<partial:InfoMessageArea Grid.Row="1" Grid.ColumnSpan="2" />
Because you inserted the information message control before the CollectionView control, modify the Grid.Row
property on the CollectionView
control to “2”, as shown in the following code snippet:
<CollectionView Grid.Row="2" ...>
Try It Out
Run the application, click on the Users menu and you should see the informational message “Found 3 Users” displayed. Click on the Products menu and see the informational message “Found 4 Products” displayed.
Display Processing Messages
Open the ViewModelClasses\UserViewModel.cs
file and add in the GetAsync()
method. Just before the call to await Repository.GetAsync(), add the following informational message:
InfoMessage = "Please wait while loading users.";
To simulate this process taking a long time, temporarily add an additional line just below this message.
await Task.Run(() =>
{
Thread.Sleep(2000);
});
Open the ViewModelClasses\ProductViewModel.cs
file and add in the GetAsync()
method, just before the await Repository.GetAsync()
call, add the following informational message.
InfoMessage = "Please wait while loading products.";
Try It Out
Run the application, click on the Users menu, and you should see informational message Please wait while loading users. displayed. After about two seconds, this message is removed, and the Found 3 Users message should appear. Remove the Thread.Sleep()
call in the UsersViewModel class.
Display Error Messages
Open the Views\UserListView.xaml
file and add a new row definition so the RowDefinitions
looks like the XAML shown below:
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Insert the exception message partial page just after the information message area.
<!-- ****************** -->
<!-- Error Message Area -->
<!-- ****************** -->
<partial:ExceptionMessageArea Grid.Row="2" Grid.ColumnSpan="2" />
Change the Grid.Row
property on the <ListView>
to “3”.
<ListView Grid.Row="3" ...>
Modify the Product List View
Open the Views\ProductListView.xaml
file and add a new row definition so the RowDefinitions
looks like the XAML shown below:
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Insert the exception message partial page just after the information message area.
<!-- ****************** -->
<!-- Error Message Area -->
<!-- ****************** -->
<partial:ExceptionMessageArea Grid.Row="2" Grid.ColumnSpan="2" />
Change the Grid.Row
property on the <CollectionView>
to “3”.
<CollectionView Grid.Row="3" ...>
Open the ExtensionClasses\ServiceExtensions.cs
file and, in the AddRepositoryClasses()
method, comment out the line that adds the UserRepository
to the DI container.
// services.AddScoped<IRepository<User>, UserRepository>();
Try It Out
Run the application and click on the Users menu and, toward the bottom of the page, you should see the error message The Repository Object is not Set displayed. Open the ExtensionClasses\ServiceExtensions.cs
file and restore the line you previously commented so you won't get that error again.
Display Error Messages on Detail Views
Open the Views\UserDetailView.xaml
file and add a new Auto to the RowDefinition
attribute. Scroll down to the bottom of the file, locate the </HorizontalStackLayout>
closing tag, and insert the following XAML in between this element and the </Grid>
closing tag.
<!-- ****************** -->
<!-- Error Message Area -->
<!-- ****************** -->
<partial:ExceptionMessageArea Grid.Row="12" Grid.ColumnSpan="2" />
Modify Product Detail View
Open the Views\ProductDetailView.xaml
file and add a new Auto to the RowDefinition
attribute. Scroll down to the bottom of the file, locate the </HorizontalStackLayout>
closing tag, and insert the following XAML in between this element and the </Grid>
closing tag:
<!-- ****************** -->
<!-- Error Message Area -->
<!-- ****************** -->
<partial:ExceptionMessageArea Grid.Row="16" Grid.ColumnSpan="2" />
Validate Data Using Data Annotations
Data annotations are not just for use in ASP.NET web applications. Any type of .NET application can use data annotations for validating data. It only takes about 10 lines of code to programmatically validate data annotations attached to entity classes. There are many built-in data annotations supplied by Microsoft to validate your data quickly. For more information on using data annotations, see my article entitled The Rich Set of Data Annotation and Validation Attributes in .NET in the January/February 2023 issue of CODE Magazine.
Add Data Annotations to Entity Class
Let's add some data annotations to the User class to enforce business rules, such as which fields are required, have a maximum and/or minimum string length, or have a range of values it must fall within. Open the EntityClasses\User.cs
file in the AdventureWorks.EntityLayer
project and replace the entire file with the code shown in Listing 10.
Listing 10: Add data annotations to your class to enforce business rules on the data entered by the user.
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Common.Library;
namespace AdventureWorks.EntityLayer;
/// <summary>
/// This class contains properties that
/// map to each field in the dbo.User table.
/// </summary>
[Table("User", Schema = "dbo")]
public partial class User : EntityBase
{
#region Private Variables
private int _UserId;
private string _LoginId = string.Empty;
private string _FirstName = string.Empty;
private string _LastName = string.Empty;
private string _Email = string.Empty;
private string _Password = "P@ssW0Rd";
private string _Phone = string.Empty;
private string? _PhoneType;
private bool? _IsFullTime;
private bool? _IsEnrolledIn401k;
private bool? _IsEnrolledInHealthCare;
private bool? _IsEnrolledInHSA;
private bool? _IsEnrolledInFlexTime;
private bool? _IsEmployed;
private DateTime? _BirthDate;
private TimeSpan? _StartTime;
#endregion
#region Public Properties
[Display(Name = "User Id")]
[Required(ErrorMessage = "{0} must be filled in.")]
public int UserId
{
get { return _UserId; }
set
{
_UserId = value;
RaisePropertyChanged(nameof(UserId));
}
}
[Display(Name = "Login Id")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(20, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
public string LoginId
{
get { return _LoginId; }
set
{
_LoginId = value;
RaisePropertyChanged(nameof(LoginId));
}
}
[Display(Name = "First Name")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(50, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
public string FirstName
{
get { return _FirstName; }
set
{
_FirstName = value;
RaisePropertyChanged(nameof(FirstName));
}
}
[Display(Name = "Last Name")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(50, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
public string LastName
{
get { return _LastName; }
set
{
_LastName = value;
RaisePropertyChanged(nameof(LastName));
}
}
[Display(Name = "Email")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(256, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
[EmailAddress()]
public string Email
{
get { return _Email; }
set
{
_Email = value;
RaisePropertyChanged(nameof(Email));
}
}
[Display(Name = "Password")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(50, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
public string Password
{
get { return _Password; }
set
{
_Password = value;
RaisePropertyChanged(nameof(Password));
}
}
[Display(Name = "Phone")]
[Required(ErrorMessage = "{0} must be filled in.")]
[StringLength(25, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
[Phone()]
public string Phone
{
get { return _Phone; }
set
{
_Phone = value;
RaisePropertyChanged(nameof(Phone));
}
}
[Display(Name = "Phone Type")]
[StringLength(10, MinimumLength = 0, ErrorMessage = "{0} must be between
{2} and {1} characters long.")]
public string? PhoneType
{
get { return _PhoneType; }
set
{
_PhoneType = value;
RaisePropertyChanged(nameof(PhoneType));
}
}
[Display(Name = "Is Full Time")]
public bool? IsFullTime
{
get { return _IsFullTime; }
set
{
_IsFullTime = value;
RaisePropertyChanged(nameof(IsFullTime));
}
}
[Display(Name = "Is Enrolled In 40 1K")]
public bool? IsEnrolledIn401k
{
get { return _IsEnrolledIn401k; }
set
{
_IsEnrolledIn401k = value;
RaisePropertyChanged(nameof(IsEnrolledIn401k));
}
}
[Display(Name = "Is Enrolled In Health Care")]
public bool? IsEnrolledInHealthCare
{
get { return _IsEnrolledInHealthCare; }
set
{
_IsEnrolledInHealthCare = value;
RaisePropertyChanged(nameof(IsEnrolledInHealthCare));
}
}
[Display(Name = "Is Enrolled In Hsa")]
public bool? IsEnrolledInHSA
{
get { return _IsEnrolledInHSA; }
set
{
_IsEnrolledInHSA = value;
RaisePropertyChanged(nameof(IsEnrolledInHSA));
}
}
[Display(Name = "Is Enrolled In Flex Time")]
public bool? IsEnrolledInFlexTime
{
get { return _IsEnrolledInFlexTime; }
set
{
_IsEnrolledInFlexTime = value;
RaisePropertyChanged(nameof(IsEnrolledInFlexTime));
}
}
[Display(Name = "Is Employed")]
public bool? IsEmployed
{
get { return _IsEmployed; }
set
{
_IsEmployed = value;
RaisePropertyChanged(nameof(IsEmployed));
}
}
[Display(Name = "Birth Date")]
public DateTime? BirthDate
{
get { return _BirthDate; }
set
{
_BirthDate = value;
RaisePropertyChanged(nameof(BirthDate));
}
}
[Display(Name = "Start Time")]
public TimeSpan? StartTime
{
get { return _StartTime; }
set
{
_StartTime = value;
RaisePropertyChanged(nameof(StartTime));
}
}
public string FullName
{
get { return FirstName + " " + LastName; }
}
public string LastNameFirstName
{
get { return LastName + ", " + FirstName; }
}
#endregion
#region ToString Override
public override string ToString()
{
return $"{LastName}";
}
#endregion
}
Microsoft Data Annotations
Instead of writing validation code in your view model class as you did in Listing 1, add attributes above those properties in your User class that you wish to validate. There are many standard data annotation attributes supplied by Microsoft such as [Required]
, [MinLength]
, [MaxLength]
, [StringLength]
, and [Range]
. From the name of the attribute, you can infer what each of these attributes validates on a property. For a complete list of data annotation attributes, visit Microsoft's website at https://bit.ly/3TJICid.
The [Required] Attribute
The various [Required] attributes shown in Listing 10 do exactly what you think: It ensures that the user has filled in some data in the input field bound to the property that this attribute decorates. Each of the data annotation attributes is inherited from an abstract class named ValidationAttribute
. This validation attribute class has properties that each of the inherited attribute classes can use. These properties are shown in Table 1 and that you see in several in the attributes in Listing 10.
The ErrorMessage Property
The ErrorMessage
property is what you use to report back the error message to the user. Use a hard-coded string or use placeholders in the string to automatically retrieve data from the property the attribute is decorating. The placeholders are what the String.FormatString()
method uses where you add numbers enclosed within curly braces, as shown in the following [Required] attribute:
[Required(ErrorMessage = "{0} Must Be Filled In")]
public string FirstName
{
get;
set;
}
The {0} placeholder is replaced with the name of the property the attribute is decorating. In the above example, the resulting string is “FirstName Must Be Filled In”. Next, look at the following code snippet that uses the [Range] attribute.
[Range(0.01, 9999,
ErrorMessage = "{0} must be between {1} and {2}")]
public decimal? Price
{
get;
set;
}
The {1} placeholder is replaced with the value in the first parameter to the Range
attribute and the {2} placeholder is replaced with the value in the second parameter. If you have more parameters, you keep on incrementing the placeholder accordingly.
The [DisplayName] Attribute
Displaying the property name to the user is generally not a good idea. Sometimes the property name won't mean much to the user. It's better to use a more readable string, such as the same label displayed on an input form. You can accomplish this by adding the [DisplayName]
attribute to any property in your class. In Listing 10, every property has a [DisplayName]
attribute so it can have a friendly label to display to the user if the property fails validation. If the [DisplayName]
attribute is attached to a property, the {0} placeholder in the ErrorMessage
property uses the Name
property from the [DisplayName]
attribute instead of the actual property name.
Other Attributes
Other attributes you see in Listing 10 are StringLength
, EmailAddress
, and Phone
. These attributes ensure that a user enters a string value between the minimum and maximum lengths specified. The EmailAddress
and Phone
attributes validate that the user entered a valid email address and valid phone number, respectively.
Add Validation Classes to the Common Library
To display a list of which fields don't pass the validation tests specified by the various attributes, create a class to hold the property name that failed and the validation message to display to the user. Right mouse-click on the Common.Library project and add a new folder named ValidationClasses
. Right mouse-click on the ValidationClasses
folder and add a new class named ValidationMessage
. Replace the entire contents of this new file with the code shown in Listing 11.
Listing 11: Create a class to hold the validation message to display to the user.
namespace Common.Library
{
public class ValidationMessage : CommonBase
{
#region Private Variables
private string _PropertyName = string.Empty;
private string _Message = string.Empty;
#endregion
#region Public Properties
public string PropertyName
{
get
{
return _PropertyName;
}
set
{
_PropertyName = value;
RaisePropertyChanged(nameof(PropertyName));
}
}
public string Message
{
get
{
return _Message;
}
set
{
_Message = value;
RaisePropertyChanged(nameof(Message));
}
}
#endregion
#region ToString() Override
public override string ToString()
{
if (string.IsNullOrEmpty(PropertyName))
{
return $"{Message}";
}
else
{
return $"{Message} ({PropertyName})";
}
}
#endregion
}
}
Add to the view model base class a Boolean property of whether to display the validation message area, and a collection of ValidationMessage objects. Open the BaseClasses\ViewModelBase.cs
file and add two private variables.
private bool _IsValidationAreaVisible;
private ObservableCollection<ValidationMessage> _ValidationMessages = new();
Next, add the two public properties (Listing 12) that you can bind to controls on a page. Locate the BeginProcessing()
method and initialize both of these new properties.
Listing 12: Add two properties to display validation messages to the user.
public bool IsValidationAreaVisible
{
get { return _IsValidationAreaVisible; }
set {
_IsValidationAreaVisible = value;
RaisePropertyChanged(nameof(IsValidationAreaVisible));
}
}
public ObservableCollection<ValidationMessage> ValidationMessages
{
get { return _ValidationMessages; }
set {
_ValidationMessages = value;
RaisePropertyChanged(nameof(ValidationMessages));
}
}
{
IsValidationAreaVisible = false;
ValidationMessages.Clear();
}
Locate the EndProcessing()
method and check to see if any validation errors have occurred and set the IsValidationAreaVisible
property appropriately.
IsValidationAreaVisible = ValidationMessages.Count > 0;
Also in this class, add a generic Validate()
method (Listing 13) to which you pass any entity class. This method checks the values in each property against any data annotations on that property. The code in Listing 13 creates an instance of a ValidationContext
object passing in the Entity
property. Create an instance of list of ValidationResult
objects so the TryValidateObject()
method can fill in this list with all the ValidationResult
objects. The TryValidateObject()
method is responsible for checking all data annotations attached to each property in the entity object. If any validations fail, the appropriate error message, along with the property name, is returned in the results
variable.
Listing 13: Use the ValidationContext and Validator objects to validate properties decorated with data annotations.
#region Validate Method
public bool Validate<T>(T entity)
{
ValidationMessages.Clear();
if (entity != null)
{
// Create ValidationContext object
ValidationContext context = new(entity, serviceProvider: null,
items: null);
List<ValidationResult> results = new();
// Call TryValidateObject() method
if (!Validator.TryValidateObject(entity, context, results, true))
{
// Get validation results
foreach (ValidationResult item in results)
{
string propName = string.Empty;
if (item.MemberNames.Any())
{
propName = ((string[])item.MemberNames)[0];
}
// Build new ValidationMessage object
ValidationMessage msg = new()
{
Message = item.ErrorMessage ?? string.Empty,
PropertyName = propName
};
// Add validation object to list
ValidationMessages.Add(msg);
}
}
}
IsValidationAreaVisible = ValidationMessages.Count > 0;
return !IsValidationAreaVisible;
}
#endregion
Loop through the results collection and add a new ValidationMessage
object to the ValidationMessages
property. The ErrorMessage
property is filled in with the ErrorMessage
property from the current ValidationResult
item. The property name is retrieved from the first element of the MemberNames
property on the ValidationResult
item. It's possible for a data annotation to have two properties to which it applies, but for most simple properties, you only need to grab the first property name.
Add Validate() Method to User View Model Class
Before you save the User object with the data entered by the user, you need to validate that data. Open the ViewModelClasses\UserViewModel.cs
file and modify the SaveAsync()
method to call the Validate()
method, as shown in the following code:
public async virtual Task<User?> SaveAsync()
{
User? ret;
if (Validate(CurrentEntity))
{
// TODO: Write code to save data
ret = new User();
}
else
{
ret = null;
}
return await Task.FromResult(ret);
}
Display Validation Errors
Just like you did with the information and exception messages, create a validation error partial page. Right mouse-click on the ViewsPartial
folder and select Add > New Item… > .NET MAUI > Content View (XAML), set the name to ValidationMessageArea
, and click on the Add button. In the <ContentView>
starting tag, add a new XML namespace to reference the Common.Library.
xmlns:common="clr-namespace:Common.Library;assembly=Common.Library"
Add an x:DataType
attribute to this starting tag so you can bind to the two properties you created in the ValidationMessage
class, as shown in the following code:
x:DataType="common:ViewModelBase"
Replace the <VerticalStackLayout>
element with the XAML shown in Listing 14. It's in this CollectionView
list where you display the validation messages from any failed data validation.
Listing 14: Use a CollectionView to display validation error messages.
<Border IsVisible="{Binding IsValidationAreaVisible}"
Style="{StaticResource ValidationErrorArea}">
<VerticalStackLayout>
<Label FontSize="Medium" Text="Please Fix These Validation Data Entry
Errors" />
<CollectionView ItemsSource="{Binding ValidationMessages}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="common:ValidationMessage">
<HorizontalStackLayout>
<Label Text="{Binding Message}" Style="{StaticResource
ValidationMessage}" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</Border>
Open the Resources\Styles\CommonStyles.xaml
file in the Common.Library.MAUI project and add two new keyed style, as shown in Listing 15.
Listing 15: Add two keyed styles for displaying the validation errors.
<Style TargetType="Label" x:Key="ValidationMessage">
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="FontSize" Value="Small" />
<Setter Property="TextColor" Value="Red" />
</Style>
<Style TargetType="Border" x:Key="ValidationErrorArea">
<Setter Property="Stroke" Value="Gray" />
<Setter Property="Margin" Value="10" />
<Setter Property="Padding" Value="10" />
</Style>
Modify the Detail View
Open the Views\UserDetailView.xaml
file and add a new Auto to the RowDefinitions
attribute of the <Grid>.
Scroll down to </Grid>
closing tag and just before this closing tag, add the validation message partial page, as shown in the following code snippet:
<!-- ******************************** -->
<!-- Validation Failure Messages Area -->
<!-- ******************************** -->
<partial:ValidationMessageArea Grid.Row="13" Grid.ColumnSpan="2" />
Try It Out
Run the application and click on the Users menu. Click the Edit button for one of the users. Delete the values in the Login ID
, First Name
, Last Name
, and Email Address
entry controls. Click the Save button and you should see a list of validation errors appear just below the Save button, as shown in Figure 12.

Move Message Area Views to Common MAUI Library
Thinking ahead, you may want the information, error, and validation message components to be used across all .NET MAUI applications you develop. If that's the case, move these partial views to the Common.Library.MAUI class library. When moving XAML components from one assembly to another, you have a couple of items to change. Let's look at how to move these files.
~
The partial views use the ViewModelBase
class located in the Common.Library project, so set a reference to this class library from the Common.Library.MAUI project. Right mouse-click on the Dependencies
folder in the Common.Library.MAUI project and add a project reference to the Common.Library project. Right mouse-click on the Common.Library.MAUI project and add a new folder named ViewsPartial
.
Move the ExceptionMessageArea.xaml
, the InfoMessageArea.xaml
, and the ValidationMessageArea.xaml
files to this new folder. Open the ExceptionMessageArea.xaml.cs
file and change the beginning part of the namespace from AdventureWorks to Common.Library as shown in the following line:
namespace Common.Library.MAUI.ViewsPartial
{
}
Open the ExceptionMessageArea.xaml
file and locate the x:Class
attribute on the ContentView
starting tag and change the beginning part of the namespace to Common.Library as well.
x:Class="Common.Library.MAUI.ViewsPartial.ExceptionMessageArea"
Open the InfoMessageArea.xaml.cs
file and change the beginning part of the namespace from AdventureWorks to Common.Library, as shown in the following line:
namespace Common.Library.MAUI.ViewsPartial
{
}
Open the InfoMessageArea.xaml
file and locate the x:Class
attribute on the ContentView
starting tag and change the beginning part of the namespace to Common.Library as well.
x:Class="Common.Library.MAUI.ViewsPartial.InfoMessageArea"
Open the ValidationMessageArea.xaml.cs
file and change the beginning part of the namespace from AdventureWorks to Common.Library, as shown in the following line:
namespace Common.Library.MAUI.ViewsPartial
{
}
Open the ValidationMessageArea.xaml
file and locate the x:Class
attribute on the ContentView
starting tag and change the beginning part of the namespace to Common.Library as well.
x:Class="Common.Library.MAUI.ViewsPartial.ValidationMessageArea"
Update References on Application Pages
Now that you've moved these partial pages to a different project, you need to modify the references on the four pages you've used these on. Open the ProductDetailView.xaml
file and add a new XML namespace to reference the Common.Library.MAUI project.
xmlns:commonPartial="clr-namespace: Common.Library.MAUI.ViewsPartial;
assembly=Common.Library.MAUI"
Locate the <partial:ExceptionMessageArea
...> element and change partial
to commonPartial
, as shown in the following XAML:
<commonPartial:ExceptionMessageArea
Grid.Row="16"
Grid.ColumnSpan="2" />
Open the ProductListView.xaml
file and add a new XML namespace to reference the Common.Library.MAUI project.
xmlns:commonPartial="clr-namespace: Common.Library.MAUI.ViewsPartial;
assembly=Common.Library.MAUI"
Locate the <partial:InfoMessageArea …>
element and change partial
to commonPartial
, as shown in the following XAML:
<commonPartial:InfoMessageArea
Grid.Row="1"
Grid.ColumnSpan="2" />
Locate the <partial:ExceptionMessageArea …>
element and change partial
to commonPartial
, as shown in the following XAML:
<commonPartial:ExceptionMessageArea
Grid.Row="2"
Grid.ColumnSpan="2" />
Repeat the steps above for the UserDetailView.xaml
and UserListView.xaml
files. When you're done making all these changes, select Build > Clean Solution from the Visual Studio menu. You may also need to close Visual Studio and reopen the project to make sure all the changes are recognized. Sometimes Visual Studio gets confused when you move components around.
Try It Out
Run the application and check you can see the information, error, and validate messages.
Summary
In this article, you created your own modal page and called it from the Login page. You returned the value selected from the modal page back to the calling page using the [QueryProperty]
attribute. A class to hold global application settings is something you'll probably create in each application you build. In this article, you built an AppSettings
class to hold a Boolean value. There are a few different built-in popup dialogs available in .NET MAUI that you may use. You used the Alert dialog to ask the user if they wish to delete a specific row. Finally, you used Microsoft Data Annotations in your User class to help with validation. You then created a few reusable components to display information, error, and validation messages to the user. I hope you have enjoyed my series on .NET MAUI. There are many more topics to explore. and I hope you continue to use this great product.
Table 1: The common properties available to all data annotation attribute classes
Property Name | Description |
---|---|
ErrorMessage | Get/Set the error message format string |
ErrorMessageString | Gets the localized error message |
ErrorMessageResourceName | Get/Set the error message resource name |
ErrorMessageResourceType | Get/Set the error resource class type |