
- •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 9 Local Data
are different kinds of databases, ranging from relational databases to distributed databases, cloud databases, and NoSQL databases. In this chapter, you will focus on relational and NoSQL databases.
Every application I have ever built has required some form of database, and I suspect that most of the applications that you will build will also require one. A database really provides value when you need to link data together or filter and sort the data in an efficient manner.
In your application, you are going to provide the ability to save a board and return a list of boards that the user has created. To abstract this slightly, you will be using the repository pattern. You will also provide the ability to store where the widgets have been placed so that they will be remembered when a user loads the board back up. Figure 9-1 shows the entity relationship diagram for the database you will be creating.
Figure 9-1. The entity relationship diagram of your database models
Repository Pattern
The repository pattern allows you to hide all the logic that deals with creating, reading, updating, and deleting (also known as CRUD) entities within your application. By using this pattern, it allows you to keep all the knowledge around how entities are loaded, saved, and more in a single place. This has the added benefit that if you want to completely change where your data is loaded from, you only need to change the implementation inside the repository. It also allows you to provide mock
implementations when wanting to perform things like unit testing and you don’t want to have to rely on an actual database existing.
262
Chapter 9 Local Data
Let’s add a new folder called Data and then add an interface for your repository to that folder called IBoardRepository. Change the code to look as follows:
using WidgetBoard.Models;
namespace WidgetBoard.Data;
public interface IBoardRepository
{
void CreateBoard(Board board);
void CreateBoardWidget(BoardWidget boardWidget);
void DeleteBoard(Board board);
IReadOnlyList<Board> ListBoards();
Board LoadBoard(int boardId);
void UpdateBoard(Board board);
}
Now that you have defined your interface, you can update your application’s codebase to use this interface when loading and saving your boards.
Creating aBoard
The first place to update is your BoardDetailsPageViewModel class, which provides support for creating a new board. Open up the class and make the following modifications.
Add a new IBoardRepository field.
private readonly IBoardRepository boardRepository;
263
Chapter 9 Local Data
Assign a valid instance to the boardRepository field; the modifications are in bold.
public BoardDetailsPageViewModel( ISemanticScreenReader semanticScreenReader, IBoardRepository boardRepository)
{
this.semanticScreenReader = semanticScreenReader; this.boardRepository = boardRepository;
SaveCommand = new Command( () => Save(),
() => !string.IsNullOrWhiteSpace(BoardName));
}
Use the boardRepository field when saving; the modifications are in bold.
private async void Save()
{
var board = new Board
{
Name = BoardName,
NumberOfColumns = NumberOfColumns, NumberOfRows = NumberOfRows
};
this.boardRepository.CreateBoard(board);
semanticScreenReader.Announce($"A new board with the name {BoardName} was created successfully.");
await Shell.Current.GoToAsync( "fixedboard",
264
Chapter 9 Local Data
new Dictionary<string, object>
{
{ "Board", board}
});
}
Listing Your Boards
In the previous chapters, you just added a fixed board and added it to the Boards collection in your AppShellViewModel class. Now you are going to modify it so that it can be populated by the boards the user creates and you store in the database. Open the AppShellViewModel.cs file and make the following changes.
Add a field for your IBoardRepository.
private readonly IBoardRepository boardRepository;
Modify your constructor to use the IBoardRepository as a dependency.
public AppShellViewModel( IBoardRepository boardRepository)
{
this.boardRepository = boardRepository;
}
Load the list of boards and populate your collection.
public void LoadBoards()
{
var boards = this.boardRepository.ListBoards();
foreach (var board in boards)
{
265

Chapter 9 Local Data
Boards.Add(board);
}
}
There is a further change that you need to make in order to allow your AppShellViewModel class to actually load the board. You need to hook into some of the lifecycle events that apply to Pages in .NET MAUI. AppShell inherits from Page, which means you get full access to those lifecycle events. The specific event you care about now is the OnAppearing event. It is called when your page is displayed on screen.
The OnAppearing method can be called multiple times during the lifetime of the page, so it is recommended to make your method idempotent or check whether it has been called before in order to prevent odd behavior when called a second time.
OnAppearing is a great choice for your scenario because it will result in your code being executed every time the view appears; this can be every time your flyout menu is opened. This provides you with the ability to refresh your list of boards every time the user opens the flyout menu. The main reason it is fine for your scenario is because you will be loading data from a local database with a limited number of boards to load, so it will be pretty quick. In scenarios where you are loading from an external webservice, it can take much more time to perform it and therefore
you may wish to maintain some level of caching and prevent calling the webservice every time the view appears. A better option under this
scenario and probably most typical scenarios in .NET MAUI applications is to use the OnNavigatedTo method.
Let’s open your AppShell.xaml.cs file and make use of this lifecycle method.
protected override void OnAppearing()
266
Chapter 9 Local Data
{
base.OnAppearing();
((AppShellViewModel)BindingContext).LoadBoards();
}
When the method gets called, you use the newly added LoadBoards method on your view model. The main reason you hook into this lifecycle event is when you eventually try to navigate to the last used board in the LoadBoards method, you need to make sure the application has started rendering; otherwise the navigation will fail.
Loading aBoard
Up until this point you have relied on passing the Board into the FixedBoardPageViewModel and displaying the details of that. The loading process would become rather inefficient if you were to load all boards and the associated BoardWidgets when listing all boards in the system, so you need to do this in a two-step process: first, list the boards as you did in the previous section and, second, load the board in the view model. This will be a slightly involved process so let’s walk through it step-by-step. Open the FixedBoardPageViewModel.cs file and make the following changes
Add the following fields to store the board that is loaded and the repository to perform the load:
private Board board;
private readonly IBoardRepository boardRepository;
In your constructor, add the board repository dependency and assign to the newly created field. Changes are in bold.
public FixedBoardPageViewModel( WidgetTemplateSelector widgetTemplateSelector, WidgetFactory widgetFactory,
267
Chapter 9 Local Data
IBoardRepository boardRepository)
{
WidgetTemplateSelector = widgetTemplateSelector; this.widgetFactory = widgetFactory; this.boardRepository = boardRepository;
Widgets = new ObservableCollection<IWidgetViewModel>();
AddWidgetCommand = new Command(OnAddWidget);
AddNewWidgetCommand = new Command<int>(index =>
{
IsAddingWidget = true; addingPosition = index;
});
}
Now let’s load the Board inside your ApplyQueryAttributes method. The changes are in bold.
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
var boardParameter = query["Board"] as Board;
board = boardRepository.LoadBoard(boardParameter.Id);
BoardName = board.Name;
NumberOfColumns = board.NumberOfColumns;
NumberOfRows = board.NumberOfRows;
foreach (var boardWidget in board.BoardWidgets)
{
268
Chapter 9 Local Data
var widgetViewModel = widgetFactory.CreateWidgetViewMod el(boardWidget.WidgetType);
widgetViewModel.Position = boardWidget.Position;
Widgets.Add(widgetViewModel);
}
}
Next, add the ability to save a widget’s position on the board.
private void SaveWidget(IWidgetViewModel widgetViewModel)
{
var boardWidget = new BoardWidget
{
BoardId = board.Id,
Position = widgetViewModel.Position, WidgetType = widgetViewModel.Type
};
boardRepository.CreateBoardWidget(boardWidget);
}
The above method will create a new BoardWidget model class and save it into the database for you.
Finally, you need to call the SaveWidget method. For the purpose of your application, you are going to provide an auto save feature, so each time a widget is added to the board, you will save it immediately to the database. In order to achieve this, you just need to add the bold line into your AddWidget method.
private void OnAddWidget()
{
if (SelectedWidget is null)
269