.NET MAUI 实现界面多态有很多种方式,今天主要来说说在日常开发中常见的需求该如何应对。

需求一:在不同设备上使用不同 UI 外观

.NET MAUI是一个跨平台的UI框架,可在一个项目中开发Android、iOS、Windows、MacOS等多个平台的应用。在不同设备上我们希望应用的界面或交互方式能够有所不同。

比如在本示例中,我们希望博客条目的菜单使用平台特性的交互方式以方便触屏或鼠标的操作:比如手机设备中博客条目的菜单使用侧滑方式呈现,而在桌面设备中使用右键菜单呈现。

要实现不同平台下的控件外观,我们可以定义一个ContentView控件,然后在不同平台上使用不同的控件模板(ControlTemplate)

控件模板(ControlTemplate)是我们的老朋友了,早在WPF时代就已经出现了,它可以完全改变一个控件的可视结构和外观,与使用Style改变控件外观样式和行为样式不同,使用Style只能改变控件已有的属性。

定义控件 UI 外观

首先用控件模板定义博客条目的外观,“博客条目”是包含博客标题,内容,以及发布时间等信息的卡片,视觉上呈现圆角矩形的白色不透明卡片效果。

博客条目控件是一个基于ContentView控件

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:service="clr-namespace:Lession1.Models;assembly=Lession1"
x:Class="Lession1.Views.TextBlogView">
<Grid>
<VerticalStackLayout>
<Label Text="{Binding Title}" FontAttributes="Bold">
</Label>
<Label Text="{Binding Content}"
LineBreakMode="WordWrap"></Label>
</VerticalStackLayout>
</Grid>
</ContentView>

在页面的资源中,添加如下两个ControlTemplate模板,分别用于手机设备和桌面设备。

  1. BlogCardViewPhone用于博客条目在手机设备中的呈现,条目菜单侧滑栏方式展开,我们配置SwipeView控件,作为卡片,用一个Frame框架包裹其内容。设置卡片的阴影,圆角,以及内边距。

代码如下

<ControlTemplate x:Key="BlogCardViewPhone">
<Grid>
<SwipeView>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem Text="编辑"
IconImageSource="delete.png"
BackgroundColor="LightGray" />
<SwipeItem Text="分享"
IconImageSource="delete.png"
BackgroundColor="LightGray" />
<SwipeItem Text="删除"
IconImageSource="delete.png"
BackgroundColor="Red" /> </SwipeItems>
</SwipeView.LeftItems>
<Frame HasShadow="True"
Margin="0,10,0,10"
CornerRadius="5"
Padding="8">
<VerticalStackLayout>
<ContentPresenter />
<Label Text="{TemplateBinding BindingContext.PostTime}"
FontFamily="FontAwesome"></Label>
<Button Text="编辑/发布"
Command="{TemplateBinding BindingContext.SwitchState}" />
</VerticalStackLayout>
</Frame>
</SwipeView>
</Grid>
</ControlTemplate>
  1. BlogCardViewDesktop用于博客条目在桌面设备中的呈现,条目菜单右键菜单方式展开,我们配置FlyoutBase.ContextFlyout属性,作为卡片,用一个Frame框架包裹其内容。设置卡片的阴影,圆角,以及内边距。

代码如下:

<ControlTemplate x:Key="BlogCardViewDesktop">
<Frame HasShadow="True"
Margin="0,10,0,10"
CornerRadius="5"
Padding="8">
<FlyoutBase.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem Text="编辑" />
<MenuFlyoutItem Text="分享" />
<MenuFlyoutItem Text="删除" />
</MenuFlyout>
</FlyoutBase.ContextFlyout> <VerticalStackLayout>
<ContentPresenter />
<Label Text="{TemplateBinding BindingContext.PostTime}"
FontFamily="FontAwesome"></Label>
<Button Text="编辑/发布"
Command="{TemplateBinding BindingContext.SwitchState}" />
</VerticalStackLayout>
</Frame> </ControlTemplate>

