In the first parts of this ongoing series on exploring .NET MAUI (Exploring .NET MAUI: Getting Started and Exploring .NET MAUI: Styles, Navigation, and Reusable UI), you created your first .NET MAUI application and ran that application on both a Windows computer and an Android emulator. You created data-entry pages, partial pages to reuse on those pages, and you navigated among those pages. In this article, you'll continue to use more data entry controls and learn to perform data binding between controls. You're going to create a class with properties that you can bind to controls on a page as well. When you change the values of properties in a class, you need to raise a PropertyChanged event so the UI can update those controls that are bound to the properties. You're going to create a base class that helps you raise that event any time the property values change.

Use a Switch Control for Yes/No Input

Simple entry controls allow a user to enter any data they want. For some input you need a simple yes or no answer from the user. For example, if you have a Boolean property such as IsEmployed on a business object, use a Switch control to represent the two states for this property. On a Windows computer, the Switch control appears as a toggle coupled with a label next to it with the words On and Off, as shown in Figure 1.

Figure 1: A switch represents a Boolean state.
Figure 1: A switch represents a Boolean state.

Open the Views\UserDetailView.xaml file and add an Auto to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “7” instead of “6”. Just before the <HorizontalStackLayout> starting tag you just modified, add the following XAML that creates the Switch control:

<Label Grid.Row="6" Text="Still Employed?" />
<Switch Grid.Row="6" Grid.Column="1" />

Try It Out

Run the application and click on Users > Navigate to Detail to see the Switch control. Click on the Switch control a couple of times to see the labels change.

Override the Switch Labels

The labels On and Off may not make sense for what you're trying to represent with the Switch control. In the example above, it makes more sense for the labels to be Yes and No. You may also not want the labels to appear at all when running on Windows. There's no exposed property to set these two labels, but you may write a little C# code to set these labels.

In the first part of this article series (Exploring .NET MAUI: Getting Started), you added a SetWindowsOptions() method to the MauiProgram.cs file. Around this method, you added a conditional compile statement #if WINDOWS. Instead of using these conditional compile statements around each method that you want to run on only a specific platform, let's create a class in the Platforms\Windows folder instead. When a class is created within any of the sub-folders under the Platforms folder, that class is only compiled for running on that specific OS, thus you don't need to use conditional compilation statements.

Right mouse-click on the Platforms\Windows folder and add a new class named WindowsHelpers. Into this class, you're going to place the SetWindowOptions() method you created in the first article, and a new method named SetSwitchText(). This new method allows you to set the OnContent and OffContent properties after a Switch control has been rendered on a .NET MAUI page. Replace the entire contents of the WindowsHelpers.cs file with the code shown in Listing 1.

Listing 1: Create a WindowsHelpers class to change Windows-only settings

using Microsoft.Maui.Handlers;
using Microsoft.Maui.LifecycleEvents;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using WinRT.Interop;

namespace AdventureWorks.MAUI;

public class WindowsHelpers
{
    #region SetWindowOptions Method
    public static void SetWindowOptions(MauiAppBuilder builder)
    {
        builder.ConfigureLifecycleEvents(events =>
        {
            events.AddWindows(wndBuilder =>
            {
                wndBuilder.OnWindowCreated(window =>
                {
                    IntPtr hndl = WindowNative.GetWindowHandle(window);
                    WindowId winId = Win32Interop.GetWindowIdFromWindow(hndl);
                    AppWindow appWnd = AppWindow.GetFromWindowId(winId);
                    if (appWnd.Presenter is OverlappedPresenter p)
                    {
                        p.Maximize();
                        //p.HasBorder = false;
                        //p.HasTitleBar = false;
                        //p.IsAlwaysOnTop = false;
                        //p.IsMaximizable = false;
                        //p.IsMinimizable = false;
                        //p.IsModal = false;
                        //p.IsResizable = false;
                    }
                });
            });
        });
    }
    #endregion

    #region SetSwitchText Method
    public static void SetSwitchText(string onContent = "", 
       string offContent = "")
    {
        SwitchHandler.Mapper.AppendToMapping("SwitchText", (h, v) =>
        {
            // Get rid of On/Off label beside switch
            h.PlatformView.OnContent = onContent;
            h.PlatformView.OffContent = offContent;
            h.PlatformView.MinWidth = 0;
        });
    }
    #endregion
}

Open the MauiProgram.cs file and in the CreateMauiApp() method where you called the SetWindowOptions() method, modify it to use WindowsHelpers.SetWindowOptions(builder) instead. Also, call the SetSwitchText() method on the same class as shown below:

#if WINDOWS
WindowsHelpers.SetWindowOptions(builder);

   // Set labels on all <Switch> elements
   WindowsHelpers.SetSwitchText("Yes", "No");
   //WindowsHelpers.SetSwitchText();
#endif

Remove the SetWindowOptions() method in the MauiProgram.cs file and the using statements wrapped in the #WINDOWS conditional compile statement at the top of the file.

Try It Out

Run the application and click on Users > Navigate to Detail to see the Switch control with the labels Yes and No, as shown in Figure 2. If you don't pass any parameters to the SetSwitchText(), no labels are displayed next to the Switch at all.

Figure 2: You can set the labels on the Switch to be anything you wish.
Figure 2: You can set the labels on the Switch to be anything you wish.

RadioButton Controls Help Users Select a Single Item from a List

If you have a small set of items that the user can select a single value from, the RadioButton control is one of a few controls you can use. The RadioButton controls (Figure 3) are a mutually exclusive set of inputs. When the user clicks on one RadioButton, the previously selected button is unchecked and the one newly clicked becomes checked. You may set the initial button to be checked by setting the IsChecked property to a true value. Each set of RadioButton controls should have the same GroupName property set to a unique value. For example, GroupName="EmployeeType" is used to group those RadioButton controls shown in Figure 3. If you had a set of RadioButton controls to group a user's ethnicity, you set the GroupName property of those controls to GroupName="Ethnicity".

