使用MvvmCross框架实现Xamarin.Forms的汉堡菜单布局

注:本文是英文写的,偷懒自动翻译过来了,原文地址:Implementing MasterDetail layout in Xamarin.Forms by MvvmCross

欢迎大家关注我的公众号:程序员在新西兰,了解美丽的新西兰和码农们的生活

阅读本文大概需要20分钟。本文目录:

前言

通过MvxScaffolding创建项目

创建MasterDetailPage

创建MasterPage

创建DetailPages

实现菜单功能

微调UI

小结

 

 

前言

在我的Xamarin和MvvmCross手册中,我展示了使用MvvmCross Framework开发基本Xamarin应用程序的基础知识。在开发真实应用程序时需要考虑更多细节,例如布局,样式和数据库等。例如,汉堡菜单布局是现代移动应用程序中非常常见的导航模式。我们可以使用MasterDetail导航模式来实现汉堡菜单。接下来,我将向您展示如何在Xamarin.Forms应用程序中实现MasterDetail布局。在开始之前,我建议您在这里阅读有关MasterDetailPage的官方文档:Xamarin.Forms Master-Detail Page

我的开发环境如下所示:

  • Windows 10版本10.0.17134
  • Visual Studio 2017版本15.9.4
  • Xamarin.Forms版本3.4.0.1008975
  • MvvmCross版本6.2.2

让我们开始吧。

通过MvxScaffolding创建项目

如果您是MvvmCross的新手,使用MvvmCross创建Xamarin应用程序可能有点棘手。幸运的是,我们有一些项目模板来简化我们的工作。您可以在官方文档中找到它们:MvvmCross入门我建议你使用这个:MvxScaffolding它是新的,支持.net标准。您可以通过单击VS 2017中的工具 - 扩展和更新来搜索它,如下所示:

 

安装后,您可以在MvvmCross类别中创建一个新的Xamarin.Forms应用程序:

 

输入MvxFormsMasterDetailDemo为项目名称。MvxScaffolding为我们提供了一个非常友好的界面来定制应用程序。为了更好地理解,我们选择Blank模板,如下所示:

 

默认设置不包含UWP项目。如果您需要支持UWP平台,请选择它,并选择Min SDK版本为1803.由于旧的Windows 10版本不支持某些新功能,因此建议此时使用。此外,您需要输入描述作为UWP应用程序名称。

 

单击NEXT按钮,您将看到一个摘要窗口。检查所有信息,然后单击DONE按钮。MvxScaffolding将生成一个具有良好结构的基本空白Xamarin.Forms应用程序。

创建MasterDetailPage

MasterDetailPage是应用程序的根页面。实际上,它是一个MasterDetailPage的实例它不应该用作子页面以确保在不同平台上的一致用户体验。

创建ViewModel

接下来,添加在MvxFormsMasterDetailDemo.Core项目MasterDetailViewModel中的ViewModels文件夹中调用的新类文件将其更改为从MvxViewModel继承通常,我们还需要使用它NavigationService来实现ViewModel中的导航。因此,IMvxNavigationService通过使用依赖注入注入实例

using MvvmCross.Navigation;
using MvvmCross.ViewModels;

namespace MvxFormsMasterDetailDemo.Core.ViewModels
{
    public class MasterDetailViewModel : MvxViewModel
    {
        readonly IMvxNavigationService _navigationService;

        public MasterDetailViewModel(IMvxNavigationService navigationService)
        {
            _navigationService = navigationService;
        }
    }
}

 

创建XAML文件

Xamarin.Forms为我们提供了一些导航模式,包括分层导航,选项卡式页面,MasterDetailPage和模态页面等。根据我们的要求,我们希望在主页面上有一个汉堡菜单。所以我们可以使用MasterDetailPage,它是应用程序的根页面,包含两个区域:左边是MasterPage,右边是DetailPage。我们可以将菜单放在MasterPage中。单击菜单项时,导航服务将在DetailPage区域中显示另一页。

在MvvmCross中,Xamarin.Forms中有MvxFromsPagePresenter不同的页面类型,它们定义了视图的显示方式。我们MvxPagePresentationAttribute用来指定不同的页面类型。有关更多详细信息,请在此处查看文档:Xamarin.Forms查看演示者

