
- •Table of Contents
- •About the Author
- •About the Technical Reviewer
- •Acknowledgments
- •Introduction
- •What is .NET MAUI?
- •Digging a Bit Deeper
- •Where Did It Come From?
- •How It Differs From the Competition
- •Why Use .NET MAUI?
- •Supported Platforms
- •Code Sharing
- •Developer Freedom
- •Community
- •Fast Development Cycle
- •.NET Hot Reload
- •XAML Hot Reload
- •Performance
- •Strong Commercial Offerings
- •Limitations of .NET MAUI
- •No Web Assembly (WASM) Support
- •No Camera API
- •Apps Won’t Look Identical on Each Platform
- •Lack of Media Playback Out of the Box
- •The Glass Is Half Full, Though
- •How to Build .NET MAUI Applications
- •Visual Studio
- •Visual Studio (Windows)
- •Visual Studio for Mac
- •Rider
- •Visual Studio Code
- •Summary
- •Setting Up Your Environment
- •macOS
- •Visual Studio for Mac
- •Xcode
- •Remote Access
- •Windows
- •Visual Studio
- •Visual Studio to macOS
- •Troubleshooting Installation Issues
- •.NET MAUI Workload Is Missing
- •Visual Studio Installer
- •Command Line
- •Creating Your First Application
- •Creating in Visual Studio
- •Creating in the Command Line
- •Building and Running Your First Application
- •Getting to Know Your Application
- •WidgetBoard
- •Summary
- •Source Code
- •Project Structure
- •/Platforms/ Folder
- •Android
- •MacCatalyst
- •Tizen
- •Windows
- •Summary
- •/Resources/ Folder
- •Fonts
- •Images
- •Generic Host Builder
- •What Is Dependency Injection?
- •Registering Dependencies
- •AddSingleton
- •AddTransient
- •AddScoped
- •Application Lifecycle
- •Application States
- •Lifecycle Events
- •Handling Lifecycle Events
- •Cross-Platform Mappings to Platform Lifecycle Events
- •Platform-Specific Lifecycle Events
- •Android
- •Windows
- •Summary
- •A Measuring Stick
- •Prerequisites
- •Model View ViewModel (MVVM)
- •Model
- •View
- •XAML
- •C# (Code-Behind)
- •ViewModel
- •Model View Update (MVU)
- •Getting Started with Comet
- •Adding Your MVU Implementation
- •XAML vs. C# Markup
- •Plain C#
- •C# Markup
- •Chosen Architecture for This Book
- •Adding IWidgetViewModel
- •Adding BaseViewModel
- •Adding ClockWidgetViewModel
- •Adding Views
- •Adding IWidgetView
- •Adding ClockWidgetView
- •Viewing Your Widget
- •Modifying MainPage.xaml
- •Modifying MainPage.xaml.cs
- •Taking the Application for a Spin
- •MVVM Enhancements
- •MVVM Frameworks
- •Magic
- •Summary
- •Source Code
- •Prerequisites
- •Models
- •BaseLayout.cs
- •FixedLayout.cs
- •Board.cs
- •Pages
- •BoardDetailsPage
- •FixedBoardPage
- •ViewModels
- •AppShellViewModel
- •BoardDetailsPageViewModel
- •FixedBoardPageViewModel
- •App Icons
- •Adding Your Own Icon
- •Platform Differences
- •Android
- •Splash Screen
- •XAML
- •Dissecting a XAML File
- •Building Your First XAML Page
- •Layouts
- •AbsoluteLayout
- •FlexLayout
- •Grid
- •HorizontalStackLayout
- •VerticalStackLayout
- •Data Binding
- •Binding
- •BindingContext
- •Path
- •Mode
- •Source
- •Applying the Remaining Bindings
- •MultiBinding
- •Command
- •Compiled Bindings
- •Shell
- •ShellContent
- •Navigation
- •Registering Pages for Navigation
- •Performing Navigation
- •Navigating Backwards
- •Passing Data When Navigating
- •Flyout
- •FlyoutHeader
- •FlyoutContent
- •Selected Board
- •Navigation to the Selected Board
- •Setting the BindingContext of Your AppShell
- •Register AppShell with the MAUI App Builder
- •Resolve the AppShell Instead of Creating It
- •Tabs
- •Search
- •Taking Your Application for a Spin
- •Summary
- •Source Code
- •Extra Assignment
- •Placeholder
- •ILayoutManager
- •BoardLayout
- •BoardLayout.xaml
- •BindableLayout
- •BoardLayout.xaml.cs
- •Adding the LayoutManager Property
- •Adding the ItemsSource Property
- •Adding the ItemTemplateSelector Property
- •Handling the ChildAdded Event
- •Adding Remaining Bits
- •FixedLayoutManager
- •Accepting the Number of Rows and Columns for a Board
- •Adding the NumberOfColumns Property
- •Adding the NumberOfRows Property
- •Building the Board Layout
- •Setting the Correct Row/Column Position for Each Widget
- •Using Your Layout
- •Allowing for the Registration of Widget Views and View Models
- •Creation of a Widget View
- •Creation of a Widget View Model
- •Registering the Factory with MauiAppBuilder
- •Registering Your ClockWidget with the Factory
- •WidgetTemplateSelector
- •Registering the Template Selector with MauiAppBuilder
- •Updating FixedBoardPageViewModel
- •Finally Using the Layout
- •Summary
- •Source Code
- •Extra Assignment
- •What Is Accessibility?
- •Why Make Your Applications Accessible?
- •What to Consider When Making Your Applications Accessible
- •How to Make Your Application Accessible
- •Screen Reader Support
- •SemanticProperties
- •SemanticProperties.Description
- •SemanticProperties.Hint
- •SemanticProperties.HeadingLevel
- •SemanticScreenReader
- •AutomationProperties
- •AutomationProperties.ExcludedWithChildren
- •AutomationProperties.IsInAccessibleTree
- •Suitable Contrast
- •Dynamic Text Sizing
- •Avoiding Fixed Sizes
- •Preferring Minimum Sizing
- •Font Auto Scaling
- •Testing Your Application’s Accessibility
- •Android
- •macOS
- •Windows
- •Accessibility Checklist
- •Summary
- •Source Code
- •Extra Assignment
- •Adding the Ability to Add a Widget to a Board
- •Possible Ways of Achieving Your Goal
- •Showing a Modal Page
- •The Chosen Approach
- •Adding Your Overlay View
- •Updating Your View Model
- •Showing the Overlay View
- •Styling
- •Examining the Default Styles
- •TargetType
- •ApplyToDerivedTypes
- •Setter
- •AppThemeBinding
- •Further Reading
- •Triggers
- •Creating ShowOverlayTriggerAction
- •Using ShowOverlayTriggerAction
- •Further Reading
- •Animations
- •Basic Animations
- •Combining Basic Animations
- •Chaining Animations
- •Concurrent Animations
- •Cancelling Animations
- •Easings
- •Complex Animations
- •Recreating the ScaleTo Animation
- •Creating a Rubber Band Animation
- •Combining Triggers and Animations
- •Summary
- •Source Code
- •Extra Assignment
- •Animate the BoxView Overlay
- •Animate the New Widget
- •What Is Local Data?
- •File System
- •Cache Directory
- •App Data Directory
- •Database
- •Repository Pattern
- •Listing Your Boards
- •SQLite
- •Installing SQLite-net
- •Using Sqlite-net
- •Connecting to an SQLite database
- •Mapping Your Models
- •Creating Your Tables
- •Inserting into an SQLite Database
- •Reading a Collection from an SQLite Database
- •Reading a Single Entity from an SQLite Database
- •Deleting from an SQLite Database
- •Updating an Entity in an SQLite Database
- •LiteDB
- •Installing LiteDB
- •Using LiteDB
- •Connecting to a LiteDB database
- •Mapping Your Models
- •Creating Your Tables
- •Inserting into a LiteDB Database
- •Reading a Collection from a LiteDB Database
- •Reading a Single Entity from a LiteDB Database
- •Deleting from a LiteDB Database
- •Updating an Entity in a LiteDB Database
- •Database Summary
- •Application Settings (Preferences)
- •What Can Be Stored in Preferences?
- •Setting a Value in Preferences
- •Getting a Value in Preferences
- •Checking if a Key Exists in Preferences
- •Secure Storage
- •Storing a Value Securely
- •Reading a Secure Value
- •Removing a Secure Value
- •Platform specifics
- •Android
- •Windows
- •Summary
- •Source Code
- •Extra Assignment
- •What Is Remote Data?
- •Considerations When Handling Remote Data
- •Loading Times
- •Failures
- •Security
- •Webservices
- •The Open Weather API
- •Creating an Open Weather Account
- •Creating an Open Weather API key
- •Using System.Text.Json
- •Creating Your Models
- •Connecting to the Open Weather API
- •Registering Your Widget
- •Testing Your Widget
- •Adding Some State
- •Converting the State to UI
- •Displaying the Loading State
- •Displaying the Loaded State
- •Displaying the Error State
- •Simplifying Webservice Access
- •Prebuilt Libraries
- •Code Generation Libraries
- •Adding the Refit NuGet Package
- •Further Reading
- •Polly
- •Summary
- •Source Code
- •Extra Assignment
- •TODO Widget
- •Quote of the Day Widget
- •NASA Space Image of the Day Widget
- •.NET MAUI Essentials
- •Permissions
- •Checking the Status of a Permission
- •Requesting Permission
- •Handling Permissions in Your Application
- •Using the Geolocation API
- •Registering the Geolocation Service
- •Using the Geolocation Service
- •Displaying Permission Errors to Your User
- •Configuring Platform-Specific Components
- •Android
- •Windows
- •Platform-Specific API Access
- •Platform-Specific Code with Compiler Directives
- •Platform-Specific Code in Platform Folders
- •Overriding the Platform-Specific UI
- •OnPlatform
- •OnPlatform Markup Extension
- •Conditional Statements
- •Handlers
- •Customizing Controls with Mappers
- •Scoping of Mapper Customization
- •Further Reading
- •Summary
- •Source Code
- •Extra Assignment
- •Barometer Widget
- •Geocoding Lookup
- •Unit Testing
- •Unit Testing in .NET MAUI
- •xUnit
- •NUnit
- •MSTest
- •Your Chosen Testing Framework
- •Adding Your Own Unit Tests
- •Adding a Unit Test Project to Your Solution
- •Modify Your Application Project to Target net7.0
- •Adding a Reference to the Project to Test
- •Modify Your Test Project to Use MAUI Dependencies
- •Testing Your View Models
- •Testing BoardDetailsPageViewModel
- •Testing INotifyPropertyChanged
- •Testing Asynchronous Operations
- •Creating Your ILocationService Mock
- •Creating Your WeatherForecastService Mock
- •Creating Your Asynchronous Tests
- •Testing Your Views
- •Creating Your ClockWidgetViewModel Mock
- •Creating Your View Tests
- •Device Testing
- •Creating a Device Test Project
- •Adding a Device-Specific Test
- •Running Device-Specific Tests
- •Snapshot Testing
- •Snapshot Testing Your Application
- •Passing Thoughts
- •Summary
- •Source Code
- •.NET MAUI Graphics
- •Maintaining the State of the Canvas
- •Further Reading
- •Building a Sketch Widget
- •Representing a User Interaction
- •Registering Your Widget
- •Taking Your Widget for a Test Draw
- •Summary
- •Source Code
- •Extra Assignment
- •Distributing Your Application
- •Android
- •Additional Resources
- •Certificate
- •Identifier
- •Capabilities
- •Entitlements
- •Provisioning Profiles
- •Additional Resources
- •macOS
- •Additional Resources
- •Windows
- •Additional Resources
- •Following Good Practices
- •Performance
- •Startup Tracing
- •Image Sizes
- •Linking
- •What Is Linking?
- •Issues That Crop Up
- •Crashes/Analytics
- •Sentry
- •App Center
- •Obfuscation
- •Distributing Test Versions
- •Summary
- •Looking at the Final Product
- •Taking the Project Further
- •Useful Resources
- •StackOverflow
- •GitHub
- •YouTube
- •Gerald Versluis
- •James Montemagno
- •Social Media
- •Yet More Goodness
- •Looking Forward
- •Comet
- •Testing
- •Index
Chapter 6 Creating Our Own Layout
Accepting the Number of Rows and Columns for a Board
You need to add the ability to set the number of rows and columns to be displayed in your fixed layout board. For this, you are going to add two bindable properties to your FixedLayoutManager class.
Adding the NumberOfColumns Property
public static readonly BindableProperty NumberOfColumnsProperty =
BindableProperty.Create(
nameof(NumberOfColumns),
typeof(int),
typeof(FixedLayoutManager), defaultBindingMode: BindingMode.OneWay, propertyChanged: OnNumberOfColumnsChanged);
public int NumberOfColumns
{
get => (int)GetValue(NumberOfColumnsProperty); set => SetValue(NumberOfColumnsProperty, value);
}
static void OnNumberOfColumnsChanged(BindableObject bindable, object oldValue, object newValue)
{
var manager = (FixedLayoutManager)bindable;
manager.InitialiseGrid();
}
179

