在Win10之前,WP平台的App主要有枢轴和全景两种导航模式,我个人更喜欢Pivot即枢轴模式,可以左右切换,非常方便。全景视图因为对设计要求比较高,自己总是做不出好的效果。对于一般的新闻阅读类App来说,Pivot更适合多个频道的展示,因为内容基本都是一样的。
到了Win10,微软模仿其他平台也推出了汉堡菜单,但并没有提供现成的控件,而是需要开发者通过一个名为SplitView的控件来实现。我个人并不觉得左上角的菜单有多么方便,汉堡菜单的使用必然会改变以前的导航模式,比如以前底部的AppBar使用很频繁,现在可以通过汉堡菜单的按钮来切换不同的页面。因此之前的App的导航模式需要重新设计。
假设有A、B、C三个平行的页面,可以在每个页面的左侧都放个汉堡菜单,也可以像web的框架页一样,做一个壳,汉堡菜单只放在外面的框架里,点击不同的按钮,在content里实现不同页面的导航。我比较倾向第二种,之前在做澎湃新闻uwp的时候就使用了这种方式,后来看了下Template10的模板,也是用的这种方式,在主页面外层套了一个Frame,而且还实现 了一个汉堡菜单控件。有兴趣的同学可以参考Template10来快速生成一个带汉堡菜单的基础App,Github地址: https://github.com/Windows-XAML/Template10 ,这个项目还带了很多好东西,比如一些常用的帮助类和一些behavior等,值得uwp开发者好好学习。
我没有直接使用T10的模板,以下介绍的还是当时使用MVVM-Sidekick框架实现的页面内导航。
首先通过MVVM-Sidekick提供的项目模板来新建一个UWP项目,命名为NavDemo。
考虑我们要实现的目的:在主页面放置一个汉堡菜单,在右侧的content中实现不同页面的导航。
先来看一下效果:
PC版:
手机版:
汉堡菜单每个选项一般是由一个图标和一个文字组成,我还是使用FontAwesomeFont这个字体来显示图标,如何使用这个字体来做图标,可参考我之前的blog。首先建立一个菜单的类 NavMenuItem ,放在Models目录下,使用provm代码段生成两个属性:
public class NavMenuItem : BindableBase < NavMenuItem >
{
/// <summary>
/// FontAwesomeFontFamily
/// </summary>
public string Glyph
{
get { return _GlyphLocator( this ).Value; }
set { _GlyphLocator( this ).SetValueAndTryNotify( value ); }
}
#region Property string Glyph Setup
protected Property < string > _Glyph = new Property < string > { LocatorFunc = _GlyphLocator };
static Func < BindableBase , ValueContainer < string >> _GlyphLocator = RegisterContainerLocator< string >( "Glyph" , model => model.Initialize( "Glyph" , ref model._Glyph, ref _GlyphLocator, _GlyphDefaultValueFactory));
static Func < string > _GlyphDefaultValueFactory = () => { return default ( string ); };
#endregion
/// <summary>
/// 文字
/// </summary>
public string Label
{
get { return _LabelLocator( this ).Value; }
set { _LabelLocator( this ).SetValueAndTryNotify( value ); }
}
#region Property string Label Setup
protected Property < string > _Label = new Property < string > { LocatorFunc = _LabelLocator };
static Func < BindableBase , ValueContainer < string >> _LabelLocator = RegisterContainerLocator< string >( "Label" , model => model.Initialize( "Label" , ref model._Label, ref _LabelLocator, _LabelDefaultValueFactory));
static Func < string > _LabelDefaultValueFactory = () => { return default ( string ); };
#endregion
}
打开NavDemo/ViewModels/MainPage_Model.cs,使用propvm代码段生成一个列表:
public ObservableCollection < NavMenuItem > NavMenuItemList
{
get { return _NavMenuItemListLocator( this ).Value; }
set { _NavMenuItemListLocator( this ).SetValueAndTryNotify( value ); }
}
#region Property ObservableCollection<HamburgerMenuItem> NavMenuItemList Setup
protected Property < ObservableCollection < NavMenuItem >> _NavMenuItemList = new Property < ObservableCollection < NavMenuItem >> { LocatorFunc = _NavMenuItemListLocator };
static Func < BindableBase , ValueContainer < ObservableCollection < NavMenuItem >>> _NavMenuItemListLocator = RegisterContainerLocator< ObservableCollection < NavMenuItem >>( "NavMenuItemList" , model => model.Initialize( "NavMenuItemList" , ref model._NavMenuItemList, ref _NavMenuItemListLocator, _NavMenuItemListDefaultValueFactory));
static Func < ObservableCollection < NavMenuItem >> _NavMenuItemListDefaultValueFactory = () => default ( ObservableCollection < NavMenuItem >);
#endregion
在vm的构造函数里,添加几个项:
public MainPage_Model()
{
if (IsInDesignMode )
{
Title = "Title is a little different in Design mode" ;
}
NavMenuItemList = new ObservableCollection < NavMenuItem >();
NavMenuItemList.Add( new NavMenuItem { Glyph = "/uf015" , Label = " 首页 " });
NavMenuItemList.Add( new NavMenuItem { Glyph = "/uf002" , Label = " 搜索 " });
NavMenuItemList.Add( new NavMenuItem { Glyph = "/uf05a" , Label = " 关于 " });
}
注意Glyph的赋值方式。
在项目中新建Resources目录,把FontAwesome.otf字体文件放在里面。在项目中新建CustomTheme目录,然后建立自定义的样式资源文件CustomStyles.xaml,代码如下:
< ResourceDictionary
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : local ="using:NavDemo">
< FontFamily x : Key ="FontAwesomeFontFamily"> /Resources/FontAwesome.otf#FontAwesome </ FontFamily >
< Style x : Key ="SplitViewTogglePaneButtonStyle" TargetType ="ToggleButton">
< Setter Property ="FontSize" Value ="20" />
< Setter Property ="FontFamily" Value ="{ ThemeResource SymbolThemeFontFamily }" />
< Setter Property ="MinHeight" Value ="48" />
< Setter Property ="MinWidth" Value ="48" />
< Setter Property ="Margin" Value ="0" />
< Setter Property ="Padding" Value ="0" />
< Setter Property ="HorizontalAlignment" Value ="Left" />
< Setter Property ="VerticalAlignment" Value ="Top" />
< Setter Property ="HorizontalContentAlignment" Value ="Center" />
< Setter Property ="VerticalContentAlignment" Value ="Center" />
< Setter Property ="Background" Value ="Transparent" />
< Setter Property ="Foreground" Value ="{ ThemeResource SystemControlForegroundBaseHighBrush }" />
< Setter Property ="Content" Value ="" />
< Setter Property ="AutomationProperties.Name" Value ="Menu" />
< Setter Property ="UseSystemFocusVisuals" Value ="True"/>
< Setter Property ="Template">
< Setter.Value >
< ControlTemplate TargetType ="ToggleButton">
< Grid Background ="{ TemplateBinding Background }" x : Name ="LayoutRoot">
< VisualStateManager.VisualStateGroups >
< VisualStateGroup x : Name ="CommonStates">
< VisualState x : Name ="Normal" />
< VisualState x : Name ="PointerOver">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="LayoutRoot" Storyboard.TargetProperty ="(Grid.Background)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightListLowBrush }"/>
</ ObjectAnimationUsingKeyFrames >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="Foreground">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightAltBaseHighBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
< VisualState x : Name ="Pressed">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="LayoutRoot" Storyboard.TargetProperty ="(Grid.Background)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightListMediumBrush }"/>
</ ObjectAnimationUsingKeyFrames >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="Foreground">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightAltBaseHighBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
< VisualState x : Name ="Disabled">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="(TextBlock.Foreground)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlDisabledBaseLowBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
< VisualState x : Name ="Checked"/>
< VisualState x : Name ="CheckedPointerOver">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="LayoutRoot" Storyboard.TargetProperty ="(Grid.Background)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightListLowBrush }"/>
</ ObjectAnimationUsingKeyFrames >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="Foreground">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightAltBaseHighBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
< VisualState x : Name ="CheckedPressed">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="LayoutRoot" Storyboard.TargetProperty ="(Grid.Background)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightListMediumBrush }"/>
</ ObjectAnimationUsingKeyFrames >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="Foreground">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlHighlightAltBaseHighBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
< VisualState x : Name ="CheckedDisabled">
< Storyboard >
< ObjectAnimationUsingKeyFrames Storyboard.TargetName ="ContentPresenter" Storyboard.TargetProperty ="(TextBlock.Foreground)">
< DiscreteObjectKeyFrame KeyTime ="0" Value ="{ ThemeResource SystemControlDisabledBaseLowBrush }"/>
</ ObjectAnimationUsingKeyFrames >
</ Storyboard >
</ VisualState >
</ VisualStateGroup >
</ VisualStateManager.VisualStateGroups >
< ContentPresenter x : Name ="ContentPresenter"
Content ="{ TemplateBinding Content }"
Margin ="{ TemplateBinding Padding }"
HorizontalAlignment ="{ TemplateBinding HorizontalContentAlignment }"
VerticalAlignment ="{ TemplateBinding VerticalContentAlignment }"
AutomationProperties.AccessibilityView ="Raw" />
</ Grid >
</ ControlTemplate >
</ Setter.Value >
</ Setter >
</ Style >
</ ResourceDictionary >
然后打开App.xaml文件,把这个资源引用进来:
< Application.Resources >
< ResourceDictionary >
< ResourceDictionary.MergedDictionaries >
< ResourceDictionary Source ="CustomTheme/CustomStyles.xaml"/>
</ ResourceDictionary.MergedDictionaries >
</ ResourceDictionary >
</ Application.Resources >
样式资源文件里主要定义了两个样式,一是定义了FontAwesomeFontFamily字体,二是定义了一个针对ToggleButton的按钮样式SplitViewTogglePaneButtonStyle,作为汉堡菜单的开关。这个开关键为什么要设置高度为48呢?参考 https://msdn.microsoft.com/zh-cn/library/windows/apps/dn997787.aspx
拆分视图控件具有一个可展开/可折叠的窗格和一个内容区域。内容区域始终可见。窗格可以展开和折叠或停留在打开状态,而且可以从应用窗口的左侧或右侧显示其自身。窗格中有三种模式:
覆盖
在打开之前隐藏窗格。在打开时,窗格覆盖内容区域。
内联
窗格始终可见,并且不会覆盖内容区域。窗格和内容区域划分可用的屏幕实际使用面积。
精简
在此模式下窗格始终可见,它仅足够宽以显示图标(通常 48 epx 宽)。 窗格和内容区域划分可用的屏幕实际使用面积。尽管标准精简模式不覆盖内容区域,但它可以转化为更宽的窗格来显示更多内容,这将覆盖该内容区域。
所以我就根据官方文档设置为48了。
修改MainPage.xaml,把根Grid改为以下代码:
< Grid Background ="{ ThemeResource ApplicationPageBackgroundThemeBrush }" DataContext ="{ StaticResource DesignVM }">
<!-- Top-level navigation menu + app content -->
< SplitView x : Name ="RootSplitView" IsPaneOpen ="True"
DisplayMode ="Inline"
OpenPaneLength ="256"
IsTabStop ="False">
< SplitView.Pane >
<!-- A custom ListView to display the items in the pane. The automation Name is set in the ContainerContentChanging event. -->
< ListView ItemsSource ="{ Binding NavMenuItemList }">
</ ListView >
</ SplitView.Pane >
< SplitView.Content >
< Frame x : Name ="mainFrame">
</ Frame >
</ SplitView.Content >
</ SplitView >
<!-- Declared last to have it rendered above everything else, but it needs to be the first item in the tab sequence. -->
< ToggleButton x : Name ="TogglePaneButton"
TabIndex ="1"
Style ="{ StaticResource SplitViewTogglePaneButtonStyle }"
IsChecked ="{ Binding IsPaneOpen , ElementName =RootSplitView, Mode =TwoWay}"
AutomationProperties.Name ="Menu"
ToolTipService.ToolTip ="Menu" />
</ Grid >
为了方便查看菜单展开的效果,暂时先把IsPaneOpen属性设置为true,OpenPaneLength设置的是菜单展开后的宽度。在Pane里放一个ListView,ItemSource绑定到之前做好的NavMenuItemList上。SplitView的Content设置为一个Frame,用来展示右侧的页面。
注意,如果当SplitView的Content直接设置为Frame的时候,也就是把外层的 < SplitView.Content > 去掉 后,会报一个错:
这个错误可以不用理会,程序是可以正常运行的。
此外 还要有一个按钮来控制菜单的展开关闭状态,用一个ToggleButton来实现,这个按钮的图标一般是三个横杠,设置其Style为SplitViewTogglePaneButtonStyle即可。
然后,还要设置ListView的项模板,可以使用Blend来设计项模板,但因为这个比较简单,我就直接手写了,在Resources目录下添加一个资源文件CustomDataTemplates.xaml,项目所有的自定义模板都可以写在这里,代码如下:
< ResourceDictionary
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns : x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns : Interactivity ="using:Microsoft.Xaml.Interactivity"
xmlns : Core ="using:Microsoft.Xaml.Interactions.Core"
xmlns : Behaviors ="using:MVVMSidekick.Behaviors">
< DataTemplate x : Key ="NavMenuItemTemplate" >
< Grid >
< Grid.ColumnDefinitions >
< ColumnDefinition MinWidth ="48" />
< ColumnDefinition />
</ Grid.ColumnDefinitions >
< FontIcon x : Name ="Glyph" FontFamily ="{ StaticResource FontAwesomeFontFamily }" FontSize ="16" Margin ="0" Glyph ="{ Binding Glyph }" VerticalAlignment ="Center" HorizontalAlignment ="Center" ToolTipService.ToolTip ="{ Binding Label }"/>
< TextBlock x : Name ="Text" Grid.Column ="1" Text ="{ Binding Label }" VerticalAlignment ="Center"/>
</ Grid >
</ DataTemplate >
</ ResourceDictionary >
在这里定义一个项模板NavMenuItemTemplate,在里面放一个FontIcon,把Glyph属性绑定到NavMenuItem的Glyph属性,当然不要忘了把FontFamily设置为我们在自定义样式里定义好的FontAwesomeFontFamily,不然是不会生效的。
再把这个项模板应用到页面的ListView控件上:
ItemTemplate ="{ StaticResource NavMenuItemTemplate }"
现在跑一下试试,报错了:
原来忘了把刚才的模板文件引入进来,修改App.xaml,修改为以下的样子:
< Application.Resources >
< ResourceDictionary >
< ResourceDictionary.MergedDictionaries >
< ResourceDictionary Source ="CustomTheme/CustomStyles.xaml"/>
< ResourceDictionary Source ="Resources/CustomDataTemplates.xaml" />
</ ResourceDictionary.MergedDictionaries >
</ ResourceDictionary >
</ Application.Resources >
现在可以运行了:
貌似左上角的按钮跟ListView重叠了,这样可不好看。
左上角的按钮应用了SplitViewTogglePaneButtonStyle样式,最小高度为48,把ListView往下移动一点,添加一个Margin属性,顶部把开关按钮的空间空出来:
< ListView Margin ="0,48,0,0" ItemsSource ="{ Binding NavMenuItemList }"
ItemTemplate ="{ StaticResource NavMenuItemTemplate }">
现在列表位置正常了,但图标的位置貌似还是偏右了,那就再给ListView设置ItemContainerStyle样式,在CustomStyles.xaml文件里添加以下代码:
< Style x : Key ="NavMenuItemContainerStyle" TargetType ="ListViewItem">
< Setter Property ="MinWidth" Value ="{ StaticResource SplitViewCompactPaneThemeLength }"/>
< Setter Property ="Height" Value ="48"/>
< Setter Property ="Padding" Value ="0"/>
</ Style >
ListView应用此样式:
< ListView Margin ="0,48,0,0" ItemsSource ="{ Binding NavMenuItemList }"
ItemTemplate ="{ StaticResource NavMenuItemTemplate }"
ItemContainerStyle ="{ StaticResource NavMenuItemContainerStyle }">
</ ListView >
再跑一下:
现在样式正常了。
现在MainPage.xaml只是一个壳,右侧内容是空的,下面来添加几个页面。在项目里添加几个页面,比如可以命名为HomePage、SearchPage、AboutPage等:
因为每个页面里已经默认添加了一个TextBlock,并且绑定到了vm的Title属性,这个属性默认取值就是当前页面的Name,所以我们就不用改了,知道当前页面是哪个就行了。
现在的问题是,如何在MainPage载入时,自动在SplitView的Content里显示HomePage呢?
这就需要用到MVVM-Sidekick的一个Behavior了,用Blend打开项目,找到行为:
有一个叫做BaeconBehavior的行为,把它拖到……咦,怎么找不到Content呢?
那就直接手写吧,把Frame部分的代码改成这样:
< SplitView.Content >
< Frame x : Name ="mainFrame" mvvm : StageManager.Beacon ="frameMain" x : FieldModifier ="public">
</ Frame >
</ SplitView.Content >
StageManager.Beacon属性是用来标识StageManager,MVVM-Sidekick已经把导航的功能封装到了StageManager里,以前我们一般使用this.StageManager.DefaultStage.Show(xxx)的方式来使用,即可实现整个页面的导航,如果要实现页面内某个区域的导航,就需要手动指定是哪个StageManager了,这就需要使用以下属性来标识某个区域:
mvvm : StageManager.Beacon ="frameMain"
找到OnBindedViewLoad方法,取消默认的注释,将该方法改为以下的样子:
protected override async Task OnBindedViewLoad(MVVMSidekick.Views. IView view)
{
await base .OnBindedViewLoad(view);
await StageManager[ "frameMain" ].Show( new HomePage_Model ());
}
这里要注意,一定要等Bind完成后再Show,不然会显示不出来哦,因为要将整个页面Bind完后,才可以进行后续的动作。
跑一下看看:
很好,默认转到HomePage页了。
现在可以处理菜单部分的导航了,点击不同的项导航到不同的页面。看到这里应该也有个大概了,处理不同项的点击事件,将名为frameMain的StageManager使用Show方法展示不同的ViewModel即可。
使用ItemClick事件吗?No,还记得我之前提过的SendToEventRouterAction吗?如果不熟悉的话就翻翻我之前的blog吧,这里我还是用这个Action来实现。
修改项模板为:
< DataTemplate x : Key ="NavMenuItemTemplate" >
< Grid >
< Interactivity : Interaction.Behaviors >
< Core : EventTriggerBehavior EventName ="Tapped">
< Behaviors : SendToEventRouterAction IsEventFiringToAllBaseClassesChannels ="True" EventRoutingName ="NavToPage" EventData ="{ Binding }" />
</ Core : EventTriggerBehavior >
</ Interactivity : Interaction.Behaviors >
< Grid.ColumnDefinitions >
< ColumnDefinition MinWidth ="48" />
< ColumnDefinition />
</ Grid.ColumnDefinitions >
< FontIcon x : Name ="Glyph" FontFamily ="{ StaticResource FontAwesomeFontFamily }" FontSize ="16" Margin ="0" Glyph ="{ Binding Glyph }" VerticalAlignment ="Center" HorizontalAlignment ="Center" ToolTipService.ToolTip ="{ Binding Label }"/>
< TextBlock x : Name ="Text" Grid.Column ="1" Text ="{ Binding Label }" VerticalAlignment ="Center"/>
</ Grid >
</ DataTemplate >
然后在MainPage_Model.cs文件中,添加一个方法:
private void RegisterCommand()
{
// 一般列表项点击事件
MVVMSidekick.EventRouting. EventRouter .Instance.GetEventChannel< Object >()
.Where(x => x.EventName == "NavToPage" )
.Subscribe(
async e =>
{
NavMenuItem item = e.EventData as NavMenuItem ;
if (item != null )
{
switch (item.Label)
{
case " 首页 " :
await StageManager[ "frameMain" ].Show( new HomePage_Model ());
break ;
case " 搜索 " :
await StageManager[ "frameMain" ].Show( new SearchPage_Model ());
break ;
case " 关于 " :
await StageManager[ "frameMain" ].Show( new AboutPage_Model ());
break ;
default :
break ;
}
}
}
).DisposeWith( this );
}
别忘了在OnBindedViewLoad方法里调用一下:
private bool isLoaded;
/// <summary>
/// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property
/// </summary>
/// <param name=" view "> View that firing Load event </param>
/// <returns> Task awaiter </returns>
protected override async Task OnBindedViewLoad(MVVMSidekick.Views. IView view)
{
if (!isLoaded)
{
this .RegisterCommand();
this .isLoaded = true ;
}
await base .OnBindedViewLoad(view);
await StageManager[ "frameMain" ].Show( new HomePage_Model ());
}
添加一个isLoaded属性是避免重复调用。
跑一下看看,咦,有时候好用,有时候不好用,点击图标和文字的时候好用,点击不到图标和文字就不好用,这是什么原因?
熟悉ListView的同学可能会想到,ListViewItem默认是没有横向撑满的,所以虽然点击了项,但因为项模板里的Grid没有横向撑满,所以并没有触发Grid的Tapped事件,那我们可以设置ListItemStyle,让ListViewItem都横向撑满。在NavMenuItemContainerStyle里添加以下代码:
< Setter Property ="HorizontalContentAlignment" Value ="Stretch"/>
< Setter Property ="VerticalContentAlignment" Value ="Stretch"/>
这样就可以横向纵向撑满了,再跑下:
又乱套了,再改哪里呢,修改项模板 NavMenuItemTemplate ,设置左侧列宽为Auto:
< DataTemplate x : Key ="NavMenuItemTemplate" >
< Grid >
< Interactivity : Interaction.Behaviors >
< Core : EventTriggerBehavior EventName ="Tapped">
< Behaviors : SendToEventRouterAction IsEventFiringToAllBaseClassesChannels ="True" EventRoutingName ="NavToPage" EventData ="{ Binding }" />
</ Core : EventTriggerBehavior >
</ Interactivity : Interaction.Behaviors >
< Grid.ColumnDefinitions >
< ColumnDefinition MinWidth ="48" Width ="Auto" />
< ColumnDefinition />
</ Grid.ColumnDefinitions >
< FontIcon x : Name ="Glyph" FontFamily ="{ StaticResource FontAwesomeFontFamily }" FontSize ="16" Margin ="0" Glyph ="{ Binding Glyph }" VerticalAlignment ="Center" HorizontalAlignment ="Center" ToolTipService.ToolTip ="{ Binding Label }"/>
< TextBlock x : Name ="Text" Grid.Column ="1" Text ="{ Binding Label }" VerticalAlignment ="Center" />
</ Grid >
</ DataTemplate >
再运行一下:
现在正常了。
看一下手机上的样子:
使用了一下感觉还是有点细节需要改进,比如菜单弹出后,点击项后应该让菜单自动缩回去,现在改一下吧。
在MainPage的vm里添加一个属性:
/// <summary>
/// 是否展开菜单
/// </summary>
public bool IsPaneOpen
{
get { return _IsPaneOpenLocator( this ).Value; }
set { _IsPaneOpenLocator( this ).SetValueAndTryNotify( value ); }
}
#region Property bool IsPaneOpen Setup
protected Property < bool > _IsPaneOpen = new Property < bool > { LocatorFunc = _IsPaneOpenLocator };
static Func < BindableBase , ValueContainer < bool >> _IsPaneOpenLocator = RegisterContainerLocator< bool >( "IsPaneOpen" , model => model.Initialize( "IsPaneOpen" , ref model._IsPaneOpen, ref _IsPaneOpenLocator, _IsPaneOpenDefaultValueFactory));
static Func < bool > _IsPaneOpenDefaultValueFactory = () => default ( bool );
#endregion
在vm的构造函数里将此值设置为false,默认为关闭。
然后将SplitView的IsPaneOpen属性绑定到上面:
< SplitView x : Name ="RootSplitView" IsPaneOpen ="{ Binding IsPaneOpen , Mode =TwoWay}"
DisplayMode ="Inline"
OpenPaneLength ="256"
IsTabStop ="False">
修改RegisterCommand方法,在点击每个项的部分,添加以下代码,关闭菜单:
this .IsPaneOpen = false ;
现在点击菜单项后可以自动关闭菜单面板了。
还可以继续针对PC版和手机版调整一下细节,PC版屏幕大,可以让菜单收起时留下图标的部分,这就需要调整PC版的DisplayMode属性为CompactInline,需要请StateTriggers出马了。
在根Grid里添加以下代码:
<!-- Adaptive triggers -->
< VisualStateManager.VisualStateGroups >
< VisualStateGroup >
< VisualState >
< VisualState.StateTriggers >
< AdaptiveTrigger MinWindowWidth ="720" />
</ VisualState.StateTriggers >
< VisualState.Setters >
< Setter Target ="RootSplitView.DisplayMode" Value ="CompactInline"/>
< Setter Target ="RootSplitView.IsPaneOpen" Value ="True"/>
< Setter Target ="RootSplitView.CompactPaneLength" Value ="48" />
</ VisualState.Setters >
</ VisualState >
< VisualState >
< VisualState.StateTriggers >
< AdaptiveTrigger MinWindowWidth ="0" />
</ VisualState.StateTriggers >
< VisualState.Setters >
< Setter Target ="RootSplitView.DisplayMode" Value ="Overlay"/>
</ VisualState.Setters >
</ VisualState >
</ VisualStateGroup >
</ VisualStateManager.VisualStateGroups >
这段代码的意思是,如果宽度大于720,就将SplitView的DisplayMode设置为CompactInline,菜单收起的时候可以保留图标部分,这部分图标的宽度通过CompactPaneLength这个值来设定。
还有一点,手机是有硬件返回键的,在菜单弹出的时候,如果用户点击了返回键,应该让菜单缩回去,所以还要额外处理一下手机的返回键。
给项目添加Mobile Extensions引用:
注意我安装了两个版本的SDK,这里需要根据项目的实际版本来选择对应的扩展。
打开MainPage.xaml.cs,添加以下代码:
protected override void OnNavigatedTo( NavigationEventArgs e)
{
if (Windows.Foundation.Metadata. ApiInformation .IsTypePresent( "Windows.Phone.UI.Input.HardwareButtons" ))
{
HardwareButtons .BackPressed += HardwareButtons_BackPressed;
}
base .OnNavigatedTo(e);
}
protected override void OnNavigatedFrom( NavigationEventArgs e)
{
if (Windows.Foundation.Metadata. ApiInformation .IsTypePresent( "Windows.Phone.UI.Input.HardwareButtons" ))
{
HardwareButtons .BackPressed -= HardwareButtons_BackPressed;
}
base .OnNavigatedFrom(e);
}
private void HardwareButtons_BackPressed( object sender, BackPressedEventArgs e)
{
//throw new NotImplementedException();
var vm = this .LayoutRoot.DataContext as MainPage_Model ;
if (vm != null )
{
if (vm.IsPaneOpen)
{
e.Handled = true ;
vm.IsPaneOpen = false ;
}
}
}
至此,一个具有基本功能的汉堡菜单就完成了,可以通过修改背景色、前景色等方式再来改善展示效果。再来总结一下主要的知识点: