五、使用 Xamarin 开发 UI

Material Design 是 Android 应用最突出的 UI 模式,苹果的人机界面指南,以及 UWP 的流体 UI 语言,这使得用户体验设计师和开发人员难以决定统一的应用设计。 要考虑的因素包括但不限于,用户对目标平台的期望,以及与品牌相关的产品所有者的要求,无论平台是什么。

在本章中,我们将演示如何设置应用布局,同时利用一些重要的决策因素来实现一致的用户体验设计。 然后,我们将创建简单的应用页面,并使用标准导航服务将它们连接起来,以创建导航层次结构。 我们还将后退一步,看看用于创建应用页面层次结构的 Xamarin Shell 实现。 我们还将浏览 Xamarin。 表单查看元素,并了解如何使用 MVVM 将元素连接到应用数据,而不需要将它们与业务逻辑强耦合。

下面的主题将带领你创建我们的示例应用的框架:

  • 应用的布局
  • 实现导航结构
  • 实现壳导航
  • 使用 Xamarin 的。 表单和本地控件
  • 创建数据驱动的观点
  • 集合视图

在本章结束时,您将能够创建有吸引力的数据驱动的 Xamarin。 表单页面使用开箱即用的布局和视图,并在它们之间设置各种导航层次结构。

技术要求

你可以在本书的 GitHub 库中找到本章将要使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter05

应用布局

在本节中,我们将看看某些 UI 模式,这些模式允许开发人员和 UX 设计人员在用户期望和产品需求之间创建一个折中方案。 通过这样做,可以在所有平台上实现一致的 UX。

对于设计人员和开发人员来说,应用生命周期中最激动人心的阶段可能是设计阶段。 在这个阶段,有许多因素需要仔细考虑,从而避免任何草率的决定。 简单地说,应用的设计应该满足以下要求:

  • 消费者的预期
  • 平台规则
  • 开发成本

让我们开始吧!

消费者期望

应用的特性集应该真正与客户的期望相关联。 布局选项和导航层次结构应该服务于应用的目的,同时牢记用户交互需求。 根据需求,应用可以设计为单页面应用,也可以设计为具有复杂导航页面层次结构的应用; 内容可以是纯文本,也可以使用富媒体元素; 上下文操作可以提供对用户操作的访问,或者可以将交互放置在多个应用页面上。

一般来说,在视图级别,应用视图包含三种不同类型的元素:内容、导航和操作。 创造这些元素的最佳混合是开发者和设计师的责任。 在大多数现代应用中,内容元素具有多种功能,内容元素成为用户交互点和导航元素。

例如,让我们假设,作为我们的设计需求,我们有一个项目列表,其中包含一个图像、一个简单的标题和一个描述(即一个简单的两列两行模板)。 此外,我们需要实现与这些内容项相关的操作。 在这种情况下,元素的设计、流和行为实际上取决于这些行为是什么。 为了演示内容元素上的不同交互,让我们使用以下用户存储 y:

“作为一个用户,我希望看到一个项目列表,并与它们交互,这样我就可以对这些项目执行某些操作。”

列表视图和恭维页面的交互模型可能有很大不同。 让我们仔细看看几个将内容项用作交互元素的模型:

  • 列表/详细信息视图:这个列表可以是用于详细信息导航的公共项目列表视图,其中项目详细信息页面展示了项目可用的操作。 在本例中,我们假设用户在对一个项目执行必要的操作之前需要查看项目的详细信息(例如,如果仅使用清单很难区分这些项目):

Figure 5.1 – Content elements as navigation items

图 5.1 -内容元素作为导航项

这个列表视图中的内容项表现为导航项,在详细信息屏幕上,用户可以执行与这些特定项相关的操作。 但是,对于一个简单的操作,用户将需要更改视图并丢失列表的上下文。

  • Item Context Actions:如果一个动作可以在列表视图上执行,我们不需要将用户带到二级视图,可以让他们直接与列表交互来执行这些动作:

Figure 5.2 – Content elements as context  items

图 5.2 -内容元素作为上下文操作项

在此实现中,列表视图充当单个交互上下文,并直接在项上执行操作。 换句话说,内容元素被用作操作元素,而不是用于导航。

  • List Context Actions: Finally, if there are actions available for execution on multiple content elements, the content items themselves could be used with additional styling (for example, an overlay of a checkmark on the image element). This implementation would replace the possible use of checkboxes or radio buttons for the economical use of design space. This would further improve the user experience since we would, again, be allowing the user to interact with the content itself rather than the user input elements.

    重要提示

    此外,为了减少不必要的控制元素和修饰,iOS 和 Windows 平台在创建内容元素时强调书法的使用。 当使用字体变化时,可以调整某些内容元素的视觉优先级以提供正确的信息。 例如,在前面的示例中,元素的标题是使用较小的字体创建的,因此强调了描述。

平台要求

当处理跨平台移动应用时,开发人员需要创建满足多种设计界面的应用,以及针对多种操作系统和习惯用法的指导方针。 平台命令指的是开发人员和设计人员需要找到一个折衷的平台指南,以创建跨平台的统一 UI 体验。

重要提示

Xamarin 中的习语是如何定义形态因子的。 表单应用。 它可以用于创建各种手机形态因素的特定视图,以及平板电脑,桌面和电视,甚至为可穿戴设备的设计表面,如 Tizen 手表。

在处理不同的习惯用法时,应该考虑目标设备功能、设计界面和输入方法。

为了充分利用桌面和移动设备上的 web 应用空间,开发人员经常使用响应式设计技术,也可以应用到 Xamarin 应用:

  • 流体布局:在流体布局中,项目被堆叠在水平列表中,根据可用的水平空间,项目可以根据需要占用尽可能多的行来列出它们:

Figure 5.3 – Fluid Layout for Idioms

图 5.3 -习语的流动布局

  • 方向改变:在屏幕较宽的设备上,在水平列表中列出的项目可以垂直堆叠在屏幕较小的设备上,屏幕的高度相对于宽度更大。
  • 重组:一般元素布局可根据可用空间完全重组。 例如,一个视图可以在横向模式下使用三个片段,而在纵向模式下,两个片段可以合并为 into:

Figure 5.4 – Restructuring UI Elements for Idioms

图 5.4 -为习语重组 UI 元素

  • 调整:富媒体内容元素,以及文本内容,可以调整大小,以充分利用可用的设计空间:

Figure 5.5 – Resizing UI Elements for Idioms

图 5.5 -为习语调整 UI 元素的大小

此外,正如我们前面提到的,设备功能在应用如何响应用户输入方面扮演着重要的角色。 例如,硬件返回按钮就是这种设计考虑的一个很好的例子。 如果我们正在设计针对 Android、iOS 和 UWP 平台的移动应用,我们需要记住,只有 Android 提供硬件或软件返回按钮。 这种能力,或者在其他平台上缺乏这种能力,使得在第二层应用视图中包含后退导航元素至关重要。 同样,如果我们设计一个应用在移动设备上使用(比方说,在 iOS 和 Android),但与此同时,应用应该运行在电视与 Tizen 或 Android 操作系统,输入方法和用户通过导航屏幕如何成为一个至关重要的设计因素。

开发成本

最后,技术可行性是客观分析应用设计需求的另一个重要方面。 在某些情况下,创建一个自定义控件来模拟 web 应用的开发成本超过了添加到相同应用的本地副本的业务或平台价值。

每个移动平台的 Xamarin 和 Xamarin。 表单目标提供不同的用户体验和一组不同的控件。 Xamarin 的。 表单在这组本机控件上创建一个抽象,以便在特定平台上使用本机视图呈现相同的抽象。 在这种情况下,尝试引入新的设计元素或自定义控件,这些元素在外观上有本质上的不同,但它们的行为却彼此相似,这可能会带来代价高昂的后果。

