学习笔记(Maui 09 Web 服务)
DailyPoetryM 项目(对应 P17 P18)
本节通过 DailyPoetryM 项目讲解值转换器和 Web 服务
1 后台类型和前台类型的值转换
PageResult 页面在诗词题目下显示 Snippet。Poetry(后台类型)与 string(前台类型)不是同一种类型,为了实现二者绑定使用值转换器。
1.1 值转换器(Value Converter)定义
在 Maui 项目添加文件夹 Converers,并添加值转换类型 ConverterPoetryToString。
class ConverterPoetryToString : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// convert
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
接口 IValueConverter 提供两个函数,Convert 实现从后端类型(Poetry)向前端类型(string)转换;ConvertBack 实现从前端类型(string)向后端类型(Poetry)转换。本例不需实现 ConvertBack。
Convert 函数的传统写法
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() == typeof(Poetry))
{
var poetry = (Poetry)value;
return $"{poetry.Dynasty} . {poetry.Author} . {poetry.Snippet}";
}
return null;
}
类型的判断也可采用运算符 is
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Poetry)
{
var poetry = (Poetry)value;
return $"{poetry.Dynasty} . {poetry.Author} . {poetry.Snippet}";
}
return null;
}
运算符 is 实现类型判断。如果 value 是 Poetry 类型,返回 true;否则,返回 false。
采用语法糖简写传统写法
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Poetry poetry)
{
return $"{poetry.Dynasty} . {poetry.Author} {poetry.Snippet}";
}
else
{
return null;
}
}
更简洁的写法
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is Poetry poetry) ? $"{poetry.Dynasty} . {poetry.Author} {poetry.Snippet}" : null;
}
1.2 值转换器注册
在 Maui 项目 DailyPoetryM 的文件夹 Views 中的 PageResult.xaml 中进行注册。先将命名空间 Converters 引入。
xmlns:lc="clr-namespace:DailyPoetryM.Converters"
lc 表示 local converter,local 的使用和名字的选择无强制要求。接着注册资源,以便在 xaml 中使用。
<ContentPage.Resources>
<lc:ConverterPoetryToString x:Key="PoetryToString"/>
</ContentPage.Resources>
1.3 值转换器应用
在 Maui 项目 DailyPoetryM 的文件夹 Views 中的项 PageResult.xaml 中的 ListView 项中对 Label 进行绑定时使用。
<Label Text="{Binding Converter={StaticResource PoetryToString}}" LineBreakMode="TailTruncation"/>
如果 Binding 后不填写内容,默认绑定 Poetry,无法得到合适的类型显示。Converter 将 Poetry 类型转换成 string 类型进行显示。TailTruncation 表示无法显示的部分截断。
2 Xaml 内添加无限滚动
在 Maui 项目 DailyPoetryM 的文件夹 Views 中的 PageResult.xaml 中进行注册。先将命名空间 scroll 引入。
xmlns:scroll="clr-namespace:TheSalLab.MauiInfiniteScrolling;assembly=MauiInfiniteScrolling"
无限滚动是 ListView 的行为,在 ListView 中注册行为(Behaviors),实现无限滚动。
<ListView ItemsSource="{Binding Poetries}">
<ListView.Behaviors>
<scroll:MauiInfiniteScrollBehavior/>
</ListView.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="0,8,8,8">
<Label Text="{Binding Name}" FontAttributes="Bold"/>
<Label Text="{Binding Converter={StaticResource PoetryToString}}" LineBreakMode="TailTruncation"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
3 Web 服务
应用 web 服务,实现今日推荐页面,访问今日诗词网站。
3.1 今日诗词后台支持类和服务的设计
在中间库项目 DailyPoetryM.Library 的 Models 文件夹中增加 PoetryToday 类。用以承接从服务器返回的今日诗词。
public class PoetryToday
{
public string Snippet { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Dynasty { get; set; } = string.Empty;
public string Author { get; set;} = string.Empty;
public string Content { get; set; } = string.Empty;
public string Source { get; set;} = string.Empty;
}
不能保证服务器总是可用,当服务器不可用时,使用本地数据库。属性 Source 用来说明诗词的来源。
在中间库项目 DailyPoetryM.Library 的 Services 文件夹中增加接口IServicePoetryToday,定义从服务器获取今日诗词的行为。
public interface IServicePoetryToday
{
Task<PoetryToday> GetPoetryTodayAsync();
}
public static class PoetryTodaySources
{
public const string Jinrishici = nameof(Jinrishici);
public const string Local = nameof(Local);
}
类 PoetryTodaySources 用来给出类 PoetryToday 中的属性 Source 赋值。
在中间库项目 DailyPoetryM.Library 的 Services 文件夹中增加接口 IServicePoetryToday 的实现类 ServicePoetryToday。从服务器获取诗词需要 Token,所以在类 ServicePoetryToday 中需要定义获得 Token 的函数 GetTokenAsync。从本地数据库随机选取一首诗的函数 GetRandomPoetryAsync。
public class ServicePoetryToday : IServicePoetryToday
{
private readonly IServiceAlert _ServiceAlert;
private readonly IStoragePoetry _StoragePoetry;
public ServicePoetryToday(IServiceAlert sa, IStoragePoetry sp)
{
_ServiceAlert = sa;
_StoragePoetry = sp;
}
public async Task<PoetryToday> GetPoetryTodayAsync()
{
// do something;
}
public async Task<string> GetTokenAsync()
{
// do something;
}
public async Task<PoetryToday> GetRandomPoetryAsync()
{
// do something;
};
}
3.2 获取 Token 的函数 GetTokenAsync 实现
public async Task<string> GetTokenAsync()
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("");
var json = await response.Content.ReadAsStringAsync();
}
该版本在访问网络时可能因网络问题或服务器问题崩溃。且 HttpClient 不能 new 太多,占用资源过多。为了解决崩溃问题,更新为如下稳定运行版本。
public async Task<string> GetTokenAsync()
{
using var httpClient = new HttpClient();
HttpResponseMessage response;
try
{
response = await httpClient.GetAsync("https://v2.jinrishici.com/token");
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
// _ServiceAlert.Alert("Wrong", ex.Message.ToString(), "OK");
return string.Empty;
}
var json = await response.Content.ReadAsStringAsync();
var tokenJinrishici = JsonSerializer.Deserialize<TokenJinrishici>(json);
return tokenJinrishici.data;
}
使用 try catch 语句包裹网络访问,保证程序不会崩溃。只要当服务器返回代码不是 200 时,EnsureSuccessStatusCode()语句都会抛出异常。网络问题和服务器异常都会导致服务器返回码不是 200。类型 TokenJinrishici 表示从服务器获得的 Token,服务器返回的 Token 是字符串,使用 JsonSerializer.Deserialize< TokenJinrishici>(json) 将 Json 字符串转换成类型 TokenJinrishici。类型 TokenJinrishici 的定义。
public class TokenJinrishici
{
public string status { get; set; }
public string data { get; set; }
}
3.3 获取诗词的函数 GetPoetryTodayAsync 实现
public async Task<PoetryToday> GetPoetryTodayAsync()
{
var token = await GetTokenAsync(); // 获取今日诗词网站 Token
if (string.IsNullOrWhiteSpace(token))
{
return await GetRandomPoetryAsync(); // 如果从今日诗词网站取数据出了问题,从本地数据库随机取数据并返回
}
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-User-Token", token); // 将 Token 加到 http 头,从今日诗词网站取诗需要 Token
HttpResponseMessage response;
try
{
response = await httpClient.GetAsync("https://v2.jinrishici.com/sentence");
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
// _ServiceAlert.Alert("Wrong", ex.Message.ToString(), "OK");
return await GetRandomPoetryAsync();
}
var json = await response.Content.ReadAsStringAsync();
JinrishiciSentence jinrishiciSentence;
try
{
jinrishiciSentence = JsonSerializer.Deserialize<JinrishiciSentence>
(
json
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}
) ?? throw new Exception();
}
catch (Exception ex)
{
// _ServiceAlert.Alert("Wrong", ex.Message.ToString(), "OK");
return await GetRandomPoetryAsync();
}
// 反序列化结果转换成 PoetryToday 类型
try
{
return new PoetryToday
{
Snippet =
jinrishiciSentence.Data?.Content ??
throw new JsonException(),
Name =
jinrishiciSentence.Data.Origin?.Title ??
throw new JsonException(),
Dynasty =
jinrishiciSentence.Data.Origin.Dynasty ??
throw new JsonException(),
Author =
jinrishiciSentence.Data.Origin.Author ??
throw new JsonException(),
Content =
string.Join("\n",
jinrishiciSentence.Data.Origin.Content ??
throw new JsonException()),
Source = PoetryTodaySources.Jinrishici
};
}
catch (Exception ex)
{
// _ServiceAlert.Alert("Wrong", ex.Message.ToString(), "OK");
return await GetRandomPoetryAsync();
}
}
类型 JinrishiciSentence 表示从服务器获得的今日诗词,服务器返回的诗词是字符串,使用 JsonSerializer.Deserialize< JinrishiciSentence>(json) 将 Json 字符串转换成类型 JinrishiciSentence。类型 JinrishiciSentence 的定义。
public class JinrishiciSentence
{
public JinrishiciData? Data { get; set; } = new();
}
public class JinrishiciData
{
public string? Content { get; set; } = string.Empty;
public JinrishiciOrigin? Origin { get; set; } = new();
}
public class JinrishiciOrigin
{
public string? Title { get; set; } = string.Empty;
public string? Dynasty { get; set; } = string.Empty;
public string? Author { get; set; } = string.Empty;
public List<string>? Content { get; set; } = new List<string>();
public List<string>? Translate { get; set; } = new List<string>();
}
Json 字符串返回小写,类 JinrishiciSentence 大写,用 option 保证大小写不敏感,即 new JsonSerializerOptions { PropertyNameCaseInsensitive = true }。
- 操作符 ??:如果空则执行接下来的语句。?? throw new Exception() 表示返回结果空就抛出异常。
- 操作符 ?.:如果空则返回空, 否则调用。jinrishiciSentence.Data?.Content ?? throw new JsonException() 表示
jinrishiciSentence.Data 如果是空返回空,否则执行操作 .Content。
3.3 从本地数据随机读取诗词函数 GetRandomPoetryAsync 实现
public async Task<PoetryToday> GetRandomPoetryAsync()
{
var poetries = await _StoragePoetry.GetPoetriesAsync(
Expression.Lambda<Func<Poetry, bool>>(Expression.Constant(true),
Expression.Parameter(typeof(Poetry), "p")),
new Random().Next(30), 1);
var poetry = poetries.First();
return new PoetryToday
{
Snippet = poetry.Snippet,
Name = poetry.Name,
Dynasty = poetry.Dynasty,
Author = poetry.Author,
Content = poetry.Content,
Source = PoetryTodaySources.Local
};
}
3.4 简单测试
测试任务比较难实现,这里提供一个简单的形式上的测试,实际意义不大。在测试项目 DailyPoetry.TestProject 中 Serveices文件夹添加 ServicePoetryTodayTest 类,类内实现 GetPoetryTodayAsync_Default 函数对函数 GetPoetryTodayAsync 的测试。
public class ServicePoetryTodayTest
{
[Fact]
public async Task GetPoetryTodayAsync_Default()
{
ServicePoetryToday serviceTodayPoetry = new ServicePoetryToday(null, null);
var result = await serviceTodayPoetry.GetPoetryTodayAsync();
Assert.NotNull(result);
}
}
只能进行是否空(即只能看是否拿到数据,数据内容无法测试)的测试。个人测试无法通过,问题在 new JsonSerializerOptions。
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}
将以上注释掉可以通过测试。
4 警告服务
服务器崩溃等现象出现时,需要给出警告提示。在中间库项目 DailyPoetryM.Library 文件夹 Services 中添加接口 IServiceAlert 给用户提供错误警告信息。
public interface IServiceAlert
{
void Alert(string title, string message, string button);
}
5 页面“今日诗词”的实现
5.1 ViewModelPeotryToday 实现
在中间库项目 DailyPoetryM.Library 的 ViewModels 文件夹内添加页面后台支撑项 ViewModelPoetryToday。
public class ViewModelPoetryToday : ObservableObject
{
private readonly IServicePoetryToday _ServicePoetryToday;
public ViewModelPoetryToday(IServicePoetryToday spt)
{
_ServicePoetryToday = spt;
_LazyCommandLoaded = new Lazy<AsyncRelayCommand>(new AsyncRelayCommand(CommandLoadedFunction));
}
private Lazy<AsyncRelayCommand> _LazyCommandLoaded;
public AsyncRelayCommand CommandLoaded => _LazyCommandLoaded.Value;
private async Task CommandLoadedFunction()
{
IsLoading = true;
PoetryToday = await _ServicePoetryToday.GetPoetryTodayAsync();
IsLoading = false;
}
private bool _IsLoading;
public bool IsLoading
{
get => _IsLoading;
set => SetProperty(ref _IsLoading, value);
}
private PoetryToday _PoetryToday;
public PoetryToday PoetryToday
{
get => _PoetryToday;
set => SetProperty(ref _PoetryToday, value);
}
}
服务 _ServicePoetryToday 用来读取今日诗词。命令 CommandLoaded 在页面加载之后触发。属性 IsLoading 用来指示读取状态。
5.2 服务 ServiceAlert 实现
ServcieAlert 实现 UI 层操作,所以在 Maui 主项目 DailyPoetryM 的文件夹 Services 中添加类 ServiceAlert。
public class ServiceAlert : IServiceAlert
{
public void Alert(string title, string message, string button)
{
throw new NotImplementedException();
}
}
该类没有实现实际功能,仅供依赖注入使用。
5.3 依赖注入
在 Maui 主项目中的类 ServiceLocator 中添加 ServiceLocator 和 View ModelPoetryToday 的注册。
internal class ServiceLocator
{
private IServiceProvider _ServiceProvider;
public ViewModelResultPage ViewModelResultPage => _ServiceProvider.GetService<ViewModelResultPage>();
public ViewModelPoetryToday ViewModelPoetryToday => _ServiceProvider.GetService<ViewModelPoetryToday>();
public ServiceLocator()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IStoragePoetry, StoragePoetry>(); // 注册 IStoragePoetry 实现类为 StoragePoetry
serviceCollection.AddSingleton<IStoragePreference, StoragePreference>();
serviceCollection.AddSingleton<ViewModelResultPage>();
serviceCollection.AddSingleton<ViewModelPoetryToday>();
serviceCollection.AddSingleton<IServicePoetryToday, ServicePoetryToday>(); // 今日诗词服务
serviceCollection.AddSingleton<IServiceAlert, ServiceAlert>();
_ServiceProvider = serviceCollection.BuildServiceProvider();
}
}
5.4 两个值转换器
为了页面设计方便,实现两个值转换器。在 Maui 主项目 DailyPoetryM 文件夹 Converters 中添加类 ConverterNegativeBool。该值转换器类实现 bool 型取反。
public class ConverterNegativeBool : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return value is bool b ? !b : null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
在 Maui 主项目 DailyPoetryM 文件夹 Converters 中添加类 ConverterPoetryTodaySourceToBool。该值转换器实现将诗词来源转换成 bool 类型,如果诗词来自于今日诗词网站,则返回 true,如果诗词来源于本地数据库,则返回 false。具体通过传来的值和传来的转换器参数是否匹配判断,即二者匹配则返回 true,否则返回 false。
public class ConverterPoetryTodaySourceToBool : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is string source && parameter is string expectedSource && source == expectedSource;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
5.5 PagePoetryToday.xaml
在 Maui 主项目 DailyPoetryM 的 Views 文件夹内添加页面 PagePoetryToday.xaml,类型为 .Net MAUI ContentPage (XAML)。设置 BindingContext 。
BindingContext="{Binding ViewModelPoetryToday, Source={StaticResource ServiceLocator}}"
为了能够使用值转换器,引入其名称空间。
xmlns:lc="clr-namespace:DailyPoetryM.Converters"
lc 表示 local converters,注册值转换器
<ContentPage.Resources>
<lc:ConverterPoetryTodaySourceToBool x:Key="SourceToBool" />
<lc:ConverterNegativeBool x:Key="NegativeBool" />
</ContentPage.Resources>
为了能够使用事件转换命令机制,引入命名空间
xmlns:b="clr-namespace:TheSalLab.MauiBehaviors;assembly=TheSalLab.MauiBehaviors"
将 Loaded 事件转化为命令 CommandLoaded
<ContentPage.Behaviors>
<b:MauiEventHandlerBehavior EventName="Loaded">
<b:MauiActionCollection>
<b:MauiInvokeCommandAction Command="{Binding CommandLoaded}"/>
</b:MauiActionCollection>
</b:MauiEventHandlerBehavior>
</ContentPage.Behaviors>
页面设计
<Grid BackgroundColor="White">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout BackgroundColor="#66000000" VerticalOptions="End">
<StackLayout Padding="8" IsVisible="{Binding IsLoading}">
<ActivityIndicator Color="White" IsRunning="{Binding IsLoading}"/>
<Label TextColor="White" Text="正在载入" HorizontalOptions="Center"/>
</StackLayout>
<StackLayout Padding="8" IsVisible="{Binding IsLoading, Converter={StaticResource NegativeBool}}">
<Label FontSize="28" TextColor="White" Margin="0,0,0,8" Text="{Binding PoetryToday.Snippet}"/>
<StackLayout x:Name="DetailStackLayout">
<Label FontSize="18" TextColor="White" Margin="0,0,8,8" Text="{Binding PoetryToday.Author}" VerticalOptions="Center"/>
<Label FontSize="18" TextColor="White" Margin="0,0,8,8" Text="{Binding PoetryToday.Name}" VerticalOptions="Center"/>
<Button HorizontalOptions="Start" Margin="0,0,0,8" Text="查看详细" Command="{Binding ShowDetailCommand}" VerticalOptions="Center"/>
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label TextColor="White" FontSize="Micro" Text="推荐自" />
<Label TextColor="White" FontSize="Micro" TextDecorations="Underline" Text="今日诗词">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding JinrishiciCommand}"/>
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
<StackLayout BackgroundColor="#66000000">
<StackLayout.Padding>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="iOS" Value="8,8,8,20"/>
<On Platform="Android, UWP" Value="8,8,8,8" />
</OnPlatform>
</StackLayout.Padding>
</StackLayout>
</StackLayout>
</Grid>
页面复制后删减得到。保留了主要功能。需要注意的几个方面:
- Margin=“0,0,0,8” 设置顺序为左上右下,设置值应为 4 的倍数,方便适应 windows 系统屏幕缩放特性。
- AARRGGBB 表示透明度红色绿色和蓝色的含量,16 进制数字表示,范围是 0 - 255。#66000000 表示透明的黑色。
- 中的 VerticalOptions=“End” 表示位于底部。
- 实现启动旋转效果
- 静态类的静态成员引用使用 x:static
在 Maui 主项目 DailyPoetryM 的 AppShell.xaml.cs 内注册页面,运行成功。
Items.Add(new FlyoutItem
{
Title = nameof(PagePoetryToday),
Route = nameof(PagePoetryToday),
Items =
{
new ShellContent
{
ContentTemplate = new DataTemplate(typeof(PagePoetryToday))
}
}
});