Figure 3: Radio buttons allow you to select a single value from a list of items.
Figure 3: Radio buttons allow you to select a single value from a list of items.

Open the Views\UserDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “8” instead of “7”. Just before the <HorizontalStackLayout> starting tag you just modified, add the following XAML that creates the set of RadioButton controls:

<Label Text="Employee Type" Grid.Row="7" />
<FlexLayout Grid.Row="7" Grid.Column="1" Wrap="Wrap" Direction="Row">
    <HorizontalStackLayout>
        <Label Text="Full-Time" />
        <RadioButton IsChecked="True" GroupName="EmployeeType" />
    </HorizontalStackLayout>
    <HorizontalStackLayout>
        <Label Text="Part-Time" />
        <RadioButton GroupName="EmployeeType" />
    </HorizontalStackLayout>
</FlexLayout>

Try It Out

Run the application and click on Users > Navigate to Detail to see the radio buttons, as shown in Figure 3. Click back and forth between the two RadioButton controls to see the other one become unchecked.

The DatePicker Control Avoids Typing in Date Values

It's always better to let users select from a list rather than typing in something by themselves. The DatePicker control displays a calendar from which the user may select a date. The Date property gets the date selected by the user or sets the date to start with when the calendar is first displayed. By default, the Date property is set to today's date. The Format property is a string value set to a standard or custom .NET format string you pass to the ToString() method, such as dateValue.ToString("D"). The default format string is set to the long date pattern “D”. There are two properties, MinimumDate and MaximumDate, that allow you to set the range of dates the user is allowed to select from.

Let's add a Birth Date field to the user detail page and use the DatePicker control to allow the user to select the date from the calendar. Open the Views\UserDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “9” instead of “8”. Just before the <HorizontalStackLayout> starting tag you just modified, add the following XAML that creates the DatePicker control.

<Label Text="Birth Date" Grid.Row="8" />
<DatePicker Grid.Row="8" Grid.Column="1" />

Try It Out

Run the application and click on Users > Navigate to Detail to click on the Birth Date entry field. A calendar control is displayed, as shown in Figure 4.

Figure 4: A full calendar is displayed from which the user may select a date.
Figure 4: A full calendar is displayed from which the user may select a date.

Select an Hour and Minute Using the TimePicker Control

The TimePicker control displays a list to the user from which they may select an hour and minute (see Figure 5). The Time property gets the time selected by the user or sets the time to start with when the list of hours and minutes is first displayed. The Time property is a TimeSpan data type. By default, the Time property is set to zero (0) or midnight (12:00:00 AM). The Format property is a string value set to a standard or custom .NET format string you pass to the ToString() method, such as timeValue.ToString("t"). The default format string is set to the short time pattern “t”. Be aware that on a macOS, the Format property has no effect on the control.

Figure 5: Use a TimePicker to select the hour and minute from an hour and minute list.
Figure 5: Use a TimePicker to select the hour and minute from an hour and minute list.

Let's add a Start Time field to the user detail page and use the TimePicker control to allow the user to select the time from an hour and minute list, as shown in Figure 5. Open the Views\UserDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “10” instead of “9”. Just before the <HorizontalStackLayout> starting tag you just modified, add the following XAML that creates the TimePicker control.

<Label Text="Start Time"
        Grid.Row="9" />
<TimePicker Grid.Row="9"
            Grid.Column="1"
            Time="08:00:00" />

Try It Out

Run the application and click on Users > Navigate to Detail. Then click on the start time entry field to have an hour and minute list from which you can select, as shown in Figure 5.

Choose an Item from a List with the Picker Control

If you have a small set of values the user can select from but not a lot of real estate on your page for radio button controls, a Picker control can be an appropriate choice. The Picker control has an ItemsSource property that can be set to any IEnumerable collection. The SelectedIndex property is an integer value representing the index number within the collection that's selected. The SelectedItem property represents the actual item selected. Most often the ItemsSource property is set at runtime via data binding, but for this sample, you're going to use a hard-coded array of strings.

Let's add a Label and Entry field to enter a Phone number. Next to the Phone entry field, display a list of phone types the user can select from, as shown in Figure 6. Open the Views\UserDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “11” instead of “10”. Just before the <HorizontalStackLayout> starting tag you just modified, add the XAML shown in Listing 2 that creates the Label, Entry, and the Picker control.

Figure 6: Use a Picker control to display a list of items from which to select.
Figure 6: Use a Picker control to display a list of items from which to select.

Listing 2: Enter a phone number and select the type of phone from a drop-down list

<Label Text="Phone"
        Grid.Row="10" />

<FlexLayout Grid.Row="10"
            Grid.Column="1"
            Wrap="Wrap"
            Direction="Row">
    <HorizontalStackLayout>
        <Entry MinimumWidthRequest="150"
               Text="" />
    </HorizontalStackLayout>

    <HorizontalStackLayout>
        <Picker VerticalTextAlignment="Center">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Home</x:String>
                    <x:String>Mobile</x:String>
                    <x:String>Work</x:String>
                    <x:String>Other</x:String>
                </x:Array>
            </Picker.ItemsSource>
        </Picker>
    </HorizontalStackLayout>
</FlexLayout>

Within the <Picker> element, create a <Picker.ItemsSource> element into which you place a collection of phone types. The collection is created using the x:Array construct where each type in the array is of the type String. For a small set of data that doesn't change very often, this is acceptable, but getting this data from a data store is probably a better option. Data binding is covered later in this article series, and you will learn to set the ItemsSource property using data from a data store.

Try It Out

Run the application and click on Users > Navigate to Detail and click on the down arrow on the right of the Picker to see the list of phone types appear. Select one of the options to watch the list close and the selected item appear next to the phone number entry.

Display Images on a Button

