65.9K
CodeProject 正在变化。 阅读更多。
Home

使用Calcium为Xamarin Forms创建跨平台应用程序栏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (20投票s)

2014 年 10 月 7 日

CPOL

19分钟阅读

viewsIcon

94136

使用 Xamarin Forms 平台特定渲染来创建跨平台应用程序栏。

Calcium for Xamarin Forms Banner

引言

Windows Phone 的应用程序栏是 Windows Phone 用户体验的标志性组成部分。我无法想象在不利用内置 ApplicationBar 的情况下构建 Windows Phone 应用程序。

在本文中,您将了解如何在 XAML 中定义自定义 AppBar;将其绑定到命令对象集合,以及直接将 AppBar 项绑定到 ViewModel 属性。您将看到如何利用平台特定渲染在 iOS、Android 和 Windows Phone 上显示 AppBar。您将了解到 Calcium AppBar 是多页面感知的,并且可以放置在由 CarouselPageTabbedPage 托管的多个页面中。您将看到如何在 iOS 和 Android 中显示自定义菜单。最后,您将了解如何注册自定义平台特定视图渲染器。

Calcium 的 Windows Phone 平台 AppBar 利用 Windows Phone SDK 内置的 ApplicationBar 来提供绑定支持,并允许在通过 XAML 定义的 PivotPanorama 控件中使用多个应用程序栏。此外,Calcium 的 AppBar 还支持切换按钮和切换菜单项、导航按钮等。这超出了您在为 Windows Phone SDK 的 ApplicationBar 控件创建原生渲染器时所能期望的。

注意。本文介绍的 Xamarin Forms 的 AppBar 控件仍在开发中。就生产就绪而言,它可能还没有完全达到。 

在开始之前,如果您还没有阅读过,我建议您在阅读本文之前先阅读本系列中的第一篇文章

本系列文章

Calcium 和示例应用程序的源代码

Calcium for Xamarin.Forms 和示例的源代码位于 https://calcium.codeplex.com/SourceControl/latest

存储库中有各种解决方案。您感兴趣的是位于 \Trunk\Source\Calcium\Xamarin\Installation 目录下的 CalciumTemplates.Xamarin.sln。

CalciumTemplates.Xamarin 解决方案的结构如图 1 所示。您可以看到已突出显示的示例“模板”项目。这些项目包含本系列文章中呈现的大部分示例源代码。

Solution Structure

图 1. CalciumTemplates.Xamarin 解决方案的结构

使用 AppBar

AppBar 包含一个按钮和菜单项的集合,并且根据应用程序运行的平台,其渲染方式不同。如果您的应用程序在 Windows Phone 上运行,AppBar 将使用内置的 ApplicationBar 控件进行渲染。在 iOS 和 Android 上运行时,AppBar 将使用 Xamarin Forms 工具栏(用于按钮)和对话框(用于菜单项)进行渲染。

AppBar 可以绑定到按钮和菜单项命令的列表,或者纯粹在 XAML 中定义,如列表 1 所示。

列表 1. 在 XAML 中定义 AppBar

<calcium:AppBar>
       <calcium:AppBar.Buttons>
            <calcium:AppBarItem Text="Foo" Tap="FooButtonHandleTap"
                IconUri="/Views/MainView/Images/AppBar/Check.png" />
              <calcium:AppBarItem Text="Bah" />
       </calcium:AppBar.Buttons>
       <calcium:AppBar.MenuItems>
              <calcium:AppBarItem Text="MenuItem1" />
       </calcium:AppBar.MenuItems>
</calcium:AppBar>   

当我开始为本文编写代码时,我决定超越我在 Windows Phone 版 Calcium 中已经创建的内容。我以一种允许开发人员更好地将 UI 与其 ViewModel 分离的方式构建了 AppBarAppBar 可以绑定到按钮命令的集合和菜单项命令的集合。我选择允许绑定到命令集合,而不是专有模型类,因为命令 API 是众所周知的,并且 Calcium 的 UICommand 对象拥有表示按钮或菜单项所需的一切。