App.cs在MvxFormsMasterDetailDemo.Core项目中打开该文件。请注意,框架将从HomeViewModel第一页开始现在让我们创建一个MasterDetailPage并用它来替换第一页。

右键单击MvxFormsMasterDetailDemo.UI项目中的Pages文件夹,然后选择AddNew ItemContent Page从Xamarin.Forms类别中选择,如下所示:

 

打开MasterDetailPage.xaml文件。请注意,此页面是一个ContentPage我们需要将其改为继承MvxMasterDetailPage用以下代码替换XAML代码:

<?xml version =“1.0”encoding =“utf-8”?> 
<views:MvxMasterDetailPage xmlns =“http://xamarin.com/schemas/2014/forms” 
             xmlns:x =“http://schemas.microsoft .com / winfx / 2009 / xaml“ 
             x:Class =”MvxFormsMasterDetailDemo.UI.Pages.MasterDetailPage“ 
             xmlns:views =”clr-namespace:MvvmCross.Forms.Views; assembly = MvvmCross.Forms“ 
             xmlns:viewModels =”clr- namespace:MvxFormsMasterDetailDemo.Core.ViewModels; assembly = MvxFormsMasterDetailDemo.Core“ 
             x:TypeArguments =”viewModels:MasterDetailViewModel“> 
</ views:MvxMasterDetailPage>

 

我们MvxMasterDetailPage用来替换默认ContentPage类型。为此,我们需要添加以下代码:

xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"

 

要设置MasterDetailPage的ViewModel,我们需要指定x:TypeArgumentsviewModels:MasterDetailViewModel不要忘记通过添加以下代码导入viewModels命名空间:xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"

打开MasterDetailPage.xaml.cs文件,将其基类替换ContentPageMvxMasterDetailPage<MasterDetailViewModel>MvxMasterDetailPagePresentation属性添加到类中,如下面的代码:

using MvvmCross.Forms.Presenters.Attributes;
using MvvmCross.Forms.Views;
using MvxFormsMasterDetailDemo.Core.ViewModels;
using Xamarin.Forms.Xaml;

namespace MvxFormsMasterDetailDemo.UI.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    [MvxMasterDetailPagePresentation(Position = MasterDetailPosition.Root, WrapInNavigationPage = false, Title = "MasterDetail Page")]
    public partial class MasterDetailPage : MvxMasterDetailPage<MasterDetailViewModel>
    {
        public MasterDetailPage()
        {
            InitializeComponent();
        }
    }
}

我们来看看MvxMasterDetailPagePresentation属性。有一些非常重要的属性MvxMasterDetailPagePresentationPosition是一个枚举值,用于指示页面的类型,在此处设置为Root。请设置如图所示的其他属性,否则,您可能会得到一些奇怪的结果。

创建MasterPage

MasterPage用于显示汉堡包菜单,ContentPage其中包含一个ListView我们将使用数据绑定来初始化菜单项。

创建ViewModel

在MvxFormsMasterDetailDemo.Core项目ViewModels文件夹中创建一个类MenuViewModel使用以下代码替换内容:

using System.Collections.ObjectModel;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;

namespace MvxFormsMasterDetailDemo.Core.ViewModels
{
    public class MenuViewModel : MvxViewModel
    {
        readonly IMvxNavigationService _navigationService;

        public MenuViewModel(IMvxNavigationService navigationService)
        {
            _navigationService = navigationService;
            MenuItemList = new MvxObservableCollection<string>()
            {
                "Contacts",
                "Todo"
            };
        }

        #region MenuItemList;
        private ObservableCollection<string> _menuItemList;
        public ObservableCollection<string> MenuItemList
        {
            get => _menuItemList;
            set => SetProperty(ref _menuItemList, value);
        }
        #endregion
    }
}

 

它有一个MenuItemList用来存储一些菜单项属性。为简单起见,只有两个字符串:ContactsTodo我们还需要IMvxNavigationService在构造函数中注入实例

创建XAML文件

接下来,在MvxFormsMasterDetailDemo.UI项目中的Pages文件夹中,添加一个新的ContentPage,命名为MenuPage.xaml打开MenuPage.xaml文件并使用以下代码替换内容:

<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
             xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
             x:Class="MvxFormsMasterDetailDemo.UI.Pages.MenuPage"
             x:TypeArguments="viewModels:MenuViewModel" >
    <ContentPage.Content>
        <StackLayout>
            <ListView></ListView>
        </StackLayout>
    </ContentPage.Content>