The Save and Cancel buttons you added to the page use text to display what each button does. If you wish to use an image instead of text, change the Button control to an ImageButton control. Use the Source property to reference an image contained in the Resources\Images folder. When I'm working with just an image, I like to add a Tooltip for when a user hovers over the button on a Windows computer or when they do a long press on a mobile device. This is accomplished by setting the ToolTipProperties.Text property to the appropriate text.

Modify the Save and Cancel buttons in the last <HorizontalStackLayout> element on the User Detail page to become ImageButton controls. I have created save.png and cancel.png images to show a disk icon for save and the letter X for cancel, as shown in Figure 7. Either download the samples for this article or locate similar images by doing a Google search. The images should be 32 pixels by 32 pixels for the height and width. Place these images into the Resources\Images folder. Replace the last <HorizontalStackLayout> element in the grid with the following XAML:

<HorizontalStackLayout Grid.Row="11"
                       Grid.Column="1">
  <ImageButton Source="save.png"
     ToolTipProperties.Text="Save Data"
     Clicked="SaveButton_Clicked" />
  <ImageButton Source="cancel.png"
     ToolTipProperties.Text="Cancel Changes" />
</HorizontalStackLayout>
Figure 7: Use an ImageButton to display a graphic on a button instead of text.
Figure 7: Use an ImageButton to display a graphic on a button instead of text.

The ImageButton control doesn't have any default styling, so open the CommonStyles.xaml file in the Resources\Styles folder in the Common.Library.MAUI project and add a new global style as shown in the following XAML:

<Style TargetType="ImageButton">
    <Setter Property="BackgroundColor" Value="Blue" />
</Style>

When dealing with images in a .NET MAUI application, make sure the name of the image is all lowercase, as mixed case image names don't work on all mobile devices. Underlines are acceptable but try not to use any other special characters as they may also not work on all operating systems.

Try It Out

Run the application and click on Users > Navigate to Detail to see the new ImageButton controls with the save and cancel images displayed, as shown in Figure 7.

Image and Text in a Single Button

What if you wish to have both an image and text (Figure 8) within a button? The Button control supports an ImageSource property that allows you to set the name of an image located within the Resources/Images folder. There's also a ContentLayout property to help you position where within the button you want the image placed. The valid values for this property are Left, Top, Right, Bottom. If you wish to add some spacing between the image and the text, place a comma and then the number of device-independent pixels you want. For example, set ContentLayout=“Left,40” to position the image on the left, with 40 pixels between the image and the text.

<HorizontalStackLayout Grid.Row="11" Grid.Column="1">
    <Button Text="Save" ImageSource="save.png" 
            ToolTipProperties.Text="Save Data" ContentLayout="Left" 
            Clicked="SaveButton_Clicked" />
  <Button Text="Cancel" ImageSource="cancel.png" ContentLayout="Left" 
            ToolTipProperties.Text="Cancel Changes" />
</HorizontalStackLayout>
Figure 8: Place an image and text within a Button control.
Figure 8: Place an image and text within a Button control.

Try It Out

Run the application and click on Users > Navigate to Detail to see the buttons with both an image and text, as shown in Figure 8.

Image Control

To display a picture on a page where there's typically no interaction with that picture, use the Image control. The Image control has a Source property to which you set the name of a file located in the Resources\Images folder. The Source property may also access a URI or a stream to display the picture. As previously mentioned, make sure your picture file names are all lowercase and don't contain any special characters other than maybe an underscore. The Aspect property controls how the picture is scaled when displaying within the container.

Let's add a Label control to display “Product Picture”. Then use the Image control to display a picture, as shown in Figure 9. Open the Views\ProductDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “14” instead of “13”. Just before the <HorizontalStackLayout> starting tag you just modified add the following XAML that creates the Image control:

<Label Text="Product Picture" Grid.Row="13" />
<Image Grid.Row="13" Grid.Column="1" HorizontalOptions="Start" 
       Aspect="Center" Source="bikeframe.jpg" />
Figure 9: Display a picture using the Image control.
Figure 9: Display a picture using the Image control.

Try It Out

Run the application and click on Products to see the new Image control, as shown in Figure 9.

Editor Control

To allow the user to enter a large amount of text, use the Editor control. Use the AutoSize property to tell the editor to automatically change the size of the control to accommodate the user input. By default, this property is set to false. Useful properties for this control are IsSpellCheckEnabled, IsTextPredictionEnabled, MaxLength, and Placeholder.

Let's add a Product Notes entry area to the product detail page and use the Editor control to allow a user to add multiple lines of text. Open the Views\ProductDetailView.xaml file and add an "Auto" to the RowDefinitions attribute of the Grid control. Locate the last <HorizontalStackLayout> starting tag at the bottom of the Grid and change the Grid.Row property to “15” instead of “14”. Just before the <HorizontalStackLayout> starting tag you just modified, add the following XAML that creates the Image control:

<Label Text="Product Notes" Grid.Row="14" />
<Editor Grid.Row="14" Grid.Column="1" HeightRequest="100" />

Try It Out

Run the application and click on Products to see the new Editor control, as shown in Figure 10.

Figure 10: Use the Editor control to allow the user to enter multiple lines of text.
Figure 10: Use the Editor control to allow the user to enter multiple lines of text.

Binding a Slider to a Label Control

A Slider control displays a long horizontal rule along which you can drag a “thumb” to change the value (see Figure 11). Using the Minimum and Maximum property, you can control the range that's put into the Value property as the user drags the thumb. The Value property is a data type of double. If you wish to have the value change in whole number increments, you need to write a single line of C# code.

Figure 11: Use a Slider to allow the user to select a value between a minimum and a maximum.
Figure 11: Use a Slider to allow the user to select a value between a minimum and a maximum.

Let's replace the Weight Entry control on the product detail page to use a Slider control instead. Open the Views\ProductDetailView.xaml file and locate the Label and Entry control for Weight. Leave the Label control but replace the Entry control with the following XAML:

<VerticalStackLayout Grid.Row="7" Grid.Column="1">
    <Slider x:Name="weight" Value="1" Minimum="1" 
            Maximum="100" ValueChanged="Weight_Changed" />
    <Label BindingContext="{x:Reference weight}" 
           Text="{Binding Value}" />
</VerticalStackLayout>

Use data binding to see the value from the Slider appear in the Label as the thumb is dragged back and forth. In this code, you set the Minimum property to a value of one (1) and Maximum property to a value of 100. Set the initial value in the Value property to a one (1). Always set a starting value to avoid having to handle nulls in the code behind. In the Label control, set the BindingContext property to reference the “weight” Slider control and bind the Text property of the Label to the Value property in the Slider. As the thumb is dragged back and forth on the slider, the label displays the current number from the Value property through the magic of data binding.

After adding the XAML, position your cursor on the “Weight_Changed” in the ValueChanged attribute and press F12. This creates the event procedure to be called each time the user slides the thumb on the slider. Write the following code in this event procedure:

private void Weight_Changed(object sender, ValueChangedEventArgs e)
{
    weight.Value = Math.Round(e.NewValue, 0);
}

The argument, e, received in this event procedure, contains both the OldValue and NewValue properties each time it's called. As mentioned, the Value property is of type double, so if you wish to use integer increments, simply apply the Math.Round() method to the e.NewValue property, passing zero (0) as the second parameter for the number of digits to use when rounding.

Try It Out

Run the application and click on Products to see the new Slider control, as shown in Figure 11. Move the thumb back and forth to watch the new value appear in the label immediately below the slider.

Bind a Stepper to an Entry Control

The Stepper control allows you to increment or decrement a number, as shown in Figure 12. By binding a Stepper to an Entry control, you allow a user to either type in a number or click on the plus or minus sign on the Stepper. The Value property on a Stepper either gets or sets the current number. The Minimum and Maximum properties allow you to specify the range of values the user can move among. The Increment property lets you set how much to increase or decrease the Value property. The Value property is a double data type, so the Increment property can be set to almost any value you wish.

Figure 12: Use the stepper control to increment and decrement values.
Figure 12: Use the stepper control to increment and decrement values.

Let's bind two Stepper controls to both the Cost and Price Entry controls on the product detail page. Open the Views\ProductDetailView.xaml file and locate the <Entry> element below the <Label Text="Cost"> and replace it with the following XAML:

<HorizontalStackLayout Grid.Row="4" Grid.Column="1">
  <Entry Text="{Binding Value}" 
         BindingContext="{x:Reference CostStepper}" />
  <Stepper x:Name="CostStepper" Value="5" Minimum="1" 
           Maximum="9999" Increment="1" />
</HorizontalStackLayout>

Locate the <Entry> element below the <Label Text="Price"> and replace it with the following XAML:

<HorizontalStackLayout Grid.Row="5" Grid.Column="1">
    <Entry Text="{Binding Value}" 
           BindingContext="{x:Reference PriceStepper}" />
    <Stepper x:Name="PriceStepper" Value="10" 
             Minimum="1" Maximum="9999" Increment="1" />
</HorizontalStackLayout>

Try It Out

Run the application and click on the Products menu. Increment and decrement each stepper to see the value change in the corresponding Entry controls, as shown in Figure 12.

Bind Two Steppers to One Another

Increase the Cost value to ten (10) and decrease the Price value to five (5). This should not be allowed, as you never want to sell something for less than it costs to make. Locate the <Stepper x:Name="CostStepper" …> and modify it to look like the following XAML:

<Stepper x:Name="CostStepper"
          Value="5"
          Minimum="1"
          Maximum="{Binding Value}"
          BindingContext="{x:Reference PriceStepper}"
          Increment="1" />

Locate the <Stepper x:Name="PriceStepper" …> and modify it to look like the following XAML:

<Stepper x:Name="PriceStepper"
         Value="10"
         Minimum="{Binding Value}"
         Maximum="9999"
         BindingContext="{x:Reference CostStepper}"
         Increment="1" />

The key to the code above is that you're binding the Maximum property on the cost stepper to the Value property of the price stepper. You then bind the Minimum property of the price stepper to the Value property of the price stepper. By binding these two stepper controls to one another, it solves the issue of being allowed to set the cost greater than the price.

Try It Out

Run the application and click on the Products menu. Increment and decrement each stepper to see the value change in the corresponding Entry controls. Notice that you can no longer make the price less than the cost, nor can the cost be greater than the price.

Disable One Control by Binding to IsEnabled on a CheckBox

A common business rule you encounter when programming is that you need to only make certain controls enabled, or visible, when another control, such as a RadioButton or CheckBox, is selected. Prior to data binding, this was accomplished using C# code. With data binding, you may now bind the IsEnabled or the IsVisible property on a control to the IsChecked property of another control.

Open the Views\UserDetailView.xaml file and locate the CheckBox under the <Label Text="Flex Time?">. Then add an x:Name attribute. You need this attribute so you can reference the CheckBox object with that name from another control.

<CheckBox x:Name="FlexTime" />

Locate the TimePicker and add an IsEnabled property to bind to the IsChecked property on the FlexTime CheckBox. Set the BindingContext attribute to refer to the FlexTime CheckBox with the x:Name set to “FlexTime” as shown in the code snippet below.

<TimePicker
    Grid.Row="9"
    Grid.Column="1"
    BindingContext="{x:Reference FlexTime}"
    IsEnabled="{Binding IsChecked}"
    Time="08:00:00"
/>

Try It Out

Run the application and click on Users > Navigate to Detail and check the FlexTime CheckBox to see the Start Time TimePicker control become enabled. Uncheck the FlexTime CheckBox to see that the Start Time becomes disabled.

Create a XAML Array Resource and Bind to Picker