.NET MAUI 提供了ContentPresenter作为模板控件中的内容占位符,用于标记模板化自定义控件或模板化页面要显示的内容将在何处显示。

各平台模板中的<ContentPresenter /> 将显示控件的Content属性,也就是将TextBlogView中定义的内容,放到ContentPresenter处。

<view:TextBlogView ControlTemplate="{StaticResource BlogCardViewPhone}">
</view:TextBlogView>

基于平台自定义配置

.NET MAUI 提供了 OnPlatform 标记扩展和 OnIdiom 标记扩展。以便在不同平台上使用不同的控件模板。

通过 OnPlatform 标记扩展可基于每个平台控件属性:

属性 描述
Default 平台的属性的默认值。
Android 属性在 Android 上应用的值。
iOS 属性在 iOS 上应用的值。
MacCatalyst 设置为要在 Mac Catalyst 的值。
Tizen 属性在 Tizen 平台的值。
WinUI 属性在 WinUI 的值。

通过 OnIdiom 标记扩展可基于设备语义上的控件属性

属性 描述
Default 设备语义的属性的默认值。
Phone 属性在手机上应用的值。
Tablet 属性在平板电脑的值。
Desktop 设置为要在桌面平台的值。
TV 属性在电视平台的值。
Watch 属性在可穿戴设备(手表)平台的值。

在本示例中,我们使用OnIdiom标记扩展,分别为手机和桌面设备配置不同的模板。

<view:TextBlogView>
<view:TextBlogView.ControlTemplate>
<OnIdiom Phone="{StaticResource BlogCardViewPhone}"
Desktop="{StaticResource BlogCardViewDesktop}">
</OnIdiom>
</view:TextBlogView.ControlTemplate>
</view:TextBlogView>

需求二:在不同数据类别中使用不同的 UI 外观

数据模板(DataTemplate) 可以在支持的控件上(如:CollectionView)定义数据表示形式

可以使用数据模板选择器(DataTemplateSelector) 来实现更加灵活的模板选择。

DataTemplateSelector 可用于在运行时根据数据绑定属性的值来选择 DataTemplate。 这样可将多个 DataTemplate 应用于同一类型的对象,以自定义特定对象的外观。

相对于ControlTemplate方式,DataTemplateSelector是从Xamarin.Forms 2.1引入的新特性。

定义视图 UI 外观

创建两种视图和模板选择器:

  • TextBlog: 文本博客
  • PhotoBlog: 图片博客

编写文本博客条目展示标题和内容,创建TextBlogView.xaml,定义如下:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:service="clr-namespace:Lession1.Models;assembly=Lession1"
x:Class="Lession1.Views.TextBlogView">
<Grid>
<VerticalStackLayout>
<Label Text="{Binding Title}" FontAttributes="Bold">
</Label>
<Label Text="{Binding Content}"
LineBreakMode="WordWrap"></Label>
</VerticalStackLayout>
</Grid>
</ContentView>

编写图片博客条目展示标题和博客中图片的缩略图,创建PhotoBlogView.xaml,定义如下:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:service="clr-namespace:Lession1.Models;assembly=Lession1"
x:Class="Lession1.Views.PhotoBlogView">
<Grid>
<Label Text="{Binding Title}"
FontAttributes="Bold">
</Label>
<StackLayout BindableLayout.ItemsSource="{Binding Images}"
Orientation="Horizontal">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"
Aspect="AspectFill"
WidthRequest="44"
HeightRequest="44" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</Grid>
</ContentView>

创建数据模板

在页面的资源中,添加各个视图创建数据模板(DataTemplate)类型的资源。