例如,如果应用的 web 对等体为某个首选项使用复选框,那么在本例中使用的移动视图将是一个切换开关。 坚持使用复选框将意味着额外的开发时间,以及目标平台上不理想的用户体验。 类似地,使用复选框进行(多重)选择而不是突出显示所选内容可能会导致特定移动平台和平台用户的 UX 退化。

如您所见,在设计应用布局时需要考虑几个因素。 当你在设计游戏时,你应该牢记这些用户体验因素。 换句话说,移动应用 UX 设计并不是简单地缩减应用在 web 或桌面平台上的功能。 到目前为止,我们已经了解了用户对应用的期望、平台要求和开发成本。 找到这些因素之间的最佳折衷可以为所有涉众提供理想的应用。 然而,应用设计决策并没有到此为止。 在下一节中,我们将演示移动应用中的不同导航策略。

实现导航结构

在开始开发之前,要做的一个主要决定是决定应用的导航层次结构。 一般来说,这个决定应该在用户体验设计阶段就考虑到。

根据应用的需求和目标用户,可以采用不同的方式设计导航层次结构。 其中一些导航策略可以总结如下:

  • 单页视图
  • 简单的导航
  • 多页视图
  • 主/详细视图

让我们开始吧!

单页视图

在单页视图中,顾名思义,单页视图用于内容和可能的用户交互,而操作要么在此视图上执行,要么在操作表单上执行。 根据设计需求,可以使用单页实现来实现该视图,例如ContentPageTemplatedPage:

Figure 5.6 – Xamarin.Forms Page Types

图 5.6 - Xamarin 表单页面类型

ContentPage是最常用的页面定义之一。 使用这种页面结构,开发人员可以自由地在内容页面的内容定义中包含任何布局和视图元素。

现在,让我们用下面的 g 用户故事来扩展我们的样例应用的需求:

“作为一个用户,我希望有一个最近添加到商店的项目列表,这样我可以很容易地获知新产品。”