在 WPF 中,RoutedUICommands 可用于封装命令的文本显示属性。其优点是,例如按钮的文本可以在命令逻辑中更新。当 Silverlight 到来时,将 UI 特定属性放在 ICommand 实现中的想法被认为是不受欢迎的。事实上,RoutedUICommand 未包含在 FCL 中。它完全留给 UI 根据其自身状态或 ViewModel 来显示文本。我认为,这导致了业务逻辑倾向于泄露到视图层。命令的概念可能与文本和图标的概念是正交的,但支持文本或图标的按钮控件的命令是如此普遍,以至于您不能否认它是一个适合合并到单个类型中的好候选。我想,这就是感知到的最佳实践和实用主义发生冲突的地方。

在评估这类设计决策时,在我看来,如果成本可以忽略不计,但有回报,那么您应该倾向于实用主义,并做出有利于简化开发的 API 决策,而不是可能导致内部复杂性的感知价值。

让我们继续概述 AppBar 的绑定功能。可下载示例代码中的 MainViewModel 包含两个 ObservableCollection,其中包含 IUICommand 类型的对象。IUICommands 扩展了熟悉的 ICommand 接口,以包含一个用于文本、可见性和图标 URL 的属性。实现 IUICommand 的对象的属性可以映射到 AppBar 中的按钮或菜单项。在以下摘录中,IUICommand 允许用户点按以导航到 HubView 页面

navigateToHubCommand = new UICommand(NavigateToHub)
       {
              Text = AppResources.MainView_AppBar_Buttons_Hub
       };

appBarButtonCommands.Add(navigateToHubCommand);

NavigateToHub 方法解析 HubView 页面(由 IoC 容器构建),并且 INavigation 实例导航到 HubView,如图所示

async void NavigateToHub(object arg)
{       
    var hubView = Dependency.Resolve<HubView>();
    Navigation.PushAsync(hubView);
}

通过创建另一个 UICommand 对象,将菜单项添加到 AppBar 中。这次,Calcium 的对话框服务向用户显示一个消息提示。DialogService 负责在 Windows Phone、iOS 和 Android 上显示对话框。

var menuCommand = new UICommand(_ => DialogService.ShowMessageAsync("Menu Item 1 tapped."))
       {
              Text = "Menu Item 1"
       };

appBarMenuCommands.Add(menuCommand);

在 MainView 页面上包含 XML 命名空间声明,如下所示

xmlns:calcium="clr-namespace:Outcoder.UI.Xaml;assembly=Outcoder.Calcium"

最后,在 MainView 页面上定义了一个 AppBar,它被数据绑定到两个 ObservableCollection,如所示

<calcium:AppBar
       ButtonCommands="{Binding AppBarButtonCommands}"
       MenuCommands="{Binding AppBarMenuCommands}" />

这在所有三个视图中都实现了,如图 2 所示。请注意三个平台上渲染的 AppBar 按钮的位置。

AppBar Expanded

图 2. 在 Windows Phone、iOS 和 Android 上显示的 AppBar

构建 Xamarin Forms 自定义 AppBar

您可以创建一个自定义的 Xamarin Forms 控件或视图,因为它们被称为,方法是继承 Xamarin.Forms.View 类。

适用于 Xamarin Forms 的 Calcium AppBar 由按钮的 ObservableCollection 和菜单项的 ObservableCollection 组成。按钮和菜单项都由实现 IAppBarItem 接口的 AppBarItem 类表示。IAppBarItem 具有以下成员

  • string Text
  • Uri IconUri
  • bool IsEnabled
  • void PerformTap()

IAppBarItem 接口的基本实现是 AppBarItemBase 抽象类。参见图 3。AppBarItem 类增加了命令支持。 

AppBar Class Diagram

图 3. AppBar 包含 IAppBarItem 对象的集合。

AppBarItemBase 类包含两个可绑定属性,它们在类的静态构造函数中进行初始化,如下所示

static AppBarItemBase()
{
       TextProperty = BindableProperty.Create(
           "Text", typeof(string), typeof(AppBarItemBase), string.Empty, BindingMode.TwoWay);
       IconUriProperty = BindableProperty.Create(
           "IconUri", typeof(Uri), typeof(AppBarItemBase), null, BindingMode.TwoWay);
}

请注意,IconUri 的类型是 Uri 而不是 string。在 Xamarin XAML VisualElements 中,使用字符串而不是专用类型作为属性是普遍存在的。Xamarin Forms 尚不支持隐式类型转换。为了允许在 XAML 中将 IconUri 属性设置为字符串值,有必要用 TypeConverter 属性装饰该属性,如以下摘录所示

