使用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文件夹,然后选择Add
- New Item
。Content 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:TypeArguments
值viewModels:MasterDetailViewModel
。不要忘记通过添加以下代码导入viewModels命名空间:xmlns:viewModels="clr-namespace:MvxFormsMasterDetailDemo.Core.ViewModels;assembly=MvxFormsMasterDetailDemo.Core"
。
打开MasterDetailPage.xaml.cs
文件,将其基类替换ContentPage
为MvxMasterDetailPage<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
属性。有一些非常重要的属性MvxMasterDetailPagePresentation
。Position
是一个枚举值,用于指示页面的类型,在此处设置为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用来
存储一些菜单项的属性。为简单起见,只有两个字符串:Contacts
和Todo
。我们还需要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.xaml
和TodoPage.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.xaml
和TodoPage.xaml.cs做同样的更改
。不要忘记更新Label控件的Text以显示页面名称。
实现菜单功能
现在,我们有我们需要显示所有的页面:根页叫MasterDetailPage
,一个MasterPage叫做MenuPage
和两个DetailPages叫做ContactsPage
和TodoPage
。接下来,我们需要使菜单正常工作。
显示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的布局,只需设置MasterBehavior
MasterDetailPage 的属性即可。它是一个枚举值,用于确定详细信息页面在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
,因为我们知道MenuItemList
is中的对象是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
,比如Button
,MenuItem
,TextCell
和一些继承自它们的类。并且,SearchBar
支持SearchCommand,
实际上也是一种ICommand
类型的属性。ListView的
也是RefreshCommand
属性ICommand
接口的实例。
对于那些不直接支持ICommand的控件,Xamarin.Forms提供了一个TapGestureRecognizer来
支持Command
绑定。有关详细信息,请阅读以下文章:添加点按手势识别器。请记住,虽然GestureRecognizer
支持多种手势,如pinch
,pan
和swipe
,但只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
替换默认的TextCell
。ViewCell
为我们提供了更多自定义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
。
ShowDetailPageAsync
在switch
段之后的方法中添加一些代码(这段代码来自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
此外,您可能认为我们可以使用ItemSelected
或ItemTapped
事件。当然,我们可以!但不幸的是,这些事件没有实现ICommand
接口,所以我们不能直接使用数据绑定。要使用ICommand
绑定,我们需要使用a Behavior
将事件转换为命令,如下所述:可重用的EventToCommandBehavior。
您可能不熟悉行为。行为来自Blend SDK,它是XAML世界中非常有用的库。可以将这些行为附加到某些控件并侦听某些事件,然后在ViewModel中调用某些命令。这是为那些未设计为与Command交互的控件添加Command模式支持的好方法。因此,我们可以优雅地使用MVVM模式,而不是在代码隐藏文件中使用事件处理程序。
您可以按照官方文档中的说明创建您的EventToCommandBehavior
,但我们可以利用第三方库快速完成:Behaviors.Xamarin.Forms.Netstandard。它不是官方项目,但易于使用。您可以通过搜索Behaviors.Xamarin.Forms
NuGet包管理器将其安装到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。请注意数据绑定语法。我们需要指定绑定Source
和Path
绑定。如果你只是使用{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
属性,表示当前平台。有一些不同的值,以确定不同的平台:iOS
,Android
,UWP
和macOS
。因此,对Android和iOS来说,我们可以通过设置其属性来创建一个包含Label
控件StackLayout并设置其其
以显示应用名称。但对于UWP来说,它是看不见的。这意味着添加代码不会对UWP进行任何更改。IsVisible属性
运行适用于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:MvxFormsMasterDetailDemo。Happy Coding!