为了实现这个需求,我们将在应用中修改MainPage,并使用以下步骤将列表视图添加到该页面:

  1. 打开ShopAcross解决方案,创建一个名为HomeView的新内容页面(使用Forms ContentPage XAML模板):
  2. 打开App.xaml.cs文件,通过将MainPage设置为HomeView:

    public App() { InitializeComponent(); MainPage = new HomeView(); }

    的新实例来修改构造器 3. Before adding the list view and the related content items, let's designate the content area and the list context-related action items toolbar:

    ``` <?xml version="1.0" encoding="UTF-8"?> <ContentPage Title="Home"

     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
     x:Class="ShopAcross.Mobile.Client.HomeView">
     <ContentPage.ToolbarItems>
         <!-- Removed for brevity -->
     </ContentPage.ToolbarItems>
    

    ```

    这里,正在使用的内容容器是ContentToolbar项。 它们将分别用于创建项目的列表视图和工具栏操作按钮。

  3. 现在,让我们用一个空的模板添加列表视图。 使用ContentPage.Content插入我们的ListView:

    <ListView ItemsSource="{Binding RecentProducts}" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <!-- TODO --> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>

  4. 现在,为列表视图项插入ViewCell定义:

  5. 在这个阶段,应用将无法工作,因为缺少绑定和绑定上下文。 为了解决这个问题,让我们在ShopAcross.Mobile.Core项目中创建一个基本绑定数据对象(即BaseBindableObject):

    public class BaseBindableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SendPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }

  6. 现在,将下面的代码复制粘贴到核心项目中的视图模型类(即ProductViewModel)中:

  7. 现在可以将HomeView的视图模型添加到HomeViewModel类:

    public class HomeViewModel: BaseBindableObject { public HomeViewModel() { RecentProducts = new ObservableCollection< ProductViewModel>(GetRecentProducts()); } public ObservableCollection<ProductViewModel> RecentProducts { get; } = new ObservableCollection<ProductViewModel>(); public IEnumerable<ProductViewModel> GetRecentProducts() { yield return new ProductViewModel { Title = "First Item", Description = "First Item short description", Image = "https://picsum.photos/800?image=0" }; //... Removed for brevity } }

  8. 最后,创建一个新的实例HomeViewModel,并将其分配给HomeView:

    public HomeView() { InitializeComponent(); BindingContext = new HomeViewModel(); }

    的类BindingContext

现在,运行应用将产生一个使用已定义项模板显示列表视图的内容页面。

ContentPageTemplatedPage的衍生,而TemplatedPage是另一种可用于 Xamarin 的页面类型。 表单应用。 TemplatedPage允许开发人员为TemplatePage(即ContentPage)创建基本样式,以便将某些全局级别的自定义应用于这些页面。

例如,在中,为了使用页脚扩展之前的实现,我们可以遵循以下步骤:

  1. First, define a style for this page (in App.xaml):

    <Application.Resources> <ResourceDictionary> <ControlTemplate x:Key="PageTemplate"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Row="0" /> <BoxView Grid.Row="1" Color="Navy" /> <Label Grid.Row="1" Margin="10,0,0,0" Text="(c) Hands-On Cross Platform 2020" TextColor="White" VerticalOptions="Center" /> </Grid> </ControlTemplate> </ResourceDictionary> </Application.Resources>

    在这个模板中,注意ContentPresenter被用作为ContentPage的占位符。

  2. 现在,我们可以在HomeView页面中使用这个模板,代码如下:

    ``` <ContentPage

    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    ControlTemplate="{StaticResource PageTemplate}" 
    x:Class="ShopAcross.Mobile.Client.HomeView">
    

    ```

这将导致页脚 app 运行在我们的HomeView上:

Figure 5.7 – HomeView

Figure 5.7 – HomeView

到目前为止,我们只将应用的主页创建为内容页面。 然而,我们已经实现了基本的结构来继续引入额外的视图和视图模型。 现在,让我们学习如何导航到视图和从视图导航。

导航简单

在 Xamarin 生态系统中,每个平台都有其固有的导航堆栈,应用围绕这些堆栈构建。 开发人员负责维护这些堆栈,以便为用户创建所需的 UX 流。

在页面之间导航,Xamarin。 Forms 公开了一个Navigation服务,它可以与NavigationPage抽象页面实现一起使用。 换句话说,NavigationPage不能被归类为为用户提供内容的页面类型; 然而,它是 Xamarin 中用于维护导航堆栈和导航栏的关键组件。 表单应用。

在上一节中,我们创建了我们的HomeView,它显示了最近添加的产品的列表。 为了演示一些简单的导航方法,让我们考虑下面的用户故事:

“作为一个用户,我希望能够从主页上打开最近添加的产品的详细信息,这样我就可以获得有关所选产品的额外信息。”

为了显示产品的详细信息,我们需要介绍我们的产品详细信息视图。 遵循以下步骤:

  1. 让我们首先在名为ProductDetailsViewShopAccross.Mobile.Client项目中创建另一个基于 xaml 的ContentPage
  2. 现在,在content部分添加以下内容:

    <StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <Label Text="{Binding Title, Mode=OneTime}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand" /> <Label Text="{Binding Description}" FontSize="Large" /> </StackLayout>

  3. 现在我们已经准备好了基本的ProductDetailsView,我们可以从HomeView导航到ProductDetailsView。 要创建导航元素,让我们引入一个导航页面作为主页,根元素设置为HomeView:

    public App() { InitializeComponent(); MainPage = new NavigationPage(new HomeView()); }

  4. 现在,将以下方法添加到HomeView.xaml.cs:

  5. 最后,将事件处理程序添加到HomeView.xaml到,当点击相应的项目时,导航到产品的详细信息:

    <ListView ItemTapped="Handle_ItemTapped" ItemsSource="{Binding RecentProducts}" >

在本例中,我们从HomeView导航到ProductDetailsView。 在这个导航之后,在 iOS 上,你会注意到第一页的标题作为后退按钮文本插入到导航栏中。 此外,由于Title属性用于HomeView,文本也会显示在导航栏中。

Xamarin 的之前。 窗体 3.2 中,自定义导航栏显示内容和方式的唯一方法是使用某种形式的本地自定义(例如,NavigationPage的自定义渲染器)。 尽管如此,您现在可以使用导航页的TitleView依赖属性向导航栏添加自定义元素。

HomeView页面为例,我们可以在ContentPage中添加以下 XAML 部分:

 <NavigationPage.TitleView>
     <StackLayout Orientation="Horizontal" VerticalOptions="Center" Spacing="10">
         <Image Source="xamarin.png"/>
         <Label 
             Text="Custom Title View" 
             FontSize="16" 
             TextColor="Black" 
             VerticalTextAlignment="Center" />
     </StackLayout>
 </NavigationPage.TitleView>

为了使xamarin.png文件对应用可用,你需要将其添加到 iOS 上的Resources文件夹和 Android 上的resources/drawable文件夹。

结果视图将具有已定义的StackLayout,而不是先前显示的Home标题:

Figure 5.8 – Custom Navigation Page Title

图 5.8 -自定义导航页标题

尽管这种定制会稍微影响导航的原生行为,但在某些场景中,这种妥协是合理的; 例如,在标题视图上有一个搜索框是 Android 应用的一个突出模式。

在本节中,我们向导航层次结构中又添加了一层,并实现了这些层之间的导航功能。 到目前为止,我们只使用了ContentPage作为视图的基础。 但是,有些模板可以承载多个页面。

多页浏览

CarouselPageTabbedPage是两种 Xamarin。 从MultiPage抽象派生出的页面实现。 这些页面都可以承载多个在它们之间具有唯一导航的页面。

为了说明MultiPage实现的用法,we 可以使用以下用户故事:

“作为一名用户,我希望拥有一系列最近产品的详细信息,这样我就可以轻松地通过滑动手势浏览它们,这样我就可以轻松地访问各种产品和详细信息,而不是从列表视图中选择一个。”

在这个实现中,我们将使用之前创建的HomeViewHomeViewModel。 废话少说,让我们开始实现:

  1. 我们将通过创建一个CarouselPage来开始实现。 我们可以使用基于 xaml 的Content Page模板作为起点。 让我们将这一页命名为RecentProductsView
  2. 现在已经创建了页面,您可以使用下面的 XAML 内容来定义来自最近产品列表的不同产品,作为该页面的多个子页面的绑定上下文:
  3. 我们还需要将类声明改为派生自CarouselPage而不是ContentPage:

    public partial class RecentProductsView : CarouselPage { }

  4. 然后,我们需要将一个新的实例HomeViewModel分配给类构造器中的BindingContext:

    public RecentProductsView() { InitializeComponent(); BindingContext = new HomeViewModel(); }

  5. 现在我们已经创建了RecentProductsView,可以将导航方法添加到HomeView:

    private void RecentItems_Clicked(object sender, EventArgs e) { var recentProducts = new RecentProductsView(); Navigation.PushAsync(recentProducts); }

  6. 最后,我们可以将工具栏菜单按钮添加到HomeView:

    <ContentPage.ToolbarItems> <ToolbarItem Text="Recent" Clicked="RecentItems_Clicked" /> </ContentPage.ToolbarItems>

现在我们有了最近的条目页面集,让我们考虑以下的用户故事:

“作为一个注册用户,我希望有设置收藏类别的选项,这样当我浏览最近添加的产品时,只显示我选择的类别中的产品。”

为了让用户选择某些类别作为他们的收藏,我们需要为用户准备一个设置屏幕。 在这个设置屏幕上,我们可以显示任何个性化选项。 如果我们要实现一个单页应用,我们可以使用一个展板来显示这个页面,但是为了演示对等导航和多页模板,让我们使用一个选项卡页面来向我们的应用引入其他页面。

按照以下步骤创建SettingsView,并将其作为 peer 添加到HomeView:

  1. 在创建TabbedPage之前,我们应该介绍SettingsView。 使用基于 xaml 的ContentPage模板创建一个名为SettingsView的页面。
  2. 创建页面后,添加以下内容:

    <ContentPage.Content> <StackLayout Orientation="Vertical" Padding="10"> <Label Text="Selected Categories" FontSize="Title" /> <ListView> </ListView> </StackLayout> </ContentPage.Content>

  3. 对于模板,我们将使用简单的Grid和两列:

    <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="75" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Switch Grid.Column="0" /> <Label Text="{Binding .}" Grid.Column="1" VerticalTextAlignment="Center" FontSize="Subtitle"/> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate>

  4. 对于内容,我们可以使用一组任意类别:

    <ListView.ItemsSource> <x:Array Type="{x:Type x:String}"> <x:String>computers</x:String> <x:String>white furniture</x:String> <x:String>gadgets</x:String> <x:String>car electronics</x:String> <x:String>iot</x:String> </x:Array> </ListView.ItemsSource>

  5. 现在,使用 FormsTabbedPageXAML 模板添加RootTabbedView

  6. 现在,将以下内容添加到我们的根页面:

    ``` <?xml version="1.0" encoding="utf-8"?> <TabbedPage

    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:ShopAcross.Mobile.Client"
    xmlns:core="clr-namespace:ShopAcross.Mobile.Core;assembly=ShopAcross.Mobile.Core"
    x:Class="ShopAcross.Mobile.Client.RootTabbedView">
    <TabbedPage.BindingContext>
        <core:HomeViewModel />
    </TabbedPage.BindingContext>
    <!--Pages can be added as references or inline-->
     <TabbedPage.Children>
         <NavigationPage Title="Home" Icon="xamarin.png">
            <x:Arguments>
                <local:HomeView BindingContext="{Binding .}" />
            </x:Arguments>
          </NavigationPage>
         <NavigationPage Title="Settings" Icon="xamarin.png">
            <x:Arguments>
                <local:SettingsView/>
            </x:Arguments>
          </NavigationPage>
     </TabbedPage.Children>
    

    ```

  7. 最后,将MainPage任务更改为App.xaml.cs中新创建的任务。

实际上,结果页面将在各自的布局和导航堆栈中托管两个子页面。 如果您现在单击最近的工具栏操作按钮,您将看到CarouselPageTabbedPage是如何在同一个应用中使用的:

Figure 5.9 – Multi-Page Views

图 5.9 -多页面视图

重要的信息

需要注意的是,在 iOS 上,子元素的标题和图标属性用于创建选项卡导航项。 为使图标正确显示,正常分辨率为30x30,高分辨率为60x60,iPhone 6 分辨率为90x90。 在 Android 上,标题用于创建选项卡项。

在中,TabbedPage是 iOS 应用中位于导航层次结构顶部的基本控件之一。 可以通过为每个选项卡分别创建一个导航堆栈来扩展TabbedPage的实现。 这样,在选项卡之间的导航将为每个选项卡独立保留导航堆栈,并支持前后导航。

在本节中,我们介绍了多页面视图,您可以使用它来创建所谓的同行导航结构。 特别地,TabbedPage是 iOS 应用中最突出的导航层设置。 另一个多页面设置被用作导航堆栈的根,在这里您需要多个页面可以同时被用户访问,这是主/详细设置。 在下一节中,我们将为应用设置导航弹出菜单。

Master/detail 视图

在 Android 和 UWP 上,突出的导航模式和相关的页面类型是 master/detail,并使用一个所谓的导航抽屉。 在这个模式中,通过ContentPage(称为母版页)来维护跨导航结构的跳跃(跨越层次结构的不同层)或跨导航(在同一层内)。 用户与母版页的交互(显示在导航抽屉中)被传播到Detail视图。 在此设置中,详细信息视图的导航堆栈是存在的,而主视图是静态的。

要复制前面示例中的选项卡结构,我们可以创建MasterDetailPage,它将托管我们的菜单项列表。 MasterDetailPage将由Master内容页面和Detail页面组成,其中托管NavigationPage以创建导航堆栈。 按照以下步骤完成设置:

  1. 我们将从使用 Forms MasterDetailPage XAML 模板创建我们的MasterDetailPage开始。 以RootView作为名称。 这个模板应该创建三个单独的页面和我作为多页面设置的一部分。 在本练习中,我们将不使用RootViewMasterRootViewDetail页,也不使用RootViewMenuItem类,因此您可以安全地删除它们。
  2. 现在,打开RootView.xaml文件,添加以下内容设置 up 的主部分:

    <MasterDetailPage.Master> <ContentPage Title="Main" Padding="0,30,0,0" Icon="slideout.png"> <StackLayout> <ListView x:Name="listView" ItemsSource="{Binding .}" SeparatorVisibility="None"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid Padding="5,10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="30"/> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Image Source="{Binding Icon}" /> <Label Grid.Column="1" Text="{Binding Title}" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage> </MasterDetailPage.Master>

  3. Notice that the Master page simply creates a ListView containing the menu item entries.

    重要提示

    还需要注意的是,所谓的汉堡包菜单图标需要添加为Master页面的Icon属性(见slideout.png); 否则,将使用母版页的标题来代替菜单图标。

  4. 在 iOS 中添加slideout.pngResources文件夹,在 Android 中添加资源/绘制文件夹。

  5. 接下来,我们将创建一个简单的数据结构来存储菜单元数据。 您可以将以下类添加到一个新文件或RootView.xaml.cs文件:

    public class NavigationItem { public int Id { get; set; } public string Title { get; set; } public string Icon { get; set; } }

  6. 现在,我们应该创建用于导航的列表。 将下面的初始化代码添加到RootView.xaml.cs的构造函数中:

    public RootView() { InitializeComponent(); var list = new List<NavigationItem>(); list.Add(new NavigationItem { Id = 0, Title = "Home", Icon = "xamarin.png" }); list.Add(new NavigationItem { Id = 1, Title = "Settings", Icon = "xamarin.png" }); BindingContext = list; }

  7. The Detail page assignment will now look as follows:

    <MasterDetailPage.Detail> <NavigationPage Title="List"> <x:Arguments> <local:HomeView /> </x:Arguments> </NavigationPage> </MasterDetailPage.Detail>

    在这个点,运行应用将创建导航抽屉和包含的Master页:

    Figure 5.10 – Master/Detail View

    图 5.10 -主/详细视图

  8. 为了完成实现,我们还需要处理主列表中的ItemTapped事件:

现在,实现已经完成。 每次使用菜单项时,导航类别将被更改,并创建一个新的导航堆栈; 但是,在导航类别中,导航堆栈是完整的。 另外,请注意,MasterDetailPageIsPresented属性被设置为false,以便在创建新的详细信息视图时立即关闭母版页。

在本节中,我们从一个单视图应用开始实现,该应用是通过添加多页面视图以及导航抽屉展开的。 在这些设置中,我们广泛地使用了 Xamarin.Forms 的NavigationPageNavigationService实现。 除了这个经典的实现之外,您还可以在应用中使用 Xamarin Shell 来降低设置导航基础设施的复杂性。 在下一节中,我们将快速了解如何使用 Xamarin Shell 创建类似的应用层次结构。

实现 Shell 导航

在本节中,我们将使用 Xamarin Shell 来演示它如何使开发人员的工作更轻松。 我们将使用 Xamarin Shell 实现一个简单的 Master/Detail 视图。

在导航层次结构比三层垂直和对等导航更复杂的应用中,广泛使用导航服务以及重新创建的视图和视图模型可能会导致可维护性和性能问题。 不同层和同级页面之间的导航链接尤其会给开发团队带来严重的头痛。

Xamarin Shell 可以通过在导航基础设施和 Xamarin 之间引入一个层来帮助减轻这种复杂性。 表单页面。 Shell的前提是提供类似于 web 应用的路由处理和模板基础设施,以便能够轻松创建复杂的导航链接和多页面视图。

说明 Xamarin Shell 如何工作的最简单方法是重新创建我们在前一节中创建的应用层次结构。 按照以下步骤转换应用,使其可以使用Shell:

  1. 我们将从添加所谓的AppShell开始。 AppShell将用于注册我们申请的主要路线。 为此,创建一个新的基于 xaml 的内容页面AppShell
  2. 创建AppShell后,将AppShell.xaml的内容更改为以下内容:

    ``` <Shell

    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:ShopAcross.Mobile.Client"
    x:Class="ShopAcross.Mobile.Client.AppShell">
    <FlyoutItem Title="Home" Icon="xamarin.png">
    

    ```

  3. Now, let's change the base class definition in AppShell.xaml.cs:

    public partial class AppShell : Shell { public AppShell() { InitializeComponent(); } }

    现在,我们已经成功地创建了带有两个飞行导航项的AppShell

  4. 接下来,让我们通过将AppShell分配给MainPage:

    public App() { InitializeComponent(); //MainPage = new RootView(); MainPage = new AppShell(); }

    来介绍AppShell到基础设施中。

现在,如果您运行应用,您将看到主/详细设置通过Shell设置; 您不需要设置项目列表和处理不同的事件来实现有状态导航菜单。

另一个重要的注意事项是,在更改SettingsView上的设置(即切换其中一个类别)并导航返回SettingsView后,您将注意到SettingsView保留了的状态。 这可以为您提供有关 Xamarin Shell 上如何管理有状态导航的线索。

让我们的实现更进一步,让我们为导航菜单项引入路由,并在视图中使用这些路由:

  1. First, let's add the following routes to the defined ShellContent items in AppShell.xaml:

    <ShellContent Route="home" ContentTemplate="{DataTemplate local:HomeView}"/> <ShellContent Route="settings" ContentTemplate="{DataTemplate local:SettingsView}"/>

    一旦这些路由配置好了,我们就可以使用 Shell 导航进入某个导航状态。

  2. 现在,添加一个按钮到SettingsView.xaml:

    <Button Text="Back to Home" Clicked="Button_Clicked" />

  3. 然后在SettingsView.xaml.cs中添加以下方法:

    private async void Button_Clicked(object sender, EventArgs e) { await Shell.Current.GoToAsync("//home"); }

现在,如果您运行应用,导航到设置视图,并单击按钮,Shell 应该会将您带回主页。

基于 uri 的导航基础结构还允许在层之间导航,以及相对路径。

在本节中,我们试图演示 Xamarin Shell 的强大功能,至少在实现导航层次结构方面是如此。 Xamarin Shell 提供了其他有用的布局,以及搜索等功能。 我们将在下一节中讨论这些内容。

使用 Xamarin。 表单和本地控件

现在我们对不同的页面类型和导航模式更加熟悉了,我们可以继续为我们的页面创建实际的 UI。 在本节中,我们将演示各种 Xamarin。 表单元素及其用法,以及如何在 Xamarin 中使用本地控件。 视觉形式树。

为 Xamarin 目标平台创建足够灵活的 UX 可能非常复杂,特别是当涉及的涉众不熟悉上述 UX 设计因素时。 然而,Xamarin 的。 表单提供了各种布局和视图,帮助开发人员找到项目需求的最佳解决方案。

重要提示

Xamarin 的。 表单,可视化树由三层组成:页面、布局和视图。 布局用作视图的容器,视图是用于创建页面的用户控件。 这些是用户的主要交互界面。

让我们仔细看看 UI 组件。

布局

布局是容器元素,用于在设计图面上分配用户控件。 为了满足平台要求,布局可以用来对齐、堆叠和定位视图元素。 不同类型的布局如下:

  • StackLayout: This is one of the most overused layout structures in Xamarin.Forms. It is used to stack various view and other layout elements with prescribed requirements. These requirements are defined through various dependency or instance properties, such as alignment options and dimension requests.

    例如,在ProductDetailsView页面上,我们使用StackLayout将条目的Image与其各自的标题和描述结合起来:

    <StackLayout Padding="10" Orientation="Vertical"> <Label Text="{Binding Title}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand"/> <Label Text="{Binding Description}" /> </StackLayout>

    在这个设置中,重要的声明是Orientation,它定义了堆叠应该垂直发生; HorizontalOptions,定义为Image元素,它允许Image根据可用空间水平和垂直展开; 以及StackLayout,可以用来创建“方向-改变-响应”的行为。

  • FlexLayout: This can be used to create fluid and flexible arrangements of view elements that can adapt to the available surface. FlexLayout has many available directives that developers can use to define alignment directions. In order to demonstrate just a few of these, let's assume ProductDetailsView requires an implementation of a horizontal layout, where certain features are listed in a floating stack that can be wrapped into as many rows as required:

    <StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <Label Text="{Binding Title}" FontSize="Large" /> <Image Source="{Binding Image}" HorizontalOptions="FillAndExpand" /> <FlexLayout Direction="Row" Wrap="Wrap"> <Label Text="Feature 1" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Gray" /> <Label Text="Feat. 2" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Lime"/> <!-- Additional Labels --> </FlexLayout> <Label Text="{Binding Description}" /> </StackLayout>

    这将创建一个类似于流体布局响应 UI 模式中描述的设计结构:

Figure 5.11 – Flex Layout

图 5.11 - Flex 布局

  • Grid: If it is not desired for the views in a layout to expand and trigger layout cycles – in other words, if a certain page requires a more top-down layout structure (that is, with the parent element determining the layout) – then Grid would be the most suitable control. Using the Grid layout, controls can be laid out in accordance with column and row definitions, which can be adjusted to respond to control size changes or the overall size of Grid.

    在为我们的页面创建控制模板时,我们使用了Grid来创建一个刚性结构,以便我们可以将页脚的绝对高度值,同时允许内容演示者覆盖屏幕的其余部分:

    <ControlTemplate x:Key="PageTemplate"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Row="0" /> <BoxView Grid.Row="1" Color="Navy" /> <Label Grid.Row="1" Margin="10,0,0,0" Text="(c) Hands-On Cross Platform 2018" TextColor="White" VerticalOptions="Center" /> </Grid> </ControlTemplate>

    注意,我们为标签使用了边距值。 为了避免使用页边距,我们可以创建一个具有固定值的列定义,并根据期望的结果设置该页边距列,以便它也适用于内容演示者:

    <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="25" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="10 /> <ColumnDefinition /> <ColumnDefinition Width="10" /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Column="1" Grid.Row="0" /> <BoxView Grid.Row="1" Grid.ColumnSpan="3" Color="Navy" /> <Label Grid.Row="1" Grid.Column="1" Text="(c) Hands-On Cross Platform 2018" TextColor="White" VerticalOptions="Center" /> </Grid>

    这样设置后,BoxView将扩展到三个列上,而脚注文本和实际内容将被隔离到第二列 column -1 上,column -0 和 column -2 充当页边距。

    Grid也可以用来构造视图的某一段。 例如,如果我们将规格节添加到我们的ProductDetailsView页中,它将类似于:

    <StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <!-- Removed for Brevity --> <Label Text="{Binding Description}" /> <Label Text="Specifications" Font="Bold" /> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="3*" /> <ColumnDefinition Width="5*" /> </Grid.ColumnDefinitions> <Label Text="Specification 1" Grid.Column="0" Grid.Row="0"/> <Label Text="Value for Specification" Grid.Column="1" Grid.Row="0" TextColor="Gray"/> <Label Text="Another Spec." Grid.Column="0" Grid.Row="1" /> <Label Text="Value for Specification that is a little longer" Grid.Column="1" Grid.Row="1" TextColor="Gray"/> <!-- Additional Specs go here --> </Grid> </StackLayout>

    请注意,列被设置为使用屏幕的 3/8 和 5/8,以便最佳地使用可用的空间。 这将创建一个类似如下的视图:

Figure 5.12 – Grid Layout

图 5.12 -网格布局

将最后一个元素添加到屏幕后,您可能会注意到屏幕空间垂直耗尽,因此最终的网格元素可能溢出视图端口,这取决于屏幕大小。

  • ScrollView: To get the screen to scroll so that all the content is visible to the user, we can introduce ScrollView. ScrollView is another prominent layout element and acts as a scrollable container for the contained view elements.

    为了能够滚动屏幕,使所有的规格都可见,我们可以简单地将ProductDetailsView.xaml中的主布局封装到ScrollView中:

    <ContentPage.Content> <ScrollView> <StackLayout Padding="10" Orientation="Vertical" Spacing="10"> <!-- Removed for brevity --> </StackLayout> </ScrollView> </ContentPage.Content>

    当涉及Entry字段时,会用到ScrollView的额外用法。 当用户点击Entry字段时,移动设备上的行为是键盘从屏幕底部向上滑动,产生垂直偏移,减少了设计空间。 在包含Entry的视图中,键盘可能与当前聚焦的Entry字段重叠。 这将产生不理想的用户体验。 为了纠正这种行为,表单内容应该放在ScrollView中,这样键盘的外观就不会将有问题的Entry字段推到屏幕之外。

  • AbsoluteLayoutRelativeLayout:这些是我们在中尚未涵盖的其他布局选项。 这两个布局,一般来说,治疗的观点几乎像一个画布,让物品被放置在彼此之上,使用当前的屏幕(在AbsoluteLayout的情况下)或其他控件(在RelativeLayout的情况下)作为定位参考。

举例来说,如果我们放置一个浮动操作按钮(工厂)对我们的HomeView从材料设计,我们可以很容易地实现,使用绝对布局,按钮在屏幕的右下角(即位置比例)和利润率的增加我们的工厂:

 <AbsoluteLayout>
     <ListView 
         ItemsSource="{Binding Items}" 
         ItemTapped="Handle_ItemTapped" 
         SeparatorVisibility="None" >
         <ListView.ItemTemplate>
             <DataTemplate>
                 <!-- Removed for brevity -->
             </DataTemplate>
         </ListView.ItemTemplate>
     </ListView>
  <Image 
 Source="AddIcon.png" 
 HeightRequest="60" 
 WidthRequest="60"
 AbsoluteLayout.LayoutFlags="PositionProportional"
 AbsoluteLayout.LayoutBounds="1.0,1.0" 
 Margin="10"/>
 </AbsoluteLayout>

这将创建一个 FAB(即,使用的图像代替 FAB)显示在列表视图项上:

Figure 5.13 – Relative Layout

图 5.13 -相对布局

此外,RelativeLayout以类似的方式允许开发人员在元素之间以及视图本身创建比例计算。

Xamarin。 表格视图元素

到目前为止,我们在应用中使用的主要视图元素是LabelImage(同时创建列表和详细信息视图)。 此外,在登录屏幕上,我们使用了EntryButton视图。 这两组控件之间的主要区别在于,虽然LabelImage用于显示(通常)只读内容,但EntryButton是用于用户输入的元素。

如果我们仔细观察Label,我们会发现在我们的设计中,各种属性用于创建文本内容的定制显示,以强调书法/排版(参考 iOS 和 UWP 的平台要求)。 开发人员不仅可以定制文本内容的外观和感觉,还可以使用Span元素创建富文本内容。 span 类似于 WPF 中的Run元素和具有相同名称的 web 元素(即Span)。 在后来的 Xamarin 版本中。 窗体Span可以识别手势,使开发人员能够在单个文本内容块中创建交互区域。 要使用 span,我们可以使用标签的FormattedText属性。

为了进一步定制(或者将品牌应用于)应用,还可以引入自定义字体。 当涉及到包含自定义字体时,每个平台都需要执行不同的步骤。

作为第一步,开发人员需要访问字体的 TFF 文件,并且需要将该文件复制到特定于平台的项目中。 在 iOS 上,文件需要设置为BundleResource,在 Android 上设置为AndroidAsset。 仅在 iOS 上,自定义字体应该声明为字体的一部分,字体由Info.plist文件中的应用条目提供:

Figure 5.14 – Bundle Resources

图 5.14 - Bundle 资源

此时,我们已经使用的自定义字体可以通过FontFamily属性添加到目标标签; 然而,在 Android 和 iOS 中,字体家族的声明是不同的:

 <Label Text="{Binding Description}">
     <Label.FontFamily>
         <OnPlatform x:TypeArguments="x:String">
             <On Platform="iOS" Value="Ubuntu-Light" />
             <On Platform="Android" Value="Ubuntu-Light.ttf#Ubuntu-
             Light" />
             <On Platform="UWP" Value="Assets/Fonts/Ubuntu-
             Light.ttf#Ubuntu-Light" />
         </OnPlatform>
     </Label.FontFamily>
 </Label> 

为了更方便地使用字体或将其应用到应用中的所有标签,可以使用App.xaml文件。 这个将把它添加到应用的资源中:

 <Application.Resources>
     <ResourceDictionary>
         <!-- Removed for brevity -->
 <OnPlatform x:Key="UbuntuBold" x:TypeArguments="x:String">
 <On Platform="iOS">Ubuntu-Bold</On>
 <On Platform="Android">Ubuntu-Bold.ttf#Ubuntu-Bold</On>
 </OnPlatform>
         <OnPlatform x:Key="UbuntuItalic" x:TypeArguments="x:String">
             <On Platform="iOS">Ubuntu-Italic</On>
             <On Platform="Android">Ubuntu-Italic.ttf#Ubuntu-
              Italic</On>
         </OnPlatform>

         <!-- Additional Fonts and Styles -->
     </ResourceDictionary>
 </Application.Resources>

现在,我们可以为特定的目标定义内隐或外显风格:

<Style x:Key="BoldLabelStyle" TargetType="Label">
 <Setter Property="FontFamily" Value="{StaticResource UbuntuBold}" />
</Style>
<!-- Or an implicit style for all labels -->
<!-- 
<Style TargetType="Label">
     <Setter Property="FontFamily" Value="{StaticResource UbuntuRegular}" />
 </Style>
--> 

重要的信息

这可以进一步包含包含字形(例如FontAwesome)的字体,这样我们就可以将标签用作菜单图标。 一个简单的实现是创建一个派生于Label的自定义控件,并设置一个以该自定义控件为目标的全局隐式样式。

Label对应的是EntryEditor,两者都源于InputView抽象。 这些控件可以放在用户表单中,分别处理单行或多行文本输入。 改善用户体验,这两个控件公开Keyboard属性,可用于设置适当的类型的软件键盘用户条目(例如,Chat,Default,Email、【显示】,Telephone,等等)。

其余的用户输入控件是更特定于场景的,如BoxViewSliderMapWebView

值得一提的是,还有三个额外的用户输入控件,即PickerDatePickerTimePicker。 这些选择器表示显示在表单上的数据字段和聚焦数据字段后使用的选择器对话框的组合。

如果定制这些控件不满足 UX 要求,Xamarin。 表单允许开发人员引用和使用本地控件。

本机组件

在某些情况下,开发人员需要求助于使用本地用户控件——特别是当某个控件仅在某个平台上存在时(即没有 Xamarin)。 表单抽象用于特定的 UI 元素)。 在这些类型的情况下,Xamarin 允许用户在 Xamarin 中声明本地视图。 表单 XAML 并设置/绑定这些控件的属性。

要包含本机视图,首先必须声明本机视图的命名空间:

xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
xmlns:formsandroid="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.Platform.Android;targetPlatform=Android"

例如,一旦命名空间被声明,我们就可以将Label替换为ItemView.xaml,并直接使用其本地对应版本:

<!-- <Label Text="{Binding Description}" /> -->
<ios:UILabel Text="{Binding Description}" View.HorizontalOptions="Start"/>
<androidWidget:TextView Text="{Binding Description}" x:Arguments="{x:Static formsandroid:Forms.Context}" />

现在,视图将为每个平台包含一个不同的本地控件。 此外,UILabel.TextTextView.Text属性现在携带到Description字段的绑定。

重要提示

需要注意的是,要让本机视图引用工作,所讨论的视图不应该包含在XamlCompilation中。 换句话说,视图应该带有[XamlCompilation(XamlCompilationOptions.Skip)]属性。

还可以使用本机类型和属性进一步定制本机字段。 例如,为添加一个投影到UILabel项,我们可以使用ShadowColorShadowOffset值:

<ios:UILabel 
    Text="{Binding Description}" 
    View.HorizontalOptions="Start" 
 ShadowColor="{x:Static ios:UIColor.Gray}">
         <ios:UILabel.ShadowOffset>
             <iosGraphics:CGSize>
                 <x:Arguments>
                     <x:Single>1</x:Single>
                     <x:Single>2</x:Single>
                 </x:Arguments>
             </iosGraphics:CGSize>
         </ios:UILabel.ShadowOffset>
 </ios:UILabel>

该声明的结果如下(将其与 Xamarin 进行比较。 formLabel字段我们之前定义):

Figure 5.15 – Native Properties

图 5.15 -原生属性

至此,我们已经完成了这个实现。 在本节中,我们使用额外的视图元素和基本布局扩展了示例应用。 如您所见,这些开箱即用的布局和视图为开发人员提供了简单的定制选项。 现在我们已经有了一个基本的 UI,让我们仔细看看如何将引入这些 UI 元素的域数据。 在下一节中,我们将讨论数据驱动视图。

创建数据驱动视图

MVVM 架构,正如你在第四章用 Xamarin 开发移动应用中看到的,主要集中在数据以及如何从视图中解耦数据。 然而,这种解耦并不意味着创建的视图和控件不应该响应数据内容的更改,这些更改可能是用户输入的结果,也可能是更新状态数据的结果。 为了方便数据模型的传播,从视图模型到视图,以及视图、数据绑定和其他数据相关的 Xamarin 之间的传播。 表单机制是至关重要的工具。

在本节中,我们将演示允许我们作为开发人员检索、转换和更新域数据而不直接引用这些数据点的各种特性。 首先,我们将修改关于数据绑定基础知识。 然后,我们将继续讨论值转换器,以及如何将它们与数据绑定结合使用。 我们还将研究数据触发器和可视化状态,以及数据如何驱动 UI 上的更改,并将某些行为强加于视图元素。

数据绑定要领

Xamarin 中最简单的数据绑定。 窗体包含我们想要链接到当前视图属性的属性的路径。 在这种类型的声明中,我们假设整个和/或父视图的BindingContext类被设置为使用目标源视图模型。

如果我们看看从HomeViewProductDetailsView的导航实现,你会注意到列表中选择的项目被设置为ProductDetailsView的绑定上下文:

private void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
 {
     var itemView = new ProductDetailsView();
     itemView.BindingContext = e.Item;
     Navigation.PushAsync(itemView);
 }

设置BindingContext后,我们可以继续使用ProductViewModel的属性模型,假设ProductViewModel被设置为触发Title属性的PropertyChangedEvent(从INotifyPropertyChanged开始):

<Label Text="{Binding Title}" FontSize="Large" />

数据绑定并不总是需要与值属性相关(例如,TextSelectedItem等等); 它还可以用来识别视图的视觉属性。

例如,我们之前添加到ProductDetailsView的芯片定义当前选择的项目是否支持某些特性。 让我们假设在视图模型侧有布尔属性来显示或隐藏这些值。 绑定看起来类似如下:

<Label x:Name="Feat1" Text="Feature 1" IsVisible="{Binding HasFeature1}" BackgroundColor="Gray" />
 <Label x:Name="Feat2" Text="Feat. 2" IsVisible="{Binding HasFeature2}" BackgroundColor="Lime"/>

在这两个绑定场景中,我们都将一个值从视图模型绑定到一个特定的视图元素。 另一个有效的场景是视图的更改影响另一个视图(即视图到视图的绑定)。 让我们假设,在ProductDetailsView上,规格的可见性取决于标签的可见性,将x:Name设置为Feat1:

<Grid IsVisible="{Binding Path=IsVisible,Source={x:Reference Feat1}}">

需要注意的是,在真实的项目中,视图到视图的绑定通常用于将用户输入反映在另一个视图上。 在本例中,绑定使用相同的视图模型属性(即HasFeature1)会更合适。

在创建可视化树之后,我们到目前为止所概述的绑定实际上并不依赖于 UI 中反映的任何更改。 在这样的设置中,监听视图模型属性上的任何更改事件是可以避免的性能损失。 为了弥补这个开销,我们可以将绑定模式设置为OneTime:

<Label Text="{Binding Title, Mode=OneTime}" FontSize="Large" />

这样,绑定只在BindingContext发生变化时执行。 如果我们希望ViewModel(通常被称为源)中的变化反映在View(被称为目标)中,我们可以使用OneWay绑定。 如果由OneWay绑定提供的单向数据流的方向是相反的,我们也可以使用OneWayToSource。 绑定提供了支持双向数据流的基础架构。

尽管运行时试图在建立绑定时将源类型转换为目标类型,但结果可能并不总是理想的(例如,不同类型的ToString方法可能不能提供正确的显示值)。 在这些情况下,开发人员可以求助于使用值转换器。

值转换器

值转换器可以被描述为实现IValueConverter接口的简单转换工具。 这个接口提供了两个方法,它们允许我们将源转换为目标,以及将目标转换为源,以支持各种绑定场景。

例如,如果我们要显示库存中一个项目的发布日期,我们将需要绑定到ProductViewModel上的相应属性。 然而,一旦页面被呈现,结果就不那么令人满意了:

Figure 5.16 – Full Date Format Displayed

图 5.16 -显示的完整日期格式

要格式化日期,we 可以创建一个值转换器,该转换器负责将DateTime值转换为字符串:

 public class DateFormatConverter : IValueConverter
 {
     public object Convert(object value, Type targetType, object
     parameter, CultureInfo culture)
     {
 if(value is DateTime date)
 {
 return date.ToShortDateString();
 }

         return null;
     }

     public object ConvertBack(object value, Type targetType, object
     parameter, CultureInfo culture)
     {
         // No Need to implement ConvertBack for OneTime and OneWay bindings.
         throw new NotImplementedException();
     }
 } 

它还负责在我们的ProductDetailsViewXAML 中声明这个转换器:

 <ContentPage 
     ...
     xmlns:converters="using:FirstXamarinFormsApplication.Client.Converters"        
     x:Class="FirstXamarinFormsApplication.Client.ItemView">
     <ContentPage.Resources>
         <ResourceDictionary>
             <converters:DateFormatConverter x:Key="DateFormatConverter" />
         </ResourceDictionary>
     </ContentPage.Resources>
     <ContentPage.Content>
         <!-- Removed for brevity -->
         <Label Text="{Binding ReleaseDate, Converter={StaticResource DateFormatConverter}}" />
         <!-- Removed for brevity -->    
     </ContentPage.Content>
 </ContentPage> 

现在,显示将使用短日期格式,这是与文化相关的(例如,EN-US 地区的M/d/yyyy):

Figure 5.17 – Formatted Date Display

图 5.17 -格式化日期显示

通过使用绑定传递日期格式字符串(例如M/d/yyyy)来使用固定的日期格式,我们可以进一步实现该实现。

Xamarin 的。 Forms 还提供了格式化字符串的使用来处理简单的字符串转换,因此可以避免等简单的转换器(如DateFormatConverter)。 具有固定日期格式的相同实现可以设置如下:

<Label Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}'}}" />

结果会是这样的:

Figure 5.18 – String Format Example

图 5.18 -字符串格式示例

此外,我们可能喜欢处理将发布日期设置为 null 的场景(也就是说,当ReleaseDate属性设置为Nullable<DateTime>或简单地DateTime)。 对于这种情况,我们可以使用TargetNullValue:

<Label Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}', TargetNullValue='Release Unknown'}" /> 

