Create a categories page with the carousel control
In this online-only section, you will implement a web service to provide CRUD operations for categories in the Northwind database and then call the web service to display categories including photos in a rotating carousel.
- Creating a minimal API web service for categories
- Configuring the .NET MAUI app to allow unsecure connections
- Implementing the Model-View-ViewModel pattern
- Getting categories from the web service
We will create a web service for working with categories in the Northwind database:
-
Using your preferred code editor, add a web service project, as defined in the following list:
- Project template: ASP.NET Core Web API /
webapi --use-minimal-apis. - Workspace/solution file and folder:
Chapter16. - Project file and folder:
Northwind.Maui.WebApi.Service. - Authentication type: None.
- Configure for HTTPS: Selected.
- Enable Docker: Cleared.
- Use controllers (uncheck to use minimal APIs): Cleared.
- Enable OpenAPI support: Selected.
- Do not use top-level statements: Cleared.
- Project template: ASP.NET Core Web API /
-
Add a project reference to the Northwind database context project for SQL Server, as shown in the following markup:
<ItemGroup>
<ProjectReference Include="..\..\Chapter03\Northwind.Common.DataContext.SqlServer\Northwind.Common.DataContext.SqlServer.csproj" />
</ItemGroup>- At the command prompt or terminal, build the
Northwind.Maui.WebApi.Serviceproject to make sure the entity model class library projects outside the current solution are properly compiled, as shown in the following command:dotnet build. - In the
Propertiesfolder, inlaunchSettings.json, for thehttpsprofile, modify theapplicationUrlto use port5161forhttpsand port5162forhttp, as shown highlighted in the following configuration:
"applicationUrl": "https://localhost:5161;http://localhost:5162",- In
launchSettings.json, for thehttpprofile, modify theapplicationUrlto use port5162forhttp, as shown highlighted in the following configuration:
"applicationUrl": "http://localhost:5162",- In
Program.cs, delete the statements about the weather service and replace them with statements to disable HTTPS redirection while developing and to configure minimal API endpoints for data operations on categories, as shown highlighted in the following code:
using Microsoft.AspNetCore.Mvc; // To use [FromServices].
using Northwind.EntityModels; // To use AddNorthwindContext method.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddNorthwindContext();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseHttpsRedirection();
}
app.MapGet("api/categories", (
[FromServices] NorthwindContext db) => db.Categories)
.WithName("GetCategories")
.Produces<Category[]>(StatusCodes.Status200OK);
app.MapGet("api/categories/{id:int}", (
[FromRoute] int id,
[FromServices] NorthwindContext db) =>
db.Categories.Where(category => category.CategoryId == id))
.WithName("GetCategory")
.Produces<Category[]>(StatusCodes.Status200OK);
app.MapPost("api/categories", async (
[FromBody] Category category,
[FromServices] NorthwindContext db) =>
{
db.Categories.Add(category);
await db.SaveChangesAsync();
return Results.Created($"api/categories/{category.CategoryId}", category);
}).WithOpenApi()
.Produces<Category>(StatusCodes.Status201Created);
app.MapPut("api/categories/{id:int}", async (
[FromRoute] int id,
[FromBody] Category category,
[FromServices] NorthwindContext db) =>
{
Category? foundCategory = await db.Categories.FindAsync(id);
if (foundCategory is null) return Results.NotFound();
foundCategory.CategoryName = category.CategoryName;
foundCategory.Description = category.Description;
foundCategory.Picture = category.Picture;
await db.SaveChangesAsync();
return Results.NoContent();
}).WithOpenApi()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status204NoContent);
app.MapDelete("api/categories/{id:int}", async (
[FromRoute] int id,
[FromServices] NorthwindContext db) =>
{
if (await db.Categories.FindAsync(id) is Category category)
{
db.Categories.Remove(category);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
}).WithOpenApi()
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status204NoContent);
app.Run();- Start the web service project and note the Swagger documentation.
- Click GET /api/categories to expand that section.
- Click the Try it out button, click the Execute button, and note that category entities are returned.
- Close the browser and shut down the web server.
Now you will configure the Northwind.Maui.Blazor.Client project to allow unsecure HTTP requests to the web service:
- In the
Northwind.Maui.Blazor.Clientproject, in thePlatforms/iOSfolder, open theInfo.plistfile by right-clicking and opening it with the XML (Text) Editor. - At the bottom of the dictionary, add a new key named
NSAppTransportSecuritythat is a dictionary, and in it, add a key namedNSAllowsArbitraryLoadsthat has a value oftrue, as shown in the following partial markup:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
...
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>- Save and close
Info.plist. - In the
Platforms/Androidfolder, in theResourcesfolder, add a new folder namedxml. - In the
xmlfolder, add a new XML file namednetwork_security_config.xml, and add entries to enable cleartext when connecting over the virtual router's special IP address that maps out to localhost, as shown in the following markup:
<?xml version="1.0" encoding="utf-8" ?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>- In the
Androidfolder, inAndroidManifest.xml, add an attribute to the<application>element to reference the new XML file, as shown in the following markup:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true"
android:icon="@mipmap/appicon"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true">
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>- Save all the changes.
Now, let's define a model and a view model for working with categories:
- In the
Northwind.Maui.Blazor.Clientproject file, add package references for the .NET MAUI Community Toolkit and for the MVVM Community Toolkit, as shown in the following markup:
<PackageReference Include="CommunityToolkit.Maui" Version="5.2.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />- Build the project to restore packages.
Note that you will see a warning because the .NET MAUI Community Toolkit runs a code analyzer that checks to see if you have called the extension method to use the toolkit. You will do that in the next step.
- In
MauiProgram.cs, add a call to an extension method to enable the .NET MAUI Community Toolkit, as shown highlighted in the following code:
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>- In the
Views/Categoriesfolder, add a new class namedCategory.cs. Modify it to use the MVVM Community Toolkit to implement an observable category model that matches theCategoryentity models defined in the SQL Server EF Core models, but with an extra property to generate a path to a picture of each category as an alternative to the bytes stored in the database, as shown in the following code:
// To use ObservableObject, [ObservableProperty].
using CommunityToolkit.Mvvm.ComponentModel;
namespace Northwind.Maui.Blazor.Client.Views.Categories;
internal partial class Category : ObservableObject
{
// The field names must be private and camelCase or _camelCase because the
// source-generated public property names will be TitleCase.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PicturePath))]
private int categoryId;
[ObservableProperty]
private string categoryName;
[ObservableProperty]
private string description;
[ObservableProperty]
private byte[] picture;
[ObservableProperty]
private string picturePath;
}The
PicturePathproperty will use the patterncategoryX_small.jpeg, whereXis the category ID. Therefore, if the category ID changes, we must inform any data bindings that anything bound to thePicturePathwill also need to be updated. We do this by decorating thecategoryIdfield with[NotifyPropertyChangedFor].
- In the
Views/Categoriesfolder, add a new class namedCategoriesViewModel.cs, and modify it to inherit fromObservableCollection<T>, have some commands, and get the categories from the web service, as shown in the following code:
using CommunityToolkit.Mvvm.Input; // To use [RelayCommand].
using System.Collections.ObjectModel; // To use ObservableCollection<T>.
using System.Net.Http.Headers; // To use MediaTypeWithQualityHeaderValue.
using System.Net.Http.Json; // To use ReadFromJsonAsync<T>.
namespace Northwind.Maui.Blazor.Client.Views.Categories;
internal partial class CategoriesViewModel : ObservableCollection<Category>
{
// These properties do not need to support two-way binding
// because they are set programmatically to show to user.
public string InfoMessage { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
public bool ErrorMessageVisible { get; set; }
public CategoriesViewModel()
{
try
{
string domain = DeviceInfo.Platform
== DevicePlatform.Android ? "10.0.2.2" : "localhost";
HttpClient client = new()
{ BaseAddress = new Uri($"http://{domain}:5162") };
InfoMessage = $"BaseAddress: {client.BaseAddress}. ";
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client
.GetAsync("api/categories").Result;
response.EnsureSuccessStatusCode();
IEnumerable<Category> categories =
response.Content.ReadFromJsonAsync
<IEnumerable<Category>>().Result;
foreach (Category category in categories)
{
category.Picture = category.Picture.AsSpan(
offset, category.Picture.Length - offset).ToArray();
category.PicturePath = $"category{category.CategoryId}_small.jpeg";
Add(category);
}
InfoMessage += $"{Count} categories loaded.";
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
ErrorMessageVisible = true;
}
}
[RelayCommand]
private void AddCategoryToFavorites()
{
Console.WriteLine("Add category to favorites");
}
[RelayCommand]
private void DeleteCategory()
{
Console.WriteLine("Delete category");
}
}Now, we can modify the categories page to show the categories in a carousel:
- In
App.xaml, modify the resources for thePageBackgroundColorandPrimaryTextColor, and theBackgroundColorfor buttons, as shown in the following markup:
<?xml version="1.0" encoding="UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Northwind.Maui.Blazor.Client"
x:Class="Northwind.Maui.Blazor.Client.App">
<Application.Resources>
<ResourceDictionary>
<Color x:Key="PageBackgroundColor">LightGray</Color>
<Color x:Key="PrimaryTextColor">SlateGray</Color>
<Style TargetType="Label">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="BackgroundColor"
Value="{DynamicResource PageBackgroundColor}" />
<Setter Property="Padding" Value="14,10" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>- In
CategoriesPage.xaml, import namespaces for working with types defined at the project level using the prefix local and types in theCategoriesfolder using the prefixcategories, then create an instance of the categories view model for the binding context of the content page, and then in the vertical stack layout, add a label to show information about the web service endpoint, a label to show any error message, and a carousel with indicator lights, as shown highlighted in the following markup:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Northwind.Maui.Blazor.Client.Views.CategoriesPage"
xmlns:local="clr-namespace:Northwind.Maui.Blazor.Client"
xmlns:categories=
"clr-namespace:Northwind.Maui.Blazor.Client.Views.Categories"
xmlns:toolkit=
"http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
Title="Categories"
BackgroundColor="{StaticResource PageBackgroundColor}">
<ContentPage.BindingContext>
<categories:CategoriesViewModel />
</ContentPage.BindingContext>
<VerticalStackLayout>
<HorizontalStackLayout Spacing="20" Padding="20">
<Label Text="{Binding InfoMessage}" />
<Label Text="{Binding ErrorMessage}" TextColor="Red"
IsVisible="{Binding ErrorMessageVisible}" />
</HorizontalStackLayout>
<CarouselView x:Name="carouselView"
ItemsSource="{Binding .}"
IndicatorView="indicatorView"
PeekAreaInsets="10"
Loop="False">
<CarouselView.EmptyView>
<ContentView>
<VerticalStackLayout HorizontalOptions="Center"
VerticalOptions="Center">
<Label Text="No results matched your filter."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</ContentView>
</CarouselView.EmptyView>
<CarouselView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Frame HasShadow="True"
BorderColor="{StaticResource PrimaryTextColor}"
CornerRadius="10"
Margin="20"
HeightRequest="450"
HorizontalOptions="Center"
VerticalOptions="Center">
<VerticalStackLayout>
<Label Text="{Binding CategoryName}"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Center"
VerticalOptions="Center" />
<Image Source="{Binding PicturePath}"
Aspect="AspectFill"
HeightRequest="250"
WidthRequest="375"
HorizontalOptions="Center" />
<Label Text="{Binding Description}"
FontAttributes="Italic"
HorizontalOptions="Center"
MaxLines="5"
LineBreakMode="TailTruncation" />
</VerticalStackLayout>
</Frame>
</VerticalStackLayout>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
<Frame BackgroundColor="{StaticResource PrimaryTextColor}"
CornerRadius="5" HorizontalOptions="Center">
<IndicatorView x:Name="indicatorView"
IndicatorColor="{StaticResource PageBackgroundColor}"
SelectedIndicatorColor="DeepSkyBlue"
HorizontalOptions="Center" />
</Frame>
</VerticalStackLayout>
</ContentPage>- Start the
Northwind.Maui.WebApi.Serviceproject using thehttpsprofile, and note the endpoints it is listening on, as shown highlighted in the following output:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5161
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5162
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\apps-services-net8\Chapter16\Northwind.Maui.WebApi.Service
- Run the
Northwind.Maui.Blazor.Clientproject using the Android emulator, navigate to theCategories page, and note that eight categories are loaded from the web service and displayed in the carousel, with indicator lights at the bottom of the page view, as shown in Figure 16C.1:
Figure 16C.1: Categories in the carousel on Android
- Note that the user can swipe left and right to flip between categories or click the dots in the indicator view to quickly jump to the matching category.
- Close the Android emulator.
- Run the Northwind.Maui.Blazor.Client project using the Windows machine, navigate to the Categories page, and note that eight categories are loaded from the web service and displayed in the carousel, with indicator lights at the bottom of the page view, as shown in Figure 16C.2:
Figure 16C.2: Categories in the carousel on Windows
- Note that the user can use the horizontal scrollbar at the bottom to scroll between categories or click the dots in the indicator view to quickly jump to the matching category.
- Close the Windows app.