</views:MvxContentPage>

打开MenuPage.xaml.cs文件并设置基类和属性,如下所示:

using MvvmCross.Forms.Presenters.Attributes;
using MvvmCross.Forms.Views;
using MvxFormsMasterDetailDemo.Core.ViewModels;
using Xamarin.Forms.Xaml;

namespace MvxFormsMasterDetailDemo.UI.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    [MvxMasterDetailPagePresentation(Position = MasterDetailPosition.Master, WrapInNavigationPage = false, Title = "HamburgerMenu Demo")]
    public partial class MenuPage : MvxContentPage<MenuViewModel>
    {
        public MenuPage ()
        {
            InitializeComponent ();
        }
    }
}

MvxMasterDetailPagePresentation的属性Position应该被设置为Master,这意味着该页面将被显示为MasterDetailPage的Master。MasterPage还有另一个陷阱:必须设置Title属性,否则,您的应用程序将被卡住。因此,您必须设置MvxMasterDetailPagePresentation属性的Title属性。

现在我们需要设置数据绑定ListView我们已经在ViewModel有MenuItemList,所以我们现在要做的就是设置ListView的ItemsSource,如下所示:

<ListView ItemsSource="{Binding MenuItemList}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding}"></TextCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

目前,我们只是使用TextCell来显示菜单文本。在我们实现汉堡包菜单的全部功能之前,让我们创建DetailPages。

创建DetailPages

为简单起见,我们只添加两个页面作为详细信息页面。

创建ViewModels

MvxFormsMasterDetailDemo.Core项目的ViewModels文件夹中添加两个名为ContactsViewModel和TodoViewModel的新文件让它们分别从MvxViewModel继承

using MvvmCross.ViewModels;

namespace MvxFormsMasterDetailDemo.Core.ViewModels
{
    public class ContactsViewModel : MvxViewModel
    {
    }
}
using MvvmCross.ViewModels;

namespace MvxFormsMasterDetailDemo.Core.ViewModels
{
    public class TodoViewModel : MvxViewModel
    {
    }
}

创建XAML文件

将两个ContentPage文件添加到MvxFormsMasterDetailDemo.UI项目的Pages文件夹中,并将它们命名为ContactsPage.xamlTodoPage.xaml要使用MvvmCross功能,我们需要将它们更改为继承自MvxContentPage打开ContactsPage.xaml文件并使用以下代码替换内容:

<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MvxFormsMasterDetailDemo.UI.Pages.ContactsPage"
             xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
             xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
             x:TypeArguments="viewModels:ContactsViewModel">
    <ContentPage.Content>
        <StackLayout>
            <Label Text="Welcome to ContactsPage!"
                VerticalOptions="CenterAndExpand" 
                HorizontalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</views:MvxContentPage>

Label用于指示当前页面。

打开ContactsPage.xaml.cs文件并更新内容,如下所示:

using MvvmCross.Forms.Presenters.Attributes;
using MvvmCross.Forms.Views;
using MvxFormsMasterDetailDemo.Core.ViewModels;
using Xamarin.Forms.Xaml;

namespace MvxFormsMasterDetailDemo.UI.Pages
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    [MvxMasterDetailPagePresentation(Position = MasterDetailPosition.Detail, NoHistory = true, Title = "Contacts Page")]
    public partial class ContactsPage : MvxContentPage<ContactsViewModel>
    {
        public ContactsPage ()
        {
            InitializeComponent ();
        }
    }
}

Position属性的值MasterDetailPosition.Detail,这意味着此页面应位于MasterDetailPage的Detail区域。NoHistory属性应该是true,用来保证针对不同平台的导航没有奇怪的行为。Title属性用于在页面顶部显示页面名称。

TodoPage.xamlTodoPage.xaml.cs做同样的更改不要忘记更新Label控件的Text以显示页面名称。

实现菜单功能

现在,我们有我们需要显示所有的页面:根页叫MasterDetailPage,一个MasterPage叫做MenuPage和两个DetailPages叫做ContactsPageTodoPage接下来,我们需要使菜单正常工作。

显示MasterPage和DetailPage

打开MasterDetailViewModel.cs文件并覆盖ViewAppearing方法,如下所示:

public override async void ViewAppearing()
        {
            base.ViewAppearing();
            await _navigationService.Navigate<MenuViewModel>();
            await _navigationService.Navigate<ContactsViewModel>();
        }

ContactsPage应用程序启动时被用作DetailPage。因为我们已经MenuPage和ContactsPage指定了属性MvxMasterDetailPagePresentation,所以MvvmCross会找到并将它们显示在正确位置。

在MvxFormsMasterDetailDemo.Core项目中打开App.cs文件,并将第一页替换为MasterDetailPage

public class App : MvxApplication
    {
        public override void Initialize()
        {
            RegisterAppStart<MasterDetailViewModel>();
        }
    }

现在我们可以为三个平台启动应用程序:

安卓:

 
 

默认视图很好。Xamarin.Forms会自动在页面左上角添加一个汉堡包图标按钮。当我们单击按钮时,菜单显示,但没有页眉。我们稍后会调整UI。

iOS版:

 
 

iOS的默认视图与Android不同。页面上没有汉堡包图标。关于MenuPage标题栏的另一个问题与Android相同。看起来我们需要添加一个汉堡图标并显示头部标题栏。我们稍后会这样做。

UWP:

 

发生了什么?MasterPage自动显示,但没有默认的汉堡包按钮。

有关MasterDetailPage导航行为的详细信息,请在此处阅读:MasterDetailPage概述根据文档,母版页应该有一个包含按钮的导航栏。但现在我们得到了一些不同的结果。无论如何,我们可以自己解决它。

要修复UWP的布局,只需设置MasterBehaviorMasterDetailPage 属性即可。它是一个枚举值,用于确定详细信息页面在MasterDetailPage中的显示方式。如果保留它Default,它将分别显示不同平台的DetailPage。这就是我们得到不同结果的原因。

MasterDetailPage.xaml在MvxFormsMasterDetailDemo.UI项目中打开该文件。MasterBehavior在页面定义中添加属性并将其设置为Popover,这意味着DetailPage将覆盖或部分覆盖MasterPage:

<?xml version="1.0" encoding="utf-8" ?>
<views:MvxMasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MvxFormsMasterDetailDemo.UI.Pages.MasterDetailPage"
             xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
             xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
             x:TypeArguments="viewModels:MasterDetailViewModel" MasterBehavior="Popover">

</views:MvxMasterDetailPage>

要查看效果,请运行UWP项目,它看起来像这样:

 
 

现在它在页面左上方有默认的汉堡包按钮。运行Android和iOS项目以确保所有内容都不会因轻微更改而中断。您可能会注意到这三个平台之间仍存在一些差异。例如,UWP项目有标题栏,但Android和iOS没有。Android和UWP有默认的汉堡包按钮,但iOS没有。我们稍后会修复它们。

设置菜单导航

单击菜单项时,应用程序应显示正确的DetailPage。现在让我们设置Command菜单项。在MvxFormsMasterDetailDemo.Core项目中打开MenuViwModel.cs文件,然后添加一个Command,如下所示:

#region ShowDetailPageAsyncCommand;
        private IMvxAsyncCommand<string> _showDetailPageAsyncCommand;
        public IMvxAsyncCommand<string> ShowDetailPageAsyncCommand
        {
            get
            {
                _showDetailPageAsyncCommand = _showDetailPageAsyncCommand ?? new MvxAsyncCommand<string>(ShowDetailPageAsync);
                return _showDetailPageAsyncCommand;
            }
        }
        private async Task ShowDetailPageAsync(string param)
        {
            // Implement your logic here.
        }
        #endregion

这是一个实例IMvxAsyncCommand<T>,其中包含来自数据绑定的参数。参数的类型是string,因为我们知道MenuItemListis中的对象是string类型如果您MenuItemList 有其他一些泛型类型,请记住将泛型类型更改为您的实际项类型。

然后我们需要为命令设置数据绑定。MenuPage.xaml在MvxFormsMasterDetailDemo.UI项目中打开该文件并检查当前ItemTemplate

<ListView ItemsSource="{Binding MenuItemList}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding}"></TextCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

这里我们只使用一个简单的TextCell来显示菜单文本。如何将命令绑定到ListView

在我们开始数据绑定之前,我建议您阅读本文:Xamarin.Forms Command 接口在Xamarin.Forms中,一些控件原生支持Command,比如ButtonMenuItemTextCell和一些继承自它们的类。并且,SearchBar支持SearchCommand,实际上也是一种ICommand类型的属性ListView的RefreshCommand属性也是ICommand接口的实例