[TypeConverter(typeof(UriTypeConverter))]
public Uri IconUri
{
       get
       {
              return (Uri)GetValue(IconUriProperty);
       }
       set
       {
              SetValue(IconUriProperty, value);
       }
}

可以使用 AppBarItemBase 类的 PerformTap 方法以编程方式启动 Tap 事件。这允许您从原生实现传递点击事件,如以下各节所示。

AppBarItem 类扩展了 AppBarItemBase 类以添加命令支持。AppBarItem 类中包含另外两个可绑定属性:Command 属性和 CommandParameter 属性。

使用 AppBarItem 对象时,命令是可选的。如果您愿意,可以仅依赖 Tap 事件来检测用户操作。

当设置了 AppBarItemCommand 属性时,我的意图是自动将 AppBarItem 绑定到命令的各种属性。不幸的是,我在这里遇到了一个障碍。 在撰写本文时,Xamarin Forms Binding 对象没有 RelativeSource 属性,这阻止了绑定到命令的 TextIconUri 属性。参见列表 2。 方法体在存储库中被注释掉了。

列表 2. AppBarItem.HandleCommandChanged 方法的第一次尝试

static void BindToCommand(AppBarItem item, ICommand command)
{
    if (command != null)
    {
        var newUICommand = command as IUICommand;
        if (newUICommand != null)
        {
            /* Without a relative source binding capability we can't bind to the command. */
            {
                if (string.IsNullOrEmpty(item.Text))
                {
                    item.SetBinding(TextProperty, new Binding("Command.Text", BindingMode.OneWay));
                }

                if (!string.IsNullOrEmpty(newUICommand.IconUrl))
                {
                    item.SetBinding(IconUriProperty, "Command.IconUrl", BindingMode.OneWay, 
                           new StringToUriConverter());
                }

                item.SetBinding(IsEnabledProperty, new Binding("Command.Enabled", BindingMode.OneWay));
            }
        }
    }
}

因此,绑定到命令的 Text 属性,例如,是在绑定表达式中完成的;如列表 3 所示。

列表 3. AboutView.xaml AppBar 摘录

<calcium:AppBar Grid.Row="1">

    <calcium:AppBar.Buttons>
        <calcium:AppBarItem Command="{Binding ExampleCommand}" 
                            Text="{Binding ExampleCommand.Text}" />
    </calcium:AppBar.Buttons>
</calcium:AppBar>

当用户点击 AppBarItem 的原生表示时,事件会通过 AppBarItem 可覆盖的 PerformTap 方法“流经”。如果为 AppBarItem 分配了命令,则会调用命令的 Execute 方法,如以下摘录所示

protected override void OnTap(EventArgs e)
{
       base.OnTap(e);

       var command = Command;
       if (command != null)
       {
              command.Execute(CommandParameter);
       }
}

注意。命令的存在不会覆盖 Tap 事件。在执行命令之前,任何订阅者仍会收到点击事件的通知。

将命令适配到 AppBar 项目

回顾一下,您可以将 AppBarButtonsMenuItems 属性绑定到 IUICommand 集合。显然,有一个中间步骤来解析要用于表示特定 IUICommandIAppBarItem 类型。这时就轮到 IAppBarItemFactory 了。IAppBarItemFactory 对象的工作是将命令映射到应用程序栏的对应项。IAppBarItemFactory 有一个方法,它接受一个 IUICommand 并返回一个 IAppBarItem。默认实现目前作用不大,只是创建一个 AppBarItem 并为其分配指定的命令,如下所示

public virtual IAppBarItem BuildItem(IUICommand command)
{
       ArgumentValidator.AssertNotNull(command, "command");
       var result = new AppBarItem {Command = command};
       return result;
}

IAppBarItemFactory 是一个可扩展点,因为您可以替换 IAppBarItemFactory 实现来执行您想要的任何自定义映射。AppBar 类的 AdaptItems 方法从 IoC 容器中检索 IAppBarItemFactory,如果未注册任何项,则回退到默认实现 AppBarItemFactory。参见列表 4。

列表 4. AppBar.AdaptItems 方法

List<IAppBarItem> AdaptItems(IEnumerable<IUICommand> commands)
{
       IAppBarItemFactory factory = Dependency.Resolve<IAppBarItemFactory, AppBarItemFactory>();

       List<IAppBarItem> result = new List<IAppBarItem>();

       foreach (var command in commands)
       {
              IAppBarItem item = factory.BuildItem(command);
              result.Add(item);
       }

       return result;
}