In the last article, you created a Picker control and within that control, you created an array of phone types as the data for that picker. If you need to use that array on other pages, you must copy and paste the array from one page to another. This creates a maintenance nightmare, as you now have the same data in multiple places. Instead, let's move that data into a resource and bind the Phone Type Picker to that resource.

Open the Resources\Styles\AppStyles.xaml file and add the array of phone type strings as a resource. You need to provide a key to the array to be able to reference it from XAML pages, as shown in the code below:

<!-- Phone Types Resource -->
<x:Array x:Key="phoneTypes"
         Type="{x:Type x:String}">
    <x:String>Home</x:String>
    <x:String>Mobile</x:String>
    <x:String>Work</x:String>
    <x:String>Other</x:String>
</x:Array>

Open the Views\UserDetailView.xaml file and locate the <Picker> and make it look like the following:

<Picker VerticalTextAlignment="Center" 
        ItemsSource="{StaticResource phoneTypes}" />

Try It Out

Run the application and click on Users > Navigate to Detail to see the picker loaded with phone type data that's now coming from the global resource.

Bind Controls to Properties in a Class

Besides just binding one control to another, you may bind controls to the data you get from a C# class. For example, if you have a User class with properties named LoginId, FirstName, LastName, etc., you can easily bind those properties to the Entry, RadioButton, CheckBox, and other controls on the user detail page. Let's create the User class and learn how to bind the properties to the various controls.

I prefer to create all my “Entity” classes, such as the User class, in a separate class library project. Right mouse-click on the Solution and add a new Class Library project named AdventureWorks.EntityLayer. Delete the Class1.cs file as you don't need this file. Right mouse-click on the AdventureWorks.EntityLayer project and add a new folder named EntityClasses. Right mouse-click on the EntityClasses folder and add a new class named User. Replace the entire contents of this new file with the code shown in Listing 3. Right mouse-click on the AdventureWorks.MAUI project's Dependencies folder and add a Project Reference to the AdventureWorks.EntityLayer project.

Listing 3: Create a User class with properties to bind to the user detail page controls

namespace AdventureWorks.EntityLayer
{
    public class User
    {
        public int UserId { get; set; }
        public string LoginId { get; set; } = string.Empty;
        public string FirstName { get; set; } = string.Empty;
        public string LastName { get; set; } = string.Empty;
        public string Email { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty;
        public string Phone { get; set; } = string.Empty;
        public string PhoneType { get; set; } = string.Empty;
        public bool IsFullTime { get; set; }
        public bool IsEnrolledIn401k { get; set; }
        public bool IsEnrolledInHealthCare { get; set; }
        public bool IsEnrolledInHSA { get; set; }
        public bool IsEnrolledInFlexTime { get; set; }
        public bool IsEmployed { get; set; }
        public DateTime BirthDate { get; set; } = 
            DateTime.Now.AddYears(-18);
        public TimeSpan StartTime { get; set; } = 
            new TimeSpan(8, 0, 0);
    }
}

Create a User Object Using XAML

Of course, you can write C# code to instantiate an instance of the User class, but you may also use XAML to create an instance. Open the Views\UserDetailView.xaml file and add an XML namespace that points to the new class library you just created.

xmlns:vm="clr-namespace: AdventureWorks.EntityLayer; 
      assembly=AdventureWorks.EntityLayer"

Add an x:DataType attribute to the <ContentPage> element to make this page use compiled bindings. Compiled bindings improve the speed of data binding at runtime by resolving binding expressions at compile-time. In addition, having this x:DataType attribute allows Visual Studio to read the properties of the class so IntelliSense can display those property names when you're assigning data binding to controls. Assign to the x:DataType the User class from the EntityLayer that's aliased as “vm”, shown in the code below.

x:DataType="vm:User"

Create a <ContentPage.Resources> element just below the <ContentPage> starting tag and declare an instance of the User class within the XAML. You must assign a unique name to this instance using the x:Key attribute, as shown in Listing 4.

Listing 4: Create an instance of the User class using XAML

<ContentPage.Resources>
    <vm:User x:Key="viewModel"
             LoginId="JohnSmith123"
             FirstName="John"
             LastName="Smith"
             Email="John@smith.com"
             Phone="615.222.2333"
             PhoneType="Mobile"
             IsFullTime="True"
             IsEnrolledIn401k="True"
             IsEnrolledInHealthCare="True"
             IsEnrolledInHSA="False"
             IsEnrolledInFlexTime="True"
             IsEmployed="True"
             BirthDate="10-03-1975"
             StartTime="08:00:00" />
</ContentPage.Resources>

Now that you have an instance of the User class created, you need to be able to bind each property to each control. Set a BindingContent on one of the parent controls to each of the individual controls on this page. You can either use the Border, the ScrollView, or the Grid. I like to use the outermost parent, so modify the Border control to have a BindingContext property that points to the viewModel resource, as shown in the following XAML:

<Border Style="{StaticResource Border.Page}" 
        BindingContext="{StaticResource viewModel}">

Bind Entry Controls

First, let's bind each of the Entry controls on the page. Add a Text property to each Entry control if it's not already there. Within the Text property, use the Binding markup extension in the format {Binding PROPERTY_NAME} where you replace PROPERTY_NAME with the property name from the User class. In the following code snippet is a list of all the Entry controls and their corresponding properties:

<Entry Text="{Binding LoginId}" />
<Entry Text="{Binding FirstName}" />
<Entry Text="{Binding LastName}" />
<Entry Text="{Binding Email}" />
<Entry Text="{Binding Phone}" />

Notice that, as you type in this code, after you press the spacebar, a list of property names in the User class is displayed. If you don't get a list of property names, rebuild the solution. Continue down the page and map each Entry control to the appropriate property.

Bind Check Box Controls

Locate each <CheckBox> element and set each one's IsChecked property to the corresponding property in the User class, as shown in the following code snippet:

<CheckBox IsChecked="{Binding IsEnrolledIn401k}" />
<CheckBox IsChecked="{Binding IsEnrolledInFlexTime}" />
<CheckBox IsChecked="{Binding IsEnrolledInHealthCare}" />
<CheckBox IsChecked="{Binding IsEnrolledInHSA}" />

Bind the Switch Control

Locate the <Switch> control and set its IsToggled property to the IsEmployed property, as shown in the following snippet:

<Switch Grid.Row="6"
        Grid.Column="1"
        IsToggled="{Binding IsEmployed}" />

Bind Radio Button Controls

Locate the RadioButton control under the <Label Text="Full-Time" /> element and set the IsChecked property to the following:

<RadioButton IsChecked="{Binding IsFullTime}" GroupName="EmployeeType" />

Don't set the IsChecked property on the Part Time RadioButton; you'll learn how to do that later.

Bind the Date Picker Control

Locate the <DatePicker> element and bind the Date property to the BirthDate property, as shown in the following XAML:

<DatePicker Grid.Row="8" Grid.Column="1" Date="{Binding BirthDate}" />

Bind the TimePicker Control

Locate the <TimePicker> control and remove the IsEnabled and the BindingContext attributes. Add the Time attribute and bind it to the StartTime property on the User class.

<TimePicker Grid.Row="9" Grid.Column="1" Time="{Binding StartTime}" />

Picker

Modify the Picker for the phone types and add the SelectedItem attribute to bind to the PhoneType property in the User class.

<Picker VerticalTextAlignment="Center"
        SelectedItem="{Binding PhoneType}"
        ItemsSource="{StaticResource phoneTypes}"
/>

Try It Out

Run the application and click on Users > Navigate to Detail to see the data from the User object appear in each of the controls.

Use a ValueConverter to Change a Boolean Value

In the User class, you have an IsFullTime property, but there's no IsPartTime property because if the IsFullTime property is set to false, it indicates that the user is a part-time employee. So, how do you get the IsChecked property on the Part Time RadioButton to be true when the IsFullTime property is set false?

As this is a very common scenario, Microsoft created an IValueConverter interface. This interface is implemented on a class to which you write code for both Convert() and ConvertBack() methods. The Convert() method takes in a value from a property and you then write code to convert that value into a different value. For example, you can take the false value from the IsFullTime property and change it to the inverse to return a true value so it can be mapped to the IsChecked property on the Part Time RadioButton. Let's look at how to create a converter class to perform this transformation.

Right mouse-click on the Common.Library.MAUI project and add a new folder named Converters. Right mouse-click on the Converters folder and add a new class named InvertedBoolConverter. Replace the entire contents of this new file with the code shown in Listing 5.

Listing 5: Create a Convert class to transform a value into a different value

using System.Globalization;

namespace Common.Library.MAUI.Converters
{
    public class InvertedBoolConverter : IValueConverter
    {
        public object? Convert(object? value, Type targetType, 
          object? parameter, CultureInfo culture)
        {
            if (value != null)
            {
                return !(bool)value;
            }
            else
            {
                return null;
            }
        }