对于那些不直接支持ICommand的控件,Xamarin.Forms提供了一个TapGestureRecognizer来支持Command绑定。有关详细信息,请阅读以下文章:添加点按手势识别器请记住,虽然GestureRecognizer支持多种手势,如pinchpanswipe,但只TapGestureRecognizer支持ICommand另一个限制是视图元素必须支持GestureRecognizers

现在让我告诉你如何使用TapGestureRecognizer绑定Command到菜单项。首先,设置MenuPage的x:Name属性

<views:MvxContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
             xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
             x:Class="MvxFormsMasterDetailDemo.UI.Pages.MenuPage"
             x:TypeArguments="viewModels:MenuViewModel" 
             x:Name="MainContent">

我们需要名称来引用页面的当前ViewModel。

更新ItemTemplate如下:

<ListView.ItemTemplate>
    <DataTemplate>
        <ViewCell>
            <StackLayout Padding="10">
                <StackLayout.GestureRecognizers>
                    <TapGestureRecognizer 
                        Command="{Binding BindingContext.DataContext.ShowDetailPageAsyncCommand, Source={x:Reference MainContent}}"
                        CommandParameter="{Binding}">
                    </TapGestureRecognizer>
                </StackLayout.GestureRecognizers>
                <Label Text="{Binding}" VerticalOptions="Center"></Label>
            </StackLayout>
        </ViewCell>
    </DataTemplate>
</ListView.ItemTemplate>

 

我们对ItemTemplate做了一些修改

首先,用ViewCell替换默认的TextCellViewCell为我们提供了更多自定义UI的灵活性。所以我们可以随意定义ItemTemplate例如,我们可能会为每个菜单项添加一个图标。

ViewCell元素中,使用一个StackLayout控件作为容器,它支持GestureRecognizers,所以我们可以将TapGestureRecognizer添加StackLayout

TapGestureRecognizer元素中,我定义了两个重要的属性,一个是Command,另一个是CommandParameter正如我在之前的文章中所说,你必须非常清楚DataContext你绑定到你的观点。对于我们的情况下,我必须找到MenuViewModel中的命令ShowDetailPageAsyncCommand,所以我用Source={x:Reference MainContent}获取源对象,这是一个当前的名为MainContent的页面现在我们可以获取页面的ViewModel,也就是 BindingContext.DataContext,然后将BindingContext.DataContext.ShowDetailPageAsyncCommand用作绑定路径。我对BindingContext.DataContext有点困惑,因为它与UWP中的语法不同。请注意,完整语法是:

Command =“{Binding Path = BindingContext.DataContext.ShowDetailPageAsyncCommand,Source = {x:Reference MainContent}}”

当Path作为命令绑定的第一个参数添加时,可以被删除。

对于CommandParameter,它更容易。只需将当前字符串绑定到它。所以它是CommandParameter="{Binding}"如果您使用包含某些属性的对象,请使用CommandParameter="{Binding YourProperty}"

接下来,让我们更新命令。再次打开MvxFormsMasterDetailApp.Core项目中ViewModels文件夹的MenuViewModel.cs文件,并完成该ShowDetailPageAsync方法,如下面的代码:

private async Task ShowDetailPageAsync(string param)
        {
            // Implement your logic here.
            switch (param)
            {
                case "Contacts":
                    await mvxNavigationService.Navigate<ContactsViewModel>();
                    break;
                case "Todo":
                    await mvxNavigationService.Navigate<TodoViewModel>();
                    break;
                default:
                    break;
            }
        }
        #endregion

此方法接收来自命令绑定的参数。因此,我们可以确定哪个页面应显示为DetailPage。

现在启动应用程序并观察导航行为。还有一个问题。单击菜单项时,虽然DetailPage显示正确,但MenuPage仍覆盖DetailPage。所以我们必须控制MasterPage的导航行为。为此,我们需要将Xamarin.Forms安装到MvxFormsMasterDetailDemo.Core项目。您可以通过Xamarin.Forms在NuGet包管理器中搜索来安装它请与其他项目安装相同版本的Xamarin.Forms以避免引用错误。对于我的演示解决方案,我使用Xamarin.Forms.3.4.0.1008975