<ContentPage.Resources>
<DataTemplate x:Key="PhotoBlog">
<view:PhotoBlogView>
<view:PhotoBlogView.ControlTemplate>
<OnIdiom Phone="{StaticResource BlogCardViewPhone}"
Tablet="{StaticResource BlogCardViewDesktop}"
Desktop="{StaticResource BlogCardViewDesktop}">
</OnIdiom>
</view:PhotoBlogView.ControlTemplate>
</view:PhotoBlogView>
</DataTemplate>
<DataTemplate x:Key="TextBlog">
<view:TextBlogView>
<view:TextBlogView.ControlTemplate> <OnIdiom Phone="{StaticResource BlogCardViewPhone}"
Tablet="{StaticResource BlogCardViewDesktop}"
Desktop="{StaticResource BlogCardViewDesktop}">
</OnIdiom> </view:TextBlogView.ControlTemplate>
</view:TextBlogView>
</DataTemplate> </ContentPage.Resources>

创建选择器

创建BlogDataTemplateSelector,根据博客的类型,返回不同的数据模板(DataTemplate)对象。

public class BlogDataTemplateSelector : DataTemplateSelector
{
public object ResourcesContainer { get; set; } protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if (item == null)
{
return default;
}
if (item is Blog)
{
var dataTemplateName = (item as Blog).Type;
if (dataTemplateName == null) { return default; }
if (ResourcesContainer == null)
{
return Application.Current.Resources[dataTemplateName] as DataTemplate;
}
return (ResourcesContainer as VisualElement).Resources[dataTemplateName] as DataTemplate; }
return default; }
}

DataTemplate将在页面资源字典中被创建。若没有绑定ResourcesContainer,则在App.xaml中寻找。

同样, 将BlogDataTemplateSelector添加到页面的资源中

  <view:BlogDataTemplateSelector x:Key="BlogDataTemplateSelector"
ResourcesContainer="{x:Reference MainContentPage}" />

注意,此时BlogDataTemplateSelector.ResourcesContainer指向MainContentPage,显式设置MainPage对象的名称:x:Name="MainContentPage"

定义数据

我们定义一个Blog类, 用于表示博客条目,包含标题,内容,发布时间,图片等属性。

public class Blog
{
public Blog()
{
PostTime = DateTime.Now;
State = BlogState.Edit;
} public Guid NoteId { get; set; }
public string Title { get; set; }
public string Type { get; set; }
public BlogState State { get; set; }
public string Content { get; set; }
public List<string> Images { get; set; }
public DateTime PostTime { get; set; }
public bool IsHidden { get; set; }
}

定义博客列表的绑定数据源 ObservableCollection<Blog> Blogs,给数据源初始化一些数据,用于测试。

private async void CreateBlogAction(object obj)
{
var type = obj as string;
if (type == "TextBlog")
{
var blog = new Blog()
{
NoteId = Guid.NewGuid(),
Title = type + " Blog",
Type = type,
Content = type + " Blog Test, There are so many little details that a software developer must take care of before publishing an application. One of the most time-consuming is the task of adding icons to your toolbars, buttons, menus, headers, footers and so on.",
State = BlogState.PreView,
IsHidden = false, };
this.Blogs.Add(blog); }
else if (type == "PhotoBlog")
{
var blog = new Blog()
{
NoteId = Guid.NewGuid(),
Title = type + " Blog",
Type = type,
Content = type + " Blog Test",
Images = new List<string>() { "p1.png", "p2.png", "p3.png", "p4.png" },
State = BlogState.PreView,
IsHidden = false,
};
this.Blogs.Add(blog);
}
}

设置博客列表控件CollectionView绑定的数据源为Blogs,并设置数据模板选择器为BlogDataTemplateSelector

<ContentPage.Content>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<!--标题区域-->
<Label Text="My Blog"
TextColor="SlateGray"
FontSize="Header"
FontAttributes="Bold"></Label>
<CollectionView x:Name="MainCollectionView"
Grid.Row="1"
ItemsSource="{Binding Blogs}"
ItemTemplate="{StaticResource BlogDataTemplateSelector}" /> </Grid>
</ContentPage.Content>

则列表中的每个博客条目将根据博客类型,使用不同的数据模板进行渲染。

效果如下:

需求三:在不同数据状态中使用不同的 UI 外观

此功能没有一个固定的解决方案,可以根据实际情况,选择合适的方式实现。

比如在本项目中,博客存在编辑和发布两个状态

public enum BlogState
{
Edit,
PreView
}

使用绑定模型更改控件的外观

最简单的方式是用IsVisible来控制控件中元素的显示和隐藏。

在本示例中,TextBlogView需要对编辑中的状态和预览中的状态进行区分。

EnumToBoolConverter是枚举到bool值的转换器,它返回当前绑定对象的State属性与指定的BlogState枚举项是否一致,详情请查看 .NET MAUI 社区工具包

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:service="clr-namespace:Lession1.Models;assembly=Lession1"
x:Class="Lession1.Views.TextBlogView">
<Grid>
<VerticalStackLayout IsVisible="{Binding State, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static service:BlogState.PreView}}">
<Label Text="{Binding Title}" FontAttributes="Bold">
</Label>
<Label Text="{Binding Content}"
LineBreakMode="WordWrap"></Label>
</VerticalStackLayout> <VerticalStackLayout IsVisible="{Binding State, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static service:BlogState.Edit}}">
<Label Text="编辑"
FontAttributes="Bold">
</Label>
<Entry Text="{Binding Title}"
Placeholder="标题"></Entry>
<Editor Text="{Binding Content}"
AutoSize="TextChanges"
Placeholder="内容"></Editor>
</VerticalStackLayout>
</Grid> </ContentView>

编辑状态:

发布状态:

使用视觉状态更改控件的外观

还可以使用定义自定义视觉状态对界面进行控制。

在本示例中,使用VisualStateManager定义了两个视觉状态,分别对应Label的编辑状态和发布状态,当State属性的值发生变化时,会触发对应的视觉状态。

<Label Grid.Row="1">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState Name="BlogPreView">
<VisualState.StateTriggers>
<StateTrigger
IsActive="{Binding State, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static service:BlogState.PreView}}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Property="Text"
Value="当前为发布模式" /> </VisualState.Setters>
</VisualState>
<VisualState Name="BlogEdit">
<VisualState.StateTriggers>
<StateTrigger IsActive="{Binding State, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static service:BlogState.Edit}}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Property="Text"
Value="当前为编辑模式" /> </VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups> </Label>

编辑状态:

发布状态:

项目地址

Github:maui-learning

关注我,学习更多.NET MAUI开发知识!

[MAUI程序设计]界面多态与实现的更多相关文章

  1. 【我要学python】面对对象编程之继承和多态

    class animal(object): def run(): print('animal is running...') class dog(animal): def run(self): pri ...

  2. java学习笔记(详细)

    java平台 1.J2SE java开发平台标准版 2.J2EE java开发平台企业版 java程序需要在虚拟机上才可以运行,换言之只要有虚拟机的系统都可以运行java程序.不同系统上要安装对应的虚 ...

  3. DeltaFish 校园物资共享平台 第三次小组会议

    一.想法 娄雨禛: 网页底层开发转移到后端,快速建站,效率高. 可以依照模板进行仿制. 可以考虑只进行页面设计. 但是出现问题不会调试. 所以自己写源码,做出一个大致的样子. 二.上周进度汇报 齐天杨 ...

  4. Golang核心编程

    源码地址: https://github.com/mikeygithub/GoCode 第1章 1Golang 的学习方向 Go 语言,我们可以简单的写成 Golang 1.2Golang 的应用领域 ...

  5. JAVA(3)

    接口注意事项: 1.接口不能被实例化 2.接口中所有的方法都不能有主体  (不能有{ }) 3.一个类可以实现多个接口 4.接口中可以有变量<但变量不能用private和protected修饰& ...

  6. OpenGL基础图形编程

    一.OpenGL与3D图形世界1.1.OpenGL使人们进入三维图形世界 我们生活在一个充满三维物体的三维世界中,为了使计算机能精确地再现这些物体,我们必须能在三维空间描绘这些物体.我们又生活在一个充 ...

  7. php面向对象之抽像类、接口、final、类常量

    一.抽像类(abstract)        在我们实际开发过程中,有些类并不需要被实例化,如前面学习到的一些父类,主要是让子类来继承,这样可以提高代码复用性语法结构:  代码如下 复制代码   ab ...

  8. C#基本线程同步

    0 概述 所谓同步,就是给多个线程规定一个执行的顺序(或称为时序),要求某个线程先执行完一段代码后,另一个线程才能开始执行. 第一种情况:多个线程访问同一个变量: 1. 一个线程写,其它线程读:这种情 ...

  9. BIOS

    转自BIOS BIOS(Basic Input/Output System的缩写.中文:基本输入输出系统),在IBM PC兼容机上,是一种业界标准的固件接口.BIOS这个字眼是在1975第一次由CP/ ...

  10. ASP.NET 设计模式(转)

    Professional ASP.NET Design Patterns 为什么学习设计模式? 运用到ASP.NET应用程序中的设计模式.原则和最佳实践.设计模式和原则支持松散耦合.高内聚的代码,而这 ...