在本节中,您将学习如何创建一个自定义视觉元素。AppBar 的渲染方式必须取决于您的应用程序运行的平台。在下一节中,您将看到如何创建平台特定渲染器。

采用平台特定渲染,又称原生渲染

让我们深入了解 AppBar 如何在多个平台上实现,而无需您进行自定义管道代码。执行平台特定渲染的任务是使用 Xamarin Forms 渲染 API。对于 Calcium 的 AppBar,它使用了三个不同的渲染器:一个用于 iOS,一个用于 Android,一个用于 Windows Phone。在本节中,您将看到每个渲染器的实现方式,并且您将学习如何向 Xamarin Forms 渲染系统注册原生渲染器。

在 Xamarin Forms 中,使用自定义 ViewRenderer<TElement, TNativeElement> 实现来使用平台特定控件,其中 TElement 是 Xamarin Forms 自定义视图的类型,TNativeElement 是平台特定元素(如 Windows Phone 自定义控件)的类型。

注意:尽管 ViewRenderer 的泛型参数命名暗示 VisualElement 就足够了,但 ViewRenderer 的类型约束要求使用派生自 Xamarin.Forms.View 的类。

使用 View Renderer 显示 Windows Phone 应用程序栏

内置的 Windows Phone 应用程序栏是 Windows Phone 的关键组件之一。大多数基于 XAML 的应用程序都使用它,并为用户体验的关键方面提供了统一性。然而,如果您想要任何非标准行为,它确实会带来一些挑战。例如,Windows Phone 8 ApplicationBar 不支持数据绑定。与 Windows Phone Silverlight 应用程序中提供的其他丰富的开箱即用控件不同,它使用起来很笨拙,充其量只是底层原生控件的一个包装器。一个特别的限制是它不支持为 PivotPanorama 项创建多个应用程序栏。

很久以前,我决定为内置应用程序栏创建一个包装器,我在许多商业应用程序中都使用了它。它已成为我工具箱中有用的补充。我利用了这个自定义应用程序栏的功能来为 Xamarin Page 提供相同的多栏支持,这些页面放置在 MultiPage 容器(如 TabbedPage)中。

自定义 Windows Phone AppBar 类由 Windows Phone 特定的 AppBarRenderer 类渲染。 您可以在 Calcium 源存储库的 Outcoder.Calcium.XamarinForms.WindowsPhone 项目中找到适用于 Windows Phone 的 AppBarRenderer 类。该类存在于 Outcoder.UI.Xaml.Renderers 命名空间中。

public class AppBarRenderer : ViewRenderer<Outcoder.UI.Xaml.AppBar, Outcoder.UI.Xaml.Controls.AppBar> 
{ ... }

创建自定义渲染器时,需要做两件事。第一件事是您应该通过重写 ViewRenderer<TView,TNative> 类的 OnElementChanged 方法来响应 ElementChanged 事件。当调用 OnElementChanged 方法时,它提供了初始化控件的机会。第二件事是,一旦初始化了原生控件,它就会通过渲染器的 SetNativeControl 方法提供给渲染器。

在列表 5 中,您可以看到该方法负责分离旧的 AppBar(取消订阅其事件)并附加新的 AppBar 实例。此外,当前 Xamarin Page 实例是使用自定义 GetHostPage 方法找到的。渲染器订阅页面的 DisappearingAppearing 事件,以知道何时隐藏和重新构建 AppBarSetNativeControl 方法将 Xamarin Forms 基础结构要显示的元素提供给平台特定的界面。

列表 5. 重写 Windows Phone AppBar 的 OnElementChanged 方法

protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
       base.OnElementChanged(e);

       var oldElement = e.OldElement;

       if (oldElement != null)
       {
              DetachAppBar(oldElement);
       }

       if (!initialized)
       {
              initialized = true;

              Page page = GetHostPage();
              page.Disappearing += HandlePageDisappearing;
              page.Appearing += HandlePageAppearing;

              var wpAppBar = new Outcoder.UI.Xaml.Controls.AppBar();

              SetNativeControl(wpAppBar);
       }

       AppBar newAppBar = e.NewElement;
       AttachAppBar(newAppBar);
}