ShowDetailPageAsyncswitch之后的方法中添加一些代码(这段代码来自https://github.com/MvvmCross/MvvmCross/issues/2995)

if (Application.Current.MainPage is MasterDetailPage masterDetailPage)
{
    masterDetailPage.IsPresented = false;
}
else if (Application.Current.MainPage is NavigationPage navigationPage
         && navigationPage.CurrentPage is MasterDetailPage nestedMasterDetail)
{
    nestedMasterDetail.IsPresented = false;
}

IsPresented用于控制是否显示母版页。要隐藏MasterPage,请将其设置为false有关更多详细信息,请在此处阅读:创建和显示详细信息页面

启动所有三个平台的应用程序,以确保菜单正常工作。

设置数据绑定的其他方法

使用TextCell的固有命令

XAML世界的数据绑定机制是灵活的。实际上,我们有多种方法来实现我们的目标。例如,如果您仅用TextCell显示菜单项,则可以使用简单的方法进行导航。正如我在上一节中所说,TextCell原生支持ICommand所以我们可以使用这样的数据绑定语法:

<DataTemplate>
    <TextCell Text="{Binding}" Command="{Binding BindingContext.DataContext.ShowDetailPageAsyncCommand, Source={x:Reference MainContent}}" CommandParameter="{Binding}"></TextCell>
</DataTemplate>

这种方式更容易。当用户点击时TextCell,它会触发Command但缺点是您无法自定义菜单项的UI。TextCell只支持文字。如果要添加一些图像或定义复杂的项目布局,则必须使用ViewCell

使用Bahaviors

此外,您可能认为我们可以使用ItemSelectedItemTapped事件。当然,我们可以!但不幸的是,这些事件没有实现ICommand接口,所以我们不能直接使用数据绑定。要使用ICommand绑定,我们需要使用a Behavior将事件转换为命令,如下所述:可重用的EventToCommandBehavior

您可能不熟悉行为。行为来自Blend SDK,它是XAML世界中非常有用的库。可以将这些行为附加到某些控件并侦听某些事件,然后在ViewModel中调用某些命令。这是为那些未设计为与Command交互的控件添加Command模式支持的好方法因此,我们可以优雅地使用MVVM模式,而不是在代码隐藏文件中使用事件处理程序。

您可以按照官方文档中的说明创建您的EventToCommandBehavior,但我们可以利用第三方库快速完成:Behaviors.Xamarin.Forms.Netstandard它不是官方项目,但易于使用。您可以通过搜索Behaviors.Xamarin.FormsNuGet包管理器将其安装到MvxFormsMasterDetailApp.UI项目

 

我们可以使用此库来使ListView控件在选择项目时在ViewModel中触发我们的命令。为此,MenuViewModel.cs文件中添加叫做SelectedMenuItem的可绑定属性,该属性用于指示当前所选项,如下所示:

#region SelectedMenuItem;
private string _selectedMenuItem;
public string SelectedMenuItem
{
    get => _selectedMenuItem;
    set => SetProperty(ref _selectedMenuItem, value);
}
#endregion

ShowDetailPageAsyncCommand下面的代码替换我们在上一节中创建区域:

#region ShowDetailPageAsyncCommand;
        private IMvxAsyncCommand _showDetailPageAsyncCommand;
        public IMvxAsyncCommand ShowDetailPageAsyncCommand
        {
            get
            {
                _showDetailPageAsyncCommand = _showDetailPageAsyncCommand ?? new MvxAsyncCommand(ShowDetailPageAsync);
                return _showDetailPageAsyncCommand;
            }
        }
        private async Task ShowDetailPageAsync()
        {
            // Implement your logic here.
            switch (SelectedMenuItem)
            {
                case "Contacts":
                    await _navigationService.Navigate<ContactsViewModel>();
                    break;
                case "Todo":
                    await _navigationService.Navigate<TodoViewModel>();
                    break;
                default:
                    break;
            }
            if (Application.Current.MainPage is MasterDetailPage masterDetailPage)
            {
                masterDetailPage.IsPresented = false;
            }
            else if (Application.Current.MainPage is NavigationPage navigationPage
                     && navigationPage.CurrentPage is MasterDetailPage nestedMasterDetail)
            {
                nestedMasterDetail.IsPresented = false;
            }
        }
        #endregion

你找到了区别吗?我从命令中删除了参数,并ShowDetailPageAsync方法中使用了SelectedMenuItem属性 接下来,我们需要为ListView的SelectedItem设置数据绑定在MvxFormsMasterDetailApp.UI项目的Pages文件夹中打开MenuPage.xaml文件,删除当前ListView控件,然后添加一个新文件ListView,如下所示:

<ListView x:Name="MenuList" ItemsSource="{Binding MenuItemList}" 
          SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding}"></TextCell>
            </DataTemplate>
        </ListView.ItemTemplate>
