.NET MAUI – Forget Me Not – Part 4

This is part 4 in an ongoing series in which I will build and dissect a non-trivial app. For details, please see the first in this series.

Part 3 ended with a teaser about the Preferences Page. As you’ll remember, we start with the Preference Model class:

namespace ForgetMeNot.Model;

[ObservableObject]
public partial class Preference
{
    [ObservableProperty] private string preferencePrompt;
    [ObservableProperty] private string preferenceValue;

}

I explained about ObservableProperties, so I won’t review that here. Let’s see how this model class is used. There are two important related classes: Preferences.xaml and PreferencesViewModel.cs

The Preferences View

The view/page starts with a label explaining that you can add as many types of preferences as you like. We start you with a set of preference prompts (e.g., favorite music, favorite books, etc.) but you are not only free to add your own, you are free to modify ours. All the fields, both prompt and value are free form editable fields.

We’ll start with a couple styles

<Style x:Key="PreferenceStyle" TargetType="Entry">
    <Setter Property="BackgroundColor" Value="AntiqueWhite" />
    <Setter Property="HorizontalOptions" Value="Start" />
    <Setter Property="VerticalOptions" Value="Center" />
    <Setter Property="FontSize" Value="10" />
    <Setter Property="TextColor" Value="Black" />
    <Setter Property="VerticalTextAlignment" Value="Center" />
    <Setter Property="WidthRequest" Value="400" />
</Style>

This first one should be pretty straightforward if you are a Xamarin.Forms programmer. Our Entries will use this style. The second one may be a bit new, even though VisualStates are now available in XF.

<Style TargetType="Entry">
    <Setter Property="FontSize" Value="18" />
    <Setter Property="VisualStateManager.VisualStateGroups">
        <VisualStateGroupList>
            <VisualStateGroup x:Name="CommonStates">
                <VisualState x:Name="Normal">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="White" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="Focused">
                    <VisualState.Setters>
                        <Setter Property="BackgroundColor" Value="Wheat" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

The short version of this is that the appearance of the entry will change based on the “state” of the entry. If it is in normal state, the background is white, if it is has focus, however, the background is wheat.

The body of the page has a label that explains how the page works, and a button to save your preferences (the button is at both the top and bottom of the page for the user’s convenience). This is all followed by a CollectionView. Let’s focus on that:

<CollectionView
    Margin="20,20,10,10"
    ItemsSource="{Binding Preferences}"
    SelectionMode="None">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Grid ColumnDefinitions="*,2*">
                <Entry
                    Grid.Column="0"
                    FontSize="10"
                    HorizontalOptions="Start"
                    HorizontalTextAlignment="Start"
                    Text="{Binding PreferencePrompt, Mode=TwoWay }"
                    TextColor="{OnPlatform Black, iOS=White}" />
                <Entry
                    Grid.Column="1"
                    FontSize="10"
                    HeightRequest="32"
                    HorizontalOptions="Start"
                    HorizontalTextAlignment="Start"
                    Text="{Binding PreferenceValue, Mode=TwoWay}"
                    TextColor="{OnPlatform Black, iOS=White}" 
                    WidthRequest="350" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

The ItemsSource for the collection view is a property in the ViewModel. The DataTemplate defines that each entry in the ItemsSource collection will be displayed in a pair of Entry fields. The first will use the item’s PreferencePrompt property and the second will use the PreferenceValue property. Again, all of this will be very familiar if you are an XF programmer. In fact, most of what you’ll see in MAUI will be very familiar — .NET MAUI really is the next incarnation of Xamarin.Forms.

The save button invokes the SavePreferencesCommand, which we’ll also see in the ViewModel.

The ViewModel

PreferencesViewModel has an ObservableProperty and RelayCommands so we’ll mark it as an ObservableObject

[ObservableObject]
public partial class PreferencesViewModel
{
    [ObservableProperty] private List<Preference> preferences;

    public PreferencesViewModel()
    {
        Preferences = PreferencesService.GetPreferences();
    }

    [RelayCommand]
    private async Task SavePreferencesAsync()
    {
        PreferencesService.Save(preferences);
    }
    
}

Nothing surprising or new here except the RelayCommand and the contents of the constructor.

The RelayCommand attributes marks SavePreferencesAsync() as the method called by the SavePreferencesCommand command. The naming is by convention and that is how MAUI associates the bound command in the XAML with the implementing method in the ViewModel. Again, a huge reduction in boilerplate code.

The constructor sets Preferences (which as you see is a list of Preference objects) by invoking GetPreferences on the PreferenceService.

GetPreferences will return a list of Preference objects by going to the client API service. For now, I’m just mocking that the easy way:

 public static  List<Preference> GetPreferences()
 {
     return GetPreferencesMock();
 }

GetPreferencesMock does not use a “real” mocking object, it just spins up a few preferences and returns them in a list.

private static List<Preference> GetPreferencesMock()
{
    List<Preference> preferences  = new();
    Preference preference = new Preference
    {
        PreferencePrompt = "Shirt size",
        PreferenceValue = "XXL"
    };
    preferences.Add(preference);

    preference = new()
    {
        PreferencePrompt = "Pants size",
        PreferenceValue = "40"
    };
    preferences.Add(preference);

Not elegant, I admit, but it works and it is temporary. Once we have the API we’ll junk the Mock (or comment it out in case we need it for testing later).

We’re using tabs in this app, and when you click on the Preferences tab you go to the Preferences page:

Note that the user can fill in any answer they want in response to the prompt, and they can even edit the prompt!

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in Essentials. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.