学习笔记(Maui 09 Web 服务)

学习笔记(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))
         }
     }
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sleevefisher

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值