TargetNullValue,顾名思义,是一个替换值,当绑定目标已被解析,但找到的值为空时。 类似地,当运行时不能解析绑定上下文上的目标属性时,可以使用FallbackValue

扩展这个实现,如果发布日期未知,我们可能想用不同的颜色显示Label。 为了实现这一点,我们可以创建一个根据发布值返回特定颜色的转换器,但是我们也可以使用一个属性触发器来根据标签的Text属性值设置字体颜色。 在这种情况下,使用触发器是更好的选择,因为使用转换器将意味着硬编码颜色值,而触发器可以使用动态或静态资源,并可以使用目标视图的样式应用。

触发器

可以将触发器定义为需要执行的声明性动作。 不同类型的触发器如下:

  • 属性触发器:视图的属性更改
  • 数据触发器:绑定更改数据值
  • 事件触发:目标视图中特定事件的发生
  • 多触发器:用于实现多触发器组合

为了说明触发器的使用,我们可以使用前面的示例,其中某项的ReleaseDate不存在。 在这个场景中,由于定义了TargetNullValue属性,标签的文本将被设置为Release Unknown。 这里,我们可以使用属性触发器来设置字体颜色:

<Label x:Name="ReleaseDate" Text="{Binding ReleaseDate, StringFormat='Release {0:M/d/yyyy}', TargetNullValue='Release Unknown'}">
     <Label.Triggers>
 <Trigger TargetType="Label" Property="Text" Value="Release 
 Unknown">
             <Setter Property="TextColor" Value="Red" />
         </Trigger>    
     </Label.Triggers>
 </Label>