Chapter 6 Creating Our Own Layout
The key difference with this implementation over the previous bindable properties that you created is the use of the propertyChanged parameter. It allows you to define a method (see OnNumberOfColumnsChanged) that will be called whenever the property value changes.
The property changed method will only be called when the value changes. This means that it may not be called initially if the value does not change from the default value.
Adding the NumberOfRows Property
public static readonly BindableProperty NumberOfRowsProperty = BindableProperty.Create(
nameof(NumberOfRows),
typeof(int),
typeof(FixedLayoutManager), defaultBindingMode: BindingMode.OneWay, propertyChanged: OnNumberOfRowsChanged);
public int NumberOfRows
{
get => (int)GetValue(NumberOfRowsProperty); set => SetValue(NumberOfRowsProperty, value);
}
static void OnNumberOfRowsChanged(BindableObject bindable, object oldValue, object newValue)
{
var manager = (FixedLayoutManager)bindable;
manager.InitialiseGrid();
}
180
Chapter 6 Creating Our Own Layout
This is virtually identical to the NumberOfColumns property that you just added, except for the NumberOfRows value.
Providing Tap/Click Support Through
a Command
The next item on your list is to provide the ability to handle tap/click support. This is your first time providing command support; you used commands in your bindings, but that was on the source side rather than the target side like here.
First, you need to add the bindable property, which should start to feel rather familiar.
public static readonly BindableProperty PlaceholderTappedCommandProperty =
BindableProperty.Create(
nameof(PlaceholderTappedCommand),
typeof(ICommand),
typeof(FixedLayoutManager));
public ICommand PlaceholderTappedCommand
{
get => (ICommand)GetValue(PlaceholderTappedCommand Property);
set => SetValue(PlaceholderTappedCommandProperty, value);
}
Next, you need to add the code that will execute the command. You will be relying on the use of a TapGestureRecognizer by adding one to your Placeholder control inside your InitialiseGrid method that you will be adding in the next section. For now, you can add the method that will be used so that you can focus on how to execute the command. Let’s add the code and then look over the details.
181
Chapter 6 Creating Our Own Layout
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
if (sender is Placeholder placeholder)
{
if (PlaceholderTappedCommand?.CanExecute(placeholder. Position) == true)
{
PlaceholderTappedCommand.Execute(placeholder.
Position);
}
}
}
You can see from the implementation that there are three main parts to the command execution logic:
•\ |
First, you make sure that command has a value. |
•\ |
Second, you check that you can execute the command. |
|
If you recall back in Chapter 5 you provided a method |
|
to prevent the command from executing if the user |
|
hadn’t entered a BoardName. |
•\ |
Finally, you execute the command and pass in the |
|
command parameter. For this scenario, you will be |
|
passing in the current position of the placeholder so |
|
when a widget is added, it can be placed in the same |
|
position. |
Building the Board Layout
Now you can focus on laying out the underlying Grids so that they display as per the user’s entered values for rows and columns.
182
Chapter 6 Creating Our Own Layout
First, add in a property to store the current Board because you need to use it when building the layout. You also need to record whether you have built the layout to prevent any unnecessary updates rebuilding the user interface.
private BoardLayout board; private bool isInitialised;
public BoardLayout Board
{
get => board; set
{
board = value;
InitialiseGrid();
}
}
Your method to build the grid layout has several parts, so let’s add them as you go and discuss their value. You initially need to make sure that you have valid values for the Board, NumberOfRows and NumberOfColumns properties plus you haven’t already built the UI.
private void InitialiseGrid()
{
if (Board is null || NumberOfColumns == 0 || NumberOfRows == 0 || isInitialised == true)
{
return;
}
isInitialised = true;
}
183
Chapter 6 Creating Our Own Layout
The next step is to use the NumberOfColumns value and add them to your Board. Let’s add this to the end of the InitialiseGrid method.
for (int i = 0; i < NumberOfColumns; i++)
{
Board.AddColumn(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
}
The GridUnitType.Star value means that each column will have an even share of the width of the grid. So, if the Grid is 300 pixels wide and you have 3 columns, then each column has a resulting width of 100 pixels.
The next step is to use the NumberOfRows value and add them to your Board. Let’s add this to the end of the InitialiseGrid method.
for (int i = 0; i < NumberOfRows; i++)
{
Board.AddRow(new RowDefinition(new GridLength(1, GridUnitType.Star)));
}
The final step in your InitialiseGrid method is to populate each cell (row and column) combination with a Placeholder control.
for (int column = 0; column < NumberOfColumns; column++)
{
for (int row = 0; row < NumberOfRows; row++)
{
var placeholder = new Placeholder();
placeholder.Position = row * NumberOfColumns + column; var tapGestureRecognizer = new TapGestureRecognizer();
184