随机推荐

  1. 解决 SMTP Error: data not accepted php邮件发送失败的问题

    php 发送邮件 出现  SMTP Error: data not accepted   1.正常情况下 都是正常的 但是偶尔 发送失败了 163.com 邮箱发送不了了. 所以去查了下问题所在  在 ...

  2. 说来惭愧,关于Session的某块知识的学习,感觉涨了知识

    Session的查漏补缺 今天在写界面的时候,想要利用servlet和jsp页面实现界面的跳转,之前实现这些内容的时候,我是没有用到session来实现这个功能的. 直到今天,想要将第一个界面的数据隔 ...

  3. webgl 系列 —— 渐变三角形

    其他章节请看: webgl 系列 渐变三角形 本文通过一个渐变三角形的示例逐步分析:varying变量.合并缓冲区.图形装配.光栅化.varying 内插 绘制三个点v1 需求:绘制三个相同颜色的点, ...

  4. 4.0 SDK Workshop 纪实:一起体验多人、多屏幕共享新功能

    在本月初,声网发布了 RTC Native SDK 4.0 版本.该版本提供了更高的开发灵活度,可明显提升实时场景开发效率,并让第三方插件开发更容易.上周六(8月20日),我们组织了一场小型的线下 W ...

  5. MyBatisPlus 逻辑删除演示

    一.数据库 数据库中添加逻辑删除字段:

  6. 【牛客小白月赛70】A-F题解【小d和超级泡泡堂】【小d和孤独的区间】【小d的博弈】【小d和送外卖】

    比赛传送门:https://ac.nowcoder.com/acm/contest/53366 难度适中. 作者:Eriktse 简介:19岁,211计算机在读,现役ACM银牌选手力争以通俗易懂的方式 ...

  7. windows 10 家庭版安装Docker和portainer汉化版

    目录 前景提要 存在问题 一.Docker的windows版本安装 1.官网下载: https://www.docker.com/(没有本文问题的直接下载就好.) 2.本文是找到其他版本的Docker ...

  8. 升级:In-Place Upgrade升级MySQL5.6.26

    升级需谨慎,事前先备份 MySQL升级的实质是对数据字典的升级,数据字典有:sys.mysql.information_schema.performance_schema . MySQL升级的两种方式 ...

  9. Java:一篇学好设计模式

    什么是设计模式 简单理解,设计模式是前人多年写代码踩坑总结出来的优秀代码攻略,目的是减少大量无用代码,让项目更好维护 七大设计原则 接下来要讲的23种设计模式,但遵循下面的七大原则: 单一职责原则 2 ...

  10. C++11强制类型转换

    C++ 强制类型转换有四种关键字:static_cast.const_cast.reinterpret_cast和dynamic_cast.它们用于不同的情况和目的,比C语言的强制类型转换更清晰和安全 ...