这里,目标类型定义包含元素(即触发器操作的目标),而属性和值定义触发器的原因。 然后可以将多个设置器应用于正在修改视图值的目标。

以类似的方式,我们可以创建一个数据触发器来设置标题的颜色,这取决于发布日期标签的值:

<Label Text="{Binding Title, Mode=OneTime}" FontSize="Large">
    <Label.Triggers>
        <DataTrigger TargetType="Label" 
            Binding="{Binding Source={x:Reference ReleaseDate},
 Path=Text}" 
            Value="Release Unknown">
            <Setter Property="TextColor" Value="Red" />
        </DataTrigger>
    </Label.Triggers>    
</Label>

这里,我们将DataTrigger的绑定上下文设置为另一个视图(即视图到视图)绑定。 如果我们使用视图模型作为绑定上下文,我们也可以使用ReleaseDate

最后,如果我们有没有发布日期的,但我们有数据来支持一个项目实际上已经向公众发布了,我们可以使用MultiTrigger:

<MultiTrigger TargetType="Label">
    <MultiTrigger.Conditions>
 <PropertyCondition Property="Text" Value="Release Unknown" />
 <BindingCondition Binding="{Binding IsReleased}" Value="false"/>
    </MultiTrigger.Conditions>
    <Setter Property="TextColor" Value="Red" />