</ListView>

通过以下代码SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}",我们可以在ViewModel 中设置ListView的SelectedItem与SelectedMenuItem属性之间的双向数据绑定

在views:MvxContentPage定义中导入Behavior命名空间xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"现在我们可以使用behaviors前缀来使用库中的行为。更新ListView如下所示的XMAL 

<ListView x:Name="MenuList" ItemsSource="{Binding MenuItemList}" 
          SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}">
    <ListView.Behaviors>
        <behaviors:EventHandlerBehavior EventName="ItemSelected">
            <behaviors:InvokeCommandAction 
                Command="{Binding BindingContext.DataContext.ShowDetailPageAsyncCommand, 
                Source={x:Reference MainContent}}"></behaviors:InvokeCommandAction>
            </behaviors:EventHandlerBehavior>
    </ListView.Behaviors>
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding}"></TextCell>
            </DataTemplate>
        </ListView.ItemTemplate>
</ListView>

ListView控件放置了一个Behaviors部分有一个被调用的行为EventHandlerBehavior,它将被ItemSelected事件触发在Behaviors中,有一个InvokeCommandAction,它将调用ViewModel中的ShowDetailPageAsyncCommand请注意数据绑定语法。我们需要指定绑定SourcePath绑定。如果你只是使用{Binding ShowDetailPageAsyncCommand}它,它将无法正常工作。所以要小心当前控件的BindingContext

运行三个平台的应用程序,您将看到它按预期工作。您可以选择任何方法来实现菜单功能。我只想告诉你如何以不同的方式做到这一点。也许你会将它们用于其他场景。

微调UI

不同平台的UI存在一些缺陷。例如,iOS的页眉和汉堡菜单图标不如我们预期的那么好。让我们解决它们。

添加iOS的汉堡包图标

根据MasterDetailPage的官方文档,我认为iOS也应该显示像Android和UWP这样的按钮,但事实并非如此。我们可以Icon为MasterPage 设置属性。

此处下载图像文件将其粘贴到MvxFormsMasterDetailDemo.iOS项目的Resources文件夹中。如果没有这样的文件夹,请创建一个。图像的Build Action属性应该是BundleResource

在MvxFormsMasterDetailApp.UI项目的Pages文件夹中打开MenuPage.xaml文件将以下代码添加到以下views:MvxContentPage部分:Icon="hamburger.png"现在启动iOS应用程序:

 

那很好!

为Android和iOS添加标题栏

UWP将为MasterPage添加默认标题栏。对于Android和iOS,我们需要分别定义它。

为了为不同的平台提供一些特定的值,我们可以使用Device类,该类包含许多属性和方法,可以帮助我们自定义特定平台的布局和功能。您可以在此处阅读有关它的详细信息:Xamarin.Forms设备类

根据我们的要求,我们只需要为Android和iOS添加标题栏。MvxFormsMasterDetailDemo.UI项目的Pages文件夹中打开MenuPage.xaml文件ListView定义之前添加以下代码

<StackLayout HeightRequest="40">
    <StackLayout.IsVisible>
        <OnPlatform x:TypeArguments="x:Boolean">
            <On Platform="Android, iOS" Value="True" />
            <On Platform="UWP" Value="False" />
        </OnPlatform>
    </StackLayout.IsVisible>
    <Label Text="My HamburgerMenu Demo" Margin="10" VerticalOptions="Center" FontSize="Large"></Label>
</StackLayout>

实际上,OnPlatform标记正在做一些类似switch在代码中创建语句的东西它包含几个On类型来接收Platform属性,表示当前平台。有一些不同的值,以确定不同的平台:iOSAndroidUWPmacOS因此,对Android和iOS来说,我们可以通过设置其属性来创建一个包含Label控件StackLayout并设置其其IsVisible属性以显示应用名称但对于UWP来说,它是看不见的。这意味着添加代码不会对UWP进行任何更改。