        public object? ConvertBack(object? value, Type targetType, 
          object? parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

Bind the RadioButton Control

Now that you've created the converter class, you may pass in the IsFullTime property to that class and have the value changed to the opposite of what it currently is. Open the Views\UserDetailView.xaml file. Add an XML namespace to reference the namespace in which the converter class resides.

xmlns:converters="clr-namespace: Common.Library.MAUI.Converters; 
      assembly=Common.Library.MAUI"

Create an instance of the converter class within the <ContentPage.Resources> element, as shown in the following code:

<ContentPage.Resources>
    <converters:InvertedBoolConverter x:Key="invertedBoolean" />
    <!-- REST OF THE XAML HERE -->
</ContentPage.Resources>

Locate the Radio Button under the <Label Text="Part-Time" /> element and bind the IsChecked property to the IsFullTime property. On the Binding markup extension, set the Converter property to the invertedBoolean resource you created. Before the binding takes place, the value in the IsFullTime property is first passed to the Converter and the value returned is bound to the IsChecked property.

<RadioButton
  IsChecked="{Binding IsFullTime, Converter={StaticResource invertedBoolean}}"
  GroupName="EmployeeType" />

Try It Out

Before you run the application, set the IsFullTime property in the XAML instance of the User object to a false value. Run the application and click on Users > Navigate to Detail. Notice that the Part Time radio button is now checked.

Bind the Time Picker Control

Let's now bind the IsEnabled binding on the TimePicker control to use the IsFullTime property in the User class. Of course, you only want the user to choose a start time if the IsFullTime property value is false, so use the converter class when binding the IsEnabled property on the TimePicker control. Locate the Flex Time CheckBox and remove the x:Name="FlexTime" attribute from that check box. Locate the TimePicker control and set the Time property to bind to the StartTime property on the User class. Set the IsEnabled property to bind to the IsFullTime property on the User class but invert the value in this property using the converter invertedBoolean, as shown in the following XAML:

<TimePicker Grid.Row="9" 
            Grid.Column="1" 
            IsEnabled="{Binding IsFullTime, 
              Converter={StaticResource invertedBoolean}}" 
            Time="{Binding StartTime}" />

Try It Out

Run the application and click on the Users > Navigate to Detail to view the user detail page. Notice that the Start Time picker is enabled because you previously set the IsFullTime property to a false value. Stop the application and change the IsFullTime property to the value “True” on the User object created in the Content.Resources element. Run the application and click on the Users > Navigate to Detail to view the user detail page. Notice that the Start Time picker is now disabled. However, if you click back and forth between the Full Time and Part Time radio buttons, the TimePicker control is not enabled or disabled during runtime. You'll learn how to do that later in this article.

Access the User Object in Code Behind

After creating the User object in XAML, you most likely want to access that object from code behind so you can modify properties on it. Open the Views\UserDetailView.xaml.cs file and add a using statement at the top of the file.

using AdventureWorks.EntityLayer;

Add a private read-only variable of the data type User to the UserDetailView class.

private readonly User _ViewModel;

You created the instance of the User class in the <ContentPage.Resources> element and assigned it the key “viewModel”. You can use the Resources property in the class and index into that collection using the key “viewModel”. Modify the constructor to retrieve the object created by XAML and assign it to the _ViewModel variable.

public UserDetailView()
{
    InitializeComponent();
    _ViewModel = (User)this.Resources["viewModel"];
}

In the Clicked event procedure on the Save button, add the line of code shown below.

System.Diagnostics.Debugger.Break();

Try It Out

Run the application and click on Users > Navigate to Detail. Make some changes to some of the data in the controls and click on the Save button. Hover over the _ViewModel variable and you should see the changes you made on the page show up in the properties of the variable.

Try to Change Data in User Object

As you just saw, the changes made on the page are reflected in the bound object. You'd assume that if you make a change to a property in the code-behind, the control would be updated as well. Let's try this out and see what happens. Open the Views\UserDetailView.xaml.cs file and add code in the constructor to change the LoginId property on the _ViewModel variable to a different value as shown below:

public UserDetailView()
{
    InitializeComponent();
    _ViewModel = (User)this.Resources["viewModel"];
    _ViewModel.LoginId = "ANewLoginId123";
}

Try It Out

Run the application and click on Users > Navigate to Detail. Notice that the value in the Login ID field DOES NOT reflect the value you put in the constructor; it displays the value set in the XAML.

Notify the UI of Changes to Objects

.NET MAUI controls know that when they change, they must put the value back into the properties they're bound to, but the reverse is not true. To inform controls of changes when you set a property in your object to a new value, you need to raise an event. .NET MAUI controls listen for the PropertyChanged event, which informs them that a change was made in the underlying object. When this event is raised, the name of the property that was changed is sent in the event arguments object. The control that's bound to the property re-reads the value in the object's property and updates the control, so the user sees the change. This event needs to be raised in all the setters on each property in your classes.

Let's create a base class that all your classes can inherit from. In this class, you're going to implement the INotifyPropertyChanged interface. This interface has one event declaration, PropertyChanged, that needs to be declared, and which is ultimately called anytime one of your property values changes. Because this interface can be used in .NET MAUI, Blazor, WPF, WinUI 3, and many other types of applications, I recommend placing this new base class in a separate class library.

Right mouse-click on the solution and select Add > New Project… from the menu. Select Class Library from the project template list. Set the Project Name to Common.Library. Delete the Class1.cs file, as this file isn't needed. Right mouse-click on the **Common.Library** project and add a new folder named BaseClasses. Right mouse-click on the BaseClasses` folder and create a class named CommonBase. Replace the entire code in this new file with the code shown in Listing 6.

Listing 6: Create a base class for all your entity classes to inherit from

using System.ComponentModel;

namespace Common.Library
{
    public abstract class CommonBase : INotifyPropertyChanged
    {
        #region Constructor
        protected CommonBase()
        {
            Init();
        }
        #endregion