</MultiTrigger>

事件触发器是触发器家族的奇怪成员,因为它们依赖于在目标视图而不是Setters上触发的事件; 他们使用Action

例如,为了增加一点 UX 增强,我们可以在项目视图中为图像添加淡出动画。 要使用这个动画,我们需要实现它作为Action的一部分:

 public class AppearingAction : TriggerAction<VisualElement>
 {
     public AppearingAction() { }

     public int StartsFrom { set; get; }

     protected override void Invoke(VisualElement visual)
     {
         visual.Animate("FadeIn", 
         new Animation((opacity) => visual.Opacity = opacity, 0, 1),
         length: 1000, // milliseconds
         easing: Easing.Linear);
     }
 } 

现在已经创建了TriggerAction,我们可以在映像上定义一个事件触发器(即使用BindingContextChanged事件):

<Image Source="{Binding Image}" HorizontalOptions="FillAndExpand">
    <Image.Triggers>
        <EventTrigger Event="BindingContextChanged">  
            <actions:AppearingAction />
        </EventTrigger>
    </Image.Triggers>
</Image>

这将创建一个微妙的淡入效果,这应该与正在加载的图像一致,从而提供一个更愉快的用户体验。

动作还可以通过使用EnterActionExitAction与属性和数据触发器一起使用,这两个触发器根据触发条件定义了这两种状态。 然而,在属性和数据触发器的上下文中,要创建更广义的状态,以及修改控件的公共状态,可以使用Visual State Manager(VSM)。 通过这种方式,多个设置器可以统一在一个状态中,从而减少 XAML 树中的混乱,并创建更易于维护的结构。