运行适用于Android和iOS的应用。它适用于Android。但在iOS平台上,标题栏稍微覆盖了手机的状态栏,如下所示:

 

我们可以为StackLayout添加一些Margin为iOS 添加另一个OnPlatform标记,如下所示:

<StackLayout.Margin>
    <OnPlatform x:TypeArguments="Thickness">
        <On Platform="iOS" Value="0,20,0,0" />
    </OnPlatform>
</StackLayout.Margin>

它只影响iOS的UI。现在来看看所有平台:

iOS版:

 

安卓:

 

UWP:

 

好的,一切都很好,除了UWP的列表项高度......

调整UWP项目的高度

您可能会注意到,如果我们将其TextCell用作ListView的项模板,则Android和iOS的ListView的项都有默认边距和样式但对于UWP平台,项没有默认样式和适当高度。让我们定义项目模板的样式。同时,我们应该确保它适用于每个平台。

打开MvxFormsMasterDetailDemo.UI项目中的Pages文件夹中MenuPage.xaml文件。通过以下代码更新ItemTemplate

<ListView.ItemTemplate>
    <DataTemplate>
        <ViewCell>
            <StackLayout HeightRequest="50">
                <Label Text="{Binding}" Margin="20,0,0,0" 
                       VerticalOptions="CenterAndExpand"></Label>
            </StackLayout>
        </ViewCell>
    </DataTemplate>
</ListView.ItemTemplate>

这就对了。最后,整个文件看起来像这样:

<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
             xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
             x:Class="MvxFormsMasterDetailDemo.UI.Pages.MenuPage"
             x:TypeArguments="viewModels:MenuViewModel" 
             x:Name="MainContent"
             xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
             Icon="hamburger.png">
    <ContentPage.Content>
        <StackLayout>
            <StackLayout HeightRequest="40">
                <StackLayout.IsVisible>
                    <OnPlatform x:TypeArguments="x:Boolean">
                        <On Platform="Android, iOS" Value="True" />
                        <On Platform="UWP" Value="False" />
                    </OnPlatform>
                </StackLayout.IsVisible>
                <StackLayout.Margin>
                    <OnPlatform x:TypeArguments="Thickness">
                        <On Platform="iOS" Value="0,20,0,0" />
                    </OnPlatform>
                </StackLayout.Margin>
                <Label Text="HamburgerMenu Demo" Margin="10" VerticalOptions="Center" FontSize="Large"></Label>
            </StackLayout>
            <ListView x:Name="MenuList" ItemsSource="{Binding MenuItemList}" 
                      SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}">
                <ListView.Behaviors>
                    <behaviors:EventHandlerBehavior EventName="ItemSelected">
                        <behaviors:InvokeCommandAction 
                            Command="{Binding BindingContext.DataContext.ShowDetailPageAsyncCommand, 
                            Source={x:Reference MainContent}}"></behaviors:InvokeCommandAction>
                        </behaviors:EventHandlerBehavior>
                </ListView.Behaviors>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <StackLayout HeightRequest="50">
                                <Label Text="{Binding}" Margin="20,0,0,0" 
                                       VerticalOptions="CenterAndExpand"></Label>
                            </StackLayout>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
    </ContentPage.Content>
</views:MvxContentPage>

现在是时候为所有三个平台启动应用程序并观察最终结果!现在,该应用程序显示三个平台的正确汉堡菜单,它具有适当的边距和样式。

小结

在本文中,我向您展示了如何通过Xamarin.Forms和MvvmCross Framework为iOS,Android和UWP创建基本的汉堡菜单布局。我不是专业设计师,因此您可能需要为自己的应用程序微调样式。我希望您可以按照这些步骤创建一个干净,优雅的MVVM架构的汉堡菜单布局。另外,我希望你能从我的演示中获得数据绑定基础知识。请记住,实现相同目标可能有多种方法,而我的实施并不是最好的方法。实际上,我认为为每个项目添加一个图标会更好!如果您找到更好的解决方案,请留下评论并在下面进行讨论。

你可以在我的GitHub上找到repo:MvxFormsMasterDetailDemoHappy Coding!

 

posted @ 2019-01-09 10:22 yan_xiaodi 阅读(...) 评论(...) 编辑 收藏