AppBarRenderer 维护对当前 Page 的弱引用,以检测何时另一页面接管了应用程序栏。以下摘录显示了 HandlePageAppearing 方法,该方法创建了一个新的 WeakReference 对象

void HandlePageAppearing(object sender, EventArgs e)
{
       Page ownerPage = GetHostPage();
       appBarOwner = new WeakReference<Page>(ownerPage);
       UpdateAppBar();
}

相反,当用户导航离开页面时,将调用 HandlePageDisappearing 方法。如果新页面与页面出现时注册的页面不对应,则表示不同的页面现在拥有应用程序栏。跟踪活动页面的原因是新页面的 Appearing 事件在当前页面的 Disappearing 事件之前触发。

void HandlePageDisappearing(object sender, EventArgs e)
{
       Page ownerPage;

       if (appBarOwner != null && appBarOwner.TryGetTarget(out ownerPage))
       {
              var hostContentPage = GetHostPage();
              if (ownerPage != hostContentPage)
              {
                     return;
              }
       }

       HideAppBar();
}

通过遍历视觉树,调用自定义方法 GetParentOfType<Page>() 来检索宿主页面。参见列表 6。我打算将此视觉树逻辑合并到 Calcium 基础库中,类似于您在 Calcium 的 WPF 和 Windows Phone 特定核心中看到的视觉树帮助逻辑。

列表 6. AppBarRenderer.GetParentOfType

T GetParentOfType<T>() where T : class
{
       T page = null;
       Element parent = Element;

       while (page == null)
       {
              parent = parent.Parent;

              if (parent == null)
              {
                     break;
              }

              page = parent as T;
       }

       return page;
}

当向渲染器呈现新的 AppBar 时,AttachAppBar 方法会订阅 AppBarButtonCollectionChangedMenuItemCollectionChanged 事件。参见列表 7。

列表 7. Windows Phone AppBarRenderer.AttachAppBar 方法

void AttachAppBar(AppBar appBar)
{
       if (appBar == null)
       {
              return;
       }

       DetachAppBar(appBar);

       appBar.ButtonCollectionChanged += HandleButtonsChanged;
       appBar.MenuItemCollectionChanged += HandleMenuItemsChanged;
}

当添加或删除按钮或菜单项时,AppBar 会通过调用渲染器的 UpdateAppBar 方法来重建。参见列表 8。使用基类 ViewRendererControl 属性来检索您之前提供的原生控件。

如果适用于 Xamarin Forms 的 AppBar 包含菜单项但没有按钮,则 Windows Phone 应用程序栏将设置为最小化状态;缩小其尺寸,同时仍允许展开菜单。

为适用于 Xamarin Forms 的 AppBar 中的每个项目创建一个新的原生 AppBar 项目。AppBarRenderer 为每个原生项目的 Click 事件分配一个处理程序,该处理程序将事件传递给 IAppBarItemPerformTap 方法。

您可能会注意到,要响应 AppBarItem 对象中的更改,还需要做更多工作。我计划让 Xamarin Forms IAppBarItem 对象中的属性更改流向原生 AppBar 项目。

列表 8. Windows Phone AppBarRenderer.UpdateAppBar 方法

void UpdateAppBar()
{
       var wpAppBar = Control;

       if (wpAppBar == null)
       {
              return;
       }

       wpAppBar.Buttons.Clear();
       wpAppBar.MenuItems.Clear();

       var xamAppBar = Element;
       var xamButtons = xamAppBar.Buttons.ToList();
       var xamMenuItems = xamAppBar.MenuItems.ToList();

       if (!xamButtons.Any())
       {
              if (!xamMenuItems.Any())
              {
                     wpAppBar.IsVisible = false;
                     return;
              }

              wpAppBar.Mode = Microsoft.Phone.Shell.ApplicationBarMode.Minimized;
       }

       wpAppBar.IsVisible = true;

       foreach (var item in xamAppBar.Buttons)
       {
              var button = new Outcoder.UI.Xaml.Controls.AppBarIconButton();
              button.Text = item.Text;
              button.IconUri = item.IconUri;
              IAppBarItem i = item;
              button.Click += (sender, args) => i.PerformTap();
              wpAppBar.Buttons.Add(button);
       }

       foreach (IAppBarItem item in xamAppBar.MenuItems)
       {
              var menuItem = new Outcoder.UI.Xaml.Controls.AppBarMenuItem();
              menuItem.Text = item.Text;
              /* Menu item icons are not supported on Windows Phone. */
              //menuItem.IconUri = item.IconUri;
              IAppBarItem i = item;
              menuItem.Click += (sender, args) => i.PerformTap();
              wpAppBar.MenuItems.Add(menuItem);
       }
}