视觉状态

可视化状态和 VSM 将是 WPF 和 UWP 开发人员熟悉的概念; 然而,他们在 Xamarin 中缺失。 窗体运行时直到最近。 可视状态定义控件呈现时必须满足的各种条件。 例如,一个Entry元素可以处于NormalFocusedDisabled状态,每种状态都为元素定义了不同的视觉 setter。 此外,还可以为可视元素定义自定义状态,并且根据触发器或对VisualStateManager的显式调用,可以管理元素的可视状态。

为了演示这一点,我们可以为我们的标签创建三个不同的状态(例如,ReleasedUnReleasedUnknown),并使用我们的触发器来处理这些状态。

首先,我们需要定义标签控件的状态(然后它可以作为样式的一部分移动到资源字典中):

 <Label x:Name="ReleaseDate" ...>
     <Label.Triggers>
         <!-- Removed for Brevity -->
     </Label.Triggers>
     <VisualStateManager.VisualStateGroups>
         <VisualStateGroup x:Name="CommonStates">
             <VisualState x:Name="Released">
                 <VisualState.Setters>
                     <Setter 
                         Property="BackgroundColor" 
                         Value="Lime" />
                     <Setter 
                         Property="TextColor" 
                         Value="Black" />
                 </VisualState.Setters>
             </VisualState>
             <VisualState x:Name="UnReleased">
                 <VisualState.Setters>
                     <Setter Property="TextColor" Value="Black" />
                 </VisualState.Setters>
             </VisualState>
 <VisualState x:Name="Unknown">
 <VisualState.Setters>
 <Setter Property="TextColor" Value="Red" />
 </VisualState.Setters>
 </VisualState>
         </VisualStateGroup>
     </VisualStateManager.VisualStateGroups>
 </Label> 