        #region Init Method
        /// <summary>
        /// Initialize properties
        /// </summary>
        public virtual void Init()
        {
        }
        #endregion

        #region RaisePropertyChanged Method
        /// <summary>
        /// Event used to raise changes
        /// to any bound UI objects
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;

        public virtual void RaisePropertyChanged(string propertyName)
        {
            this.PropertyChanged?.Invoke(this, 
                new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }
}

In the CommonBase class, you have a constructor that calls an Init() method. This method is marked as virtual so you can override it in your classes if you wish to initialize properties to a valid start value. Next, you see the declaration of the PropertyChanged event. The final method is named RaisePropertyChanged() and it's this method you pass the name of the property that raises the PropertyChanged event. Pass the property name to the new instance of the PropertyChangedEventArgs class, as it's this object that informs the UI which bound control to update.

Create an Entity Base Class

Eventually, you're going to create many kinds of classes for your applications. You'll create many data classes, view model classes, entity classes, etc. I recommend that you create base classes for each type of these classes. To that end, create a base class that's just going to be inherited from your entity classes. This class is inherited from the CommonBase class so it gets all its functionality as well as any other functionality you may add that's specific just for your entity classes. Right mouse-click on the BaseClasses folder in the Common.Library project and add a new class named EntityBase. Replace the entire contents of the new file with the following code.

namespace Common.Library
{
    public abstract class EntityBase : CommonBase
    {
    }
}

Set Dependencies to the New Class Library

Right mouse-click on the Dependencies folder in the AdventureWorks.EntityLayer and add a project reference to the Common.Library project. Right mouse-click on the AdventureWorks.MAUI project's Dependencies folder and add a project dependency to the Common.Library project.

Inherit from the EntityBase Class

It's now time to inherit the EntityBase class from your User class. Open the EntityClasses\User.cs file and add a using statement at the top of the file.

using Common.Library;

Change the class declaration so the User class inherits from the EntityBase class.

public class User
{
}

To illustrate the use of the RaisePropertyChanged() method, just replace the one of the properties right now. You added code to change the LoginId property in the constructor of the UserDetailView class, so replace the simple LoginId property with the following code:

private string _LoginId = string.Empty;

public string LoginId
{
    get
    {
        return _LoginId;
    }
    set
    {
        _LoginId = value;
        RaisePropertyChanged(nameof(LoginId));
    }
}

In the setter for this property after setting the private variable, _LoginId, call the RaisePropertyChanged() method passing in the string name of this property.

Try It Out

Run the application and click on Users > Navigate to Detail. You should now see that the LoginId value has changed to the value you typed into the constructor. The User object assigned to the viewModel variable in the XAML is created when the call to the InitializeComponent() method is invoked in the constructor. At that time, the Entry control bound to the LoginId property contains the value created in the XAML. When you change the LoginId property with the assignment _ViewModel.LoginId = "ANewLoginId123", the RaisePropertyChanged() event is raised with the event argument set to “LoginId” and thus the Entry control bound to the LoginId property receives this notification to re-read the LoginId property in the object. When that happens, the Entry control now reads the value “ANewLoginId123” from the property and updates the Text property on itself.

Update all Properties on the User Class

All the properties in the User class follow the same design pattern as the one you just created for the LoginId property. Open the EntityClasses\User.cs file and replace the entire contents with the code shown in Listing 7.

Listing 7: All property set methods should call the RaisePropertyChanged() method

using Common.Library;
namespace AdventureWorks.EntityLayer;
public 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 = string.Empty;
  private string _Phone = string.Empty;
  private string _PhoneType = string.Empty;
  private bool _IsFullTime;
  private bool _IsEnrolledIn401k;
  private bool _IsEnrolledInHealthCare;
  private bool _IsEnrolledInHSA;
  private bool _IsEnrolledInFlexTime;
  private bool _IsEmployed;
  private DateTime _BirthDate = DateTime.Now.AddYears(-18);
  private TimeSpan? _StartTime = new(8, 0, 0);
  #endregion
  