使用 View Renderer 在 iOS 上显示工具栏

顺便说一句,当我开始构建跨平台 AppBar 时,Xamarin API 非常不同。Xamarin 的开发人员进行了一些重构,以统一三个平台上的渲染系统;这倒不是坏事。

我最初的意图是利用平台特定 API 在 iOS 和 Android 上填充工具栏。然而,事实证明,我的代码和 Xamarin 的代码开始相互干扰。您可以看到,在 Xamarin Forms 中创建工具栏的方法是填充 Xamarin Forms Page 的 ToolbarItems 属性。然后,Page 渲染器在构建页面时会构造工具栏。这干扰了我的 AppBar。我改变了策略,不再手动构建工具栏,而是决定利用 Xamarin Forms ToolbarItems 属性;根据 AppBar 的内容来填充它。

您会注意到,在列表 9 中,iOS AppBarRenderer 将其原生控件设置为一个标签。这是因为没有我需要操作的原生后备控件,但我仍然需要向 Xamarin 渲染子系统提供一些东西。我希望 Xamarin 团队能给我一个提供伪控件的替代方案。

每个平台上的图像解析方式存在差异,这使得提供统一的方法来放置和定位图像变得困难。我相信我已经找到了一些巧妙的解决方案。您将在本系列后面的文章中看到一个专门介绍此内容的章节。IImageUrlTransformer 对象与此方面相关,因此请暂时忽略它,稍后我们将返回讨论它。

填充工具栏的触发器几乎与 Windows Phone 实现相同;AppRenderer 依赖于活动 PageAppearingDisappearing 事件来确定何时应填充当前页面的 Xamarin Forms ToolbarItems 属性。

列表 9. iOS AppBarRenderer.OnElementChanged 方法

protected override void OnElementChanged(ElementChangedEventArgs<AppBar> e)
{
       base.OnElementChanged(e);

       var newAppBar = e.NewElement;

       if (!initialized)
       {
              initialized = true;

              imageUrlTransformer = Dependency.Resolve<IImageUrlTransformer, ImageUrlTransformer>(true);

              /* An Exception is raised if a control is not provided. */
              SetNativeControl(new UILabel(RectangleF.Empty));

              Page page = GetHostContentPage();
              page.Disappearing += (sender, args) =>
              {
                     DetachAppBar(newAppBar);
                     Page ownerPage;

                     if (toolbarOwner != null && toolbarOwner.TryGetTarget(out ownerPage))
                     {
                           var hostContentPage = GetHostContentPage();

                           if (ownerPage != hostContentPage)
                           {
                                  return;
                           }
                     }

                     RemoveToolbar();
              };

              page.Appearing += (sender, args) =>
              {
                     Page ownerPage = GetHostContentPage();
                     toolbarOwner = new WeakReference<Page>(ownerPage);
                     UpdateAppBar();
                     AttachAppBar(newAppBar);
              };
       }
}

现在您将看到 AppBar 如何使用 Xamarin Forms ToolbarItems 集合表示。为 AppBar 中的每个按钮创建一个 ToolbarItem。参见列表 10。当 ToolBarItemActivated 事件触发时,事件处理程序会调用 AppBarItemPerformTap 方法。

工具栏不存在菜单项集合。因此,AppBarRenderer 会构造一个特殊的菜单按钮,这类似于 Windows Phone 应用程序栏上的省略号按钮。当激活菜单按钮时,将调用自定义的 DisplayMenu 方法。接下来,我们将讨论该方法。

列表 10. iOS AppRenderer.UpdateAppBar 方法