如您所见,已定义状态中的一个是Unknown,它应该将文本颜色设置为红色。 要使用触发器来改变标签的状态,我们可以实现一个触发器动作:

 public class ChangeStateAction : TriggerAction<VisualElement>
 {
     public ChangeStateAction() { }

     public string State { set; get; }

     protected override void Invoke(VisualElement visual)
     {
         if(visual.HasVisualStateGroups())
         {
             VisualStateManager.GoToState(visual, State);
         }
     }
 } 

我们可以使用这个动作作为我们之前定义的多触发器的EnterAction:

<MultiTrigger TargetType="Label">
    <MultiTrigger.Conditions>
        <!-- Removed for brevity -->
     </MultiTrigger.Conditions>
     <MultiTrigger.EnterActions>
         <actions:ChangeStateAction State="Unknown" />
     </MultiTrigger.EnterActions>
</MultiTrigger> 

我们可以通过使用 setter 来达到相同的结果。 但是,需要指出的是,一旦标签被设置为给定状态,如果不定义ExitAction,它将不会恢复到以前的状态。

在本节中,您学习了如何成功地保持数据与视图元素的解耦,同时创建一个声明性可视树,该树将响应不同的数据类型和用户交互。 本节中使用的大多数可视化元素都针对单个数据项及其属性。 在下一节中,我们将分析专门处理数据项集合的不同视图。

集合视图

在 Xamarin 的早期阶段。 窗体,StackLayoutListView是显示元素集合的两个最流行的选项。 的区分因素这两个观点的是StackLayout只是用于静态内容(也就是说,没有收集数据绑定)而ListView是用于创建一个集合视图的数据项的集合(即ItemsSource)显示在模板定义的形式。

事实上,到目前为止,我们在创建HomeViewRootView菜单时使用了ListView元素。 如果你观察 HomeView 如何使用ListView,你会立即注意到集合绑定的主要元素允许数据绑定在ListView:

<ListView ItemsSource="{Binding RecentProducts}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <Grid>
                    <!-- Removed for brevity -->
                </Grid>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
 </ListView>

ItemsSource可以描述为集合绑定上下文,它定义在视图中使用什么数据,而ItemTemplate包含DataTemplate,它用于定义数据应该如何显示。

ItemsSource定义为IEnumerable,这意味着可以将任何类型的集合作为数据源分配给ListView。 然而,在处理动态数据集时,使用实现INotifyCollectionChanged接口的集合(如ObservableCollection)是非常有用的。 在这种设置中,对数据源集合的任何更改(例如添加或删除项)都将立即反映在呈现的元素集上。

DataTemplate可以使用ViewCell定义自定义模板。 例如,在我们的示例中,我们使用了Grid来设置产品列表的布局。 在这些类型的场景中,一般的经验法则是避免相对大小和位置,这样我们就不会招致性能损失。 此外,可以使用专门的细胞模板(如EntryCellSwitchCellTextCellImageCell来表示简单的数据项。

dattemplate 可以与DataTemplateSelector组合使用,以根据针对绑定数据上下文的谓词显示不同的模板。

如前所述,StackLayoutGrid最初仅用于显示静态内容。 但是,这些静态视图也可以使用附加的属性绑定到数据集合,例如BindableLayoutItemsSourceBindableLayout.ItemTemplate。 就像在我们的ListView示例中一样,这些属性将在呈现时用于在父布局中创建子元素。 然而,如果集合大小很大且数据是动态的,那么使用BindableLayout设置和StackLayout之类的控件会带来性能损失。

显示数据项集合的另一个选项是使用CollectionViewCollectionViewListView引入得晚一些,它提供了更灵活的模板选项集,并且性能更好。 与ListView一样,CollectionView也支持开箱即用的“下拉刷新”功能。

总结

在本章中,我们使用 Xamarin 的固有控件实现了一些简单的视图。 窗体框架,并设置一个基本的导航层次结构。 我们还研究了 Xamarin Shell 导航基础设施,它为导航基础设施提供了另一种选择。 在我们的视图中,我们使用了各种控件,并讨论了如何基于简单的数据项和集合创建响应性和数据驱动的 UI 元素。

通过大量的布局、视图和定制选项,开发人员可以创建有吸引力和直观的用户界面。 此外,数据驱动的 UI 选项可以帮助开发人员从这些视图中分离(解耦)任何业务域实现,从而提高任何移动开发项目的可维护性。

然而,有时,标准控制可能不足以满足项目需求。 在下一章中,我们将进一步研究如何定制现有的 UI 视图并实现自定义的本地元素。