  #region Public Properties
  public int UserId 
  {
    get { return _UserId; }
    set 
    {
      _UserId = value;
      RaisePropertyChanged(nameof(UserId));
    }
  }
  
  public string LoginId 
  {
    get { return _LoginId; }
    set {
      _LoginId = value;
      RaisePropertyChanged(nameof(LoginId));
    }
  }
  
  public string FirstName 
  {
    get { return _FirstName; }
    set {
      _FirstName = value;
      RaisePropertyChanged(nameof(FirstName));
    }
  }
  
  public string LastName 
  {
    get { return _LastName; }
    set {
      _LastName = value;
      RaisePropertyChanged(nameof(LastName));
    }
  }
  
  public string Email 
  {
    get { return _Email; }
    set {
      _Email = value;
      RaisePropertyChanged(nameof(Email));
    }
  }
  
  public string Password 
  {
    get { return _Password; }
    set {
      _Password = value;
      RaisePropertyChanged(nameof(Password));
    }
  }
  
  public string Phone 
  {
    get { return _Phone; }
    set {
      _Phone = value;
      RaisePropertyChanged(nameof(Phone));
    }
  }
  
  public string PhoneType 
  {
    get { return _PhoneType; }
    set {
      _PhoneType = value;
      RaisePropertyChanged(nameof(PhoneType));
    }
  }
  
  public bool IsFullTime 
  {
    get { return _IsFullTime; }
    set {
      _IsFullTime = value;
      RaisePropertyChanged(nameof(IsFullTime));
    }
  }
  
  public bool IsEnrolledIn401k 
  {
    get { return _IsEnrolledIn401k; }
    set {
      _IsEnrolledIn401k = value;
      RaisePropertyChanged(nameof(IsEnrolledIn401k));
    }
  }
  
  public bool IsEnrolledInHealthCare 
  {
    get { return _IsEnrolledInHealthCare; }
    set {
      _IsEnrolledInHealthCare = value;
      RaisePropertyChanged(nameof(IsEnrolledInHealthCare));
    }
  }
  
  public bool IsEnrolledInHSA 
  {
    get { return _IsEnrolledInHSA; }
    set {
      _IsEnrolledInHSA = value;
      RaisePropertyChanged(nameof(IsEnrolledInHSA));
    }
  }
  
  public bool IsEnrolledInFlexTime 
  {
    get { return _IsEnrolledInFlexTime; }
    set {
      _IsEnrolledInFlexTime = value;
      RaisePropertyChanged(
        nameof(IsEnrolledInFlexTime));
    }
  }
  
  public bool IsEmployed 
  {
    get { return _IsEmployed; }
    set {
      _IsEmployed = value;
      RaisePropertyChanged(nameof(IsEmployed));
    }
  }
  
  public DateTime BirthDate 
  {
    get { return _BirthDate; }
    set {
      _BirthDate = value;
      RaisePropertyChanged(nameof(BirthDate));
    }
  }
  
  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
}

Use the OnAppearing() Method

I don't like placing code that modifies properties in the constructor of a class. Doing this may cause exceptions that can be very difficult to troubleshoot. Instead, I recommend any changes to properties be performed in the OnAppearing() event. Setting properties, or calling methods, in this event also has the benefit that if the page is removed the shell, but later navigated back to, this event is fired again. This is ideal if you want to refresh the data from your data store and have the updated values displayed in the controls.

Open the Views\UserDetailView.xaml.cs file and remove the line of code that changes the LoginId in the constructor. Add the following code to override the OnAppearing() event:

protected override void OnAppearing()
{
    base.OnAppearing();
    _ViewModel.LoginId = "PeterPiper384";
    _ViewModel.FirstName = "Peter";
    _ViewModel.LastName = "Piper";
    _ViewModel.Email = "Peter@pipering.com";
}

Try It Out

Run the application and click on Users > Navigate to Detail to see the changes in each control that are bound to the properties you modified in the OnAppearing() event.

Summary

In this article, you learned many ways for the user to provide input other than simple fill-in-the-blank Entry controls. You also saw how to bind one control to another to solve some basic business application scenarios without writing C# code. You took advantage of using data binding to bind the properties of a User object to controls on the page. If you want to make changes to an object in your code behind, you need to raise the PropertyChanged event to have that change reflected in the user interface. Coming up in the next article, you'll be introduced to the Model-View View Model (MVVM) and the Dependency Injection (DI) design patterns. Both of these design patterns make your applications reusable, maintainable, and testable. In addition, commanding eliminates code behind and moves that code down into your view model classes for even more reusability.