void UpdateAppBar()
{
    var appBar = Element;
    Page page = GetParentOfType<ContentPage>();

    if (page == null)
    {
        return;
    }

    List<ToolbarItem> items = null;

    foreach (IAppBarItem appBarItem in appBar.Buttons)
    {
        string text;

        if (!AppBarItemPropertyResolver.TryGetItemText(appBarItem, out text))
        {
            throw new Exception("Unable to resolve text for button.");
        }

        ToolbarItem item = new ToolbarItem { Name = text };

        IAppBarItem itemForClosure = appBarItem;
        item.Activated += (sender, e) => itemForClosure.PerformTap();

        string iconUrl;

        if (AppBarItemPropertyResolver.TryGetItemUrl(appBarItem, out iconUrl))
        {
            string transformedUrl = imageUrlTransformer.TransformForCurrentPlatform(iconUrl);
            item.Icon = transformedUrl;
        }

        if (items == null)
        {
            items = new List<ToolbarItem>();
        }

        items.Add(item);
    }

    var menuItemsEnumerable = appBar.MenuItems;
    if (menuItemsEnumerable != null)
    {
        var menuItems = menuItemsEnumerable.ToList();
        int menuItemCount = menuItems.Count;
        if (menuItemCount > 0)
        {
            string[] buttonTitles = new string[menuItemCount];
            for (int i = 0; i < menuItemCount; i++)
            {
                string text;

                if (!AppBarItemPropertyResolver.TryGetItemText(menuItems[i], out text))
                {
                    throw new Exception("Unable to resolve text for menu item.");
                }

                buttonTitles[i] = text;
            }

            ToolbarItem item = new ToolbarItem { Name = menuToolbarItemText };
            item.Activated += async (sender, e) =>
            {
                var viewController = UIApplication.SharedApplication.Windows[0].RootViewController;
                var uiView = viewController.View;

                var action = await DisplayMenu(uiView, "Cancel", buttonTitles);
                if (action > -1)
                {
                    var selectedItem = menuItems[action];
                    selectedItem.PerformTap();
                }
            };

            if (items == null)
            {
                items = new List<ToolbarItem>();
            }

            items.Add(item);
        }
    }

    if (items != null && items.Any())
    {
        var toolbarItems = page.ToolbarItems;
        toolbarItems.Clear();
        items.Reverse();
        toolbarItems.AddRange(items);
    }
}

AppBarItemPropertyResolver 类用于检索 AppBarItem 的文本和图标 URL。它首先尝试直接从 AppBarItem 检索属性。如果为 null,则从 AppBarItem 的命令中检索属性。参见列表 11。

列表 11. AppBarItemPropertyResolver.TryGetItemText 方法。

internal static bool TryGetItemText(IAppBarItem appBarItem, out string text)
{
    bool resolvedText = false;

    text = appBarItem.Text;

    if (!string.IsNullOrEmpty(text))
    {
        resolvedText = true;
    }
    else
    {
        var commandItem = appBarItem as AppBarItem;
        if (commandItem != null && commandItem.Command != null)
        {
            var uiCommand = commandItem.Command as IUICommand;
            if (uiCommand != null)
            {
                text = uiCommand.Text;
                resolvedText = true;
            }
        }
    }

    return resolvedText;
}

在 Xamarin.iOS 中显示自定义菜单

iOS AppBarRenderer 依赖 UIActionSheet 类来显示选项列表。使用 UIActionSheet 对象的 AddButton 方法将其按钮添加到其中。参见列表 12。

DisplayMenu 方法是可等待的,UI 线程在显示 UIActionSheet 时不会被阻塞。TaskCompletionSource<int> 用于等待 UIActionSheet 关闭,结果 int 值是点击的按钮的索引,如果取消则为 -1。

列表 12. iOS AppBarRenderer.DisplayMenu 方法

Task<int> DisplayMenu(UIView uiView, string cancelText, params string[] buttons)
{
       var sheet = new UIActionSheet();

       foreach (var button in buttons)
       {
              sheet.AddButton(button);
       }

       int cancelButtonIndex = sheet.ButtonCount;
       sheet.AddButton(cancelText);
       sheet.CancelButtonIndex = cancelButtonIndex;

       TaskCompletionSource<int> source = new TaskCompletionSource<int>();

       try
       {
              sheet.Clicked += delegate(object sender, UIButtonEventArgs args)
              {
                     int result;

                     if (args != null)
                     {
                           int buttonIndex = args.ButtonIndex;

                           if (buttonIndex != cancelButtonIndex)
                           {
                                  result = buttonIndex;
                           }
                           else
                           {
                                  result = -1;
                           }
                     }
                     else
                     {
                           result = -1;
                     }

                     source.SetResult(result);
              };

              sheet.ShowInView(uiView);
       }
       catch (Exception ex)
       {
              source.SetException(ex);
       }

       return source.Task;
}

当用户点击我们自定义 AppBar 中的省略号按钮时,将显示 UIActionSheet。参见图 4。每个 AppBarItemTap 事件都会被触发,如果 AppBarItem 关联了命令,则执行该命令。

iOS AppBar Expanded

图 4. 使用 UIActionSheet 显示菜单。

 

使用 View Renderer 显示 Android 工具栏

在 AppBar 视图渲染器概述的最后一部分,我们将介绍 Android 的 AppBar ViewRenderer 实现。

Android 的 AppBarRenderer 位于 Calcium 源代码存储库的 Outcoder.Calcium.Android 项目中。它与 iOS AppBarRenderer 非常相似,因为它也利用了活动页面的 Xamarin Forms ToolbarItems 属性。因此,原生活动很少。关键区别在于菜单显示给用户的方式。参见列表 13。

Calcium ActionDialog 用于向用户显示选项列表。DisplayMenu 方法是可等待的,并返回一个 Task<int>。方法的返回值是点击的按钮的索引。

列表 13. Android AppBarRenderer.DisplayMenu 方法

Task<int> DisplayMenu(params string[] buttons)
{
       ActionDialogArguments arguments = new ActionDialogArguments(null, null, null, buttons);
       ActionDialog dialog = new ActionDialog(arguments, Context);

       dialog.Show();

       return arguments.Result.Task;
}

ActionDialog 类似于 Xamarin.Android ActionSheet 类。但是,有几个显著的区别。Calcium 的 ActionDialog 使用基于索引的系统而不是基于字符串的系统来标识哪个按钮被点击。它还允许隐藏对话框的标题。我认为这对于 AppBar 场景很有意义。它通过调用基类 Dialog 类的 RequestWindowFeature 来实现,如 ActionDialogOnCreate 方法的以下摘录所示

protected override void OnCreate(Bundle savedInstanceState)
{
       string actionSheetTitle = arguments.ActionSheetTitle;

       bool showTitle = !string.IsNullOrWhiteSpace(actionSheetTitle);

       if (!showTitle)
       {
              RequestWindowFeature((int)WindowFeatures.NoTitle);
       }

       base.OnCreate(savedInstanceState);

…

}

注意。如果您创建自己的自定义 Android 对话框,则需要在调用 base.OnCreate 方法之前调用 RequestWindowFeature

我将不再在此详细介绍 ActionDialog 的其余代码。如果您对此感兴趣,可以在 Calcium 源代码存储库的 Outcoder.Calcium.Android 项目中找到 ActionDialog 类。

点击 Android 上的省略号按钮会显示菜单项。参见图 5。

Android with Menu Expanded

图 5. Android 中的 AppBar 菜单

注册 View Renderer

要注册 Xamarin Forms 原生渲染器,您可以使用 ExportRenderer 属性,如下所示

[assembly: ExportRenderer(typeof(Outcoder.UI.Xaml.AppBar),
    typeof(Outcoder.UI.Xaml.Renderers.AppBarRenderer))]

在撰写本文时,导出渲染器存在平台差异。Xamarin.Android 和 Xamarin.iOS 项目都允许您从类库中导出渲染器。Windows Phone 的情况并非如此。要在 Windows Phone 项目中使用 AppBar,您必须在代码中显式添加 ExportRenderer 属性。在示例中,我选择在 Windows Phone 项目的 MainPage.xaml.cs 文件中进行此操作。

结论

在本文中,您了解了如何在 XAML 中定义自定义 AppBar;将其绑定到命令对象集合,以及直接将 AppBar 项绑定到 ViewModel 属性。您看到了如何利用平台特定渲染在 iOS、Android 和 Windows Phone 上显示 AppBar。您了解到 Calcium AppBar 是多页面感知的,并且可以放置在 CarouselPage 或 TabbedPage 托管的多个页面中。您看到了如何在 iOS 和 Android 中显示自定义菜单。最后,您了解了如何注册自定义视图渲染器。

下一篇文章中,您将学习如何在共享项目中将图像放置为任何地方的内容资源,就像在 Windows Phone 中一样,并在所有三个平台上以相同的方式使用它们。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

历史

2014 年 10 月

  • 首次发布。
© . All rights reserved.