?说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
导航
有关导航的话题在介绍NavigationWindow与Page等元素时有提及。这篇文章将详细分析导航相关话题。同其它话题,针对WPF,Silverlight与WP 7,导航特性大致相似又有着些许不同。在介绍此特性时相同的特性将合在一起,每个框架独有的特性也将独立介绍并有明显标识。
导航的功能及目的就是从一个页面转向另一个页面,可能是前进亦或是后退/返回。以下几种实现导航的方法:
- 调用Navigate方法
- 使用Hyperlink
- 使用导航日志
- 调用Navigate方法
导航容器支持通过使用Navigate方法改变其中的内容页。Navigate方法有两个重载,分别接受目标页的实例或指向目标页的URI。另外提示,这两种重载分别有等效的属性设置的实现,这两个属性设置方式主要用于在Xaml中以声明方式设置这两个值,在代码中应使用Navigate的两个重载,在下面的代码示例中演示所有这些方式的C#代码实现:
//通过页面实例进行导航PhotoPage npage = new PhotoPage();this.NavigationService.Navigate(npage);//通过属性导航到页面实例this.NavigationService.Content = npage;//通过URI进行导航this.NavigationService.Navigate(new Uri(“PhotoPage.xaml”,UriKind.Relative));//通过属性导航到URIthis.NavigationService.Source = new Uri(“PhotoPage.xaml”,UriKind.Relative);
?
Uri指向的Page可以是一般的xaml文件,也可以是编译后的内容。必须的要求就是根元素为Page。如果要导航至一个Html页,需要使用Navigate接收Uri的重载,如;
this.NavigationService.Navigate(new Uri(“http://www.microsoft.com”));
- 使用Hyperlink
WPF中的Hyperlink与Html中的超链接非常相似,其提供了一种容易使用的导航方案。Hyperlink元素可以嵌在TextBlock元素中,会被自动呈现为可以被点击的超链接样式(就如Html中<a>标签被自动格式化那样)。Hyperlink通过NavigateUri属性指定导航的目标页(就如Html中<a>标签的href属性)。
提示:
类似Html的超链接可以通过使用Target属性指定目标页在哪个Frame中打开,WPF中的Hyperlink也可以通过TargetName属性指定目标页在那个Frame中打开,TargetName属性为Frame的名称。
另外WPF中Hyperlink也支持Html中锚点的效果,方式与Html也类似,即在Uri后面跟上一个#,并在后面接上Page中某个元素的名称,表示要导航到的部分。
如果要在导航的同时做一些样式等处理,可以给NavigateUri属性指定一个假的路径,并处理Click事件,在这个事件中完成样式设置并调用Navigate方法手工完成导航。
- 使用导航日志
导航日志是所有导航容器都记录的导航历史信息。Web浏览器也有这个特征。导航日志通过两个栈 – 前进栈与后退栈来提供后退与前进的功能。
这两个栈具体工作方式如下:
动 作 |
结 果 |
后退 |
把当前页推入前进栈,从后退栈弹出一个页,并导航至这个页 |
前进 |
把当前页推入后退栈,从前进栈弹出一个页,并导航至这个页 |
进入新页面 |
把当前页推入到后退栈,并把前进栈清空 |
要完成前进与后退动作,可以调用导航容器的GoBack和GoForward方法,注意,在调用到这两个方法之前需要先调用CanGoBack或CanGoForward来确保后退栈或前进栈不为空,从而确保这个导航行为正确无误的执行。
NavigationWindow总会保存导航日志,而Frame是否保存导航日志取决于其JournalOwnership属性值的设置,有如下几种设置:
- OwnsJournal:Frame有自己的导航日志
- UsesParentJournal:在父容器中保存导航日志,如果父容器不支持导航日志,则Frame也不会保存导航日志。
- Automatic(默认值):当Frame寄宿于NavigationWindow或Frame,使用同UsesParentJournal的设置,否则使用同OwnsJournal的设置。
在Frame拥有导航日志的情况下,Frame中就会出现内置的导航按钮,当然可以通过将NavigationUIVisibility设为Hidden来隐藏这个导航按钮。
提示:
当 通过URI导航至一个Page时(无论是通过调用Navigate方法,还是通过Hyperlink控件),即使这个页面已经被访问过,仍然会创建此 page的一个实例(当然,通过调用Navigate接受page实例的重载构造方法可以控制是否创建Page的新实例)。这是如果需要在同一个page 多个实例间维持共享数据,可以通过使用静态变量或者使用Application.Properties属性。
另外在使用导航日志导航时,把JournalEntry.KeepAlive附加属性设置为true,可强迫Page重用同一个示例。
提示:使用导航日志时怎么实现停止与刷新?
这两个功能没有内建的按钮支持,可以通过调用导航容器中的方法来实现这个目的:
- StopLoading方法:可以在任意事件停止一个正在处理的导航操作。
- Refresh方法:可以刷新容器中当前page对象。(这与向Navigate方法传入当前页面的URI实例是等价的,唯一区别是传给Navigating事件处理函数的值中包含一个NavigationMode.Refresh,这样事件处理函数可以区别处理)
这两个方法都是没有参数的。
提示:
通过设置Page的RemoveFromJournal属性为true,可以将该页从导航日志中移除。通过这个属性可以实现按顺序访问每一页(即将访问过的页面的此属性置为true,从而保证用户无法执行后退操作)。
提示:可以利用导航日志进行其它操作
导航日志中允许用户添加自定义项,从而轻松实现(得益于导航日志维护的两个栈)一些如应用程序级的撤销/重做功能而不单单是用于导航。
下 面详细介绍利用导航日志实现一个自定义撤销动作(撤销图像旋转)的过程。首先我们需要创建一个派生自CustomContentState抽象类的类,实 现父类中的Replay方法,并重写JournalEntryName属性(这是可选的)。这个Replay方法会在前进或后退操作发生时被调用。这个子 类的实现如下:
[Serializable]class RotateState : CustomContentState{ FrameworkElement element; double rotation; public RotateState(FrameworkElement element, double rotation) { this.element = element; this.rotation = rotation; } public override string JournalEntryName { get { return "Rotate " + rotation + "。¡ê"; } } public override void Replay(NavigationService navigationService, NavigationMode mode) { element.LayoutTransform = new RotateTransform(rotation); }}?然后在需要撤销操作的地方调用NavigationService的AddBackEntry方法,并以上面这个子类的一个实例作为参数。这样就可以在导航日志中添加一条自定义的记录。代码如:
void fix_RotateClockwise_Click(object sender, RoutedEventArgs e){ this.NavigationService.AddBackEntry(new RotateState(image, (image.LayoutTransform as RotateTransform).Angle)); (image.LayoutTransform as RotateTransform).Angle += 90;}?
这样在撤销操作发生时,将调用导航日志中这条自定义记录的RotateState对象的Replay方法,来实现操作的撤销。
与内置页面导航对比,初次执行对象旋转操作相当于导航到一个新页面。这个时候自定义项被添加到导航日志的后退栈中,清空前进栈。当撤销操作发生时,这条自定义项由后退栈弹出,执行其中定义的Replay方法,后被压入前进栈。这时候再调用前进操作,则过程相反。
导航事件
上文介绍的3种导航方式都是以异步方式执行的,在这个过程中,许多事件依次被触发,你可以处理这些事件来完成如显示进度UI等,甚至可以在事件处理中取消导航。下面两个图展示了导航过程中顺序触发的事件:
页面第一次被加载时,所有触发的导航事件如下
页面之间导航时触发的导航事件:
如上面流程图可看出,在Navigated事件触发前,NavigationProcess会被周期性调用。这个过程中如果发生错误或者取消导航,则将会触发NavigationStopped事件(而不是LoadCompleted事件)。
提示:
在Application中定义了与导航容器中同名的这些导航事件(见上图),这样可以在统一的地方(Application对象中)处理所有导航容器(包括子导航容器)的导航事件。
注意:导航事件触发的场合
当WPF页面之间导航或WPF页面与Html页面间导航时,WPF的导航事件会被触发,而Html页面间导航时怒会触发这些事件,也不会被记录在导航日志内。
数据传递
类似于基于Html的Web应用程序使用将数据编码到Url的方式进行页间数据传递,WPF的导航过程也很需要页间传递数据,下面介绍WPF中夜间数据传递的方式:
向页面传递数据
- 方法1:
WPF可以在导航过程中传递参数,这是通过Navigate方法的重载来支持的。对于接收Page实例的Navigate重载与接收Uri的重载又分别有一个重载接收另一个参数作为导航过程中传递的数据。如:
int photoId = 10;//导航到页面实例并传递参数this.NavigationService.Navigate(new PhotoPage(), photoId);//通过URI进行导航并传递参数this.NavigationService.Navigate(new Uri(“PhotoPage.xaml”,UriKind.Relative), photoId);
?
在目标页中需要处理导航容器的LoadCompleted事件,并由事件参数(NavigationEventArgs类型的参数)的ExtraData属性中得到原始页传递的数据。代码即:
This.NavigationService.LoadCompleted += new LoadCompletedEventHandler(container_LoadCompleted);…void container_LoadCompleted(object sender, NavigationEventArgs e){ if(e.ExtraData != null) LoadPhoto((int)e.ExtraData);}
?
- 方法2:
此方法更为简单,为导航的目标页面增加一个属性来存储希望在导航过程中传递的数据,并提供一个构造函数可以方便的设置这个属性。然后通过调用接收Page实例的Navigate方法的重载,传入使用待传递数据初始化的目标页面的实例。这样就实现了数据的传递。代码如:
//目标页面构造函数Public PhotoPage(int id){ LoadPhoto(id); //或将id存储到Property中}//导航到Page实例PhotoPage next = new PhotoPage(3);this.NavigationService.Navigate(next);
?
这种传递方式一个显而易见的好处即参数是强类型的,而不用再做是否为null或类型的判断。
- 方法3:
利用Application对象的Properties集合共享全局数据,具体做法可以参见对Application对象的介绍。如果方法1,这种方法也不是类型安全的。
通过PageFunction从页面里返回数据
为了满足如下这种需求(导航至每个页,进行一些操作,然后自动返回到前一页,同时把新数据传回,下图是其中一个例子):
WPF提供了一个叫做PageFunction的类来完成以类型安全的方式把数据返回前一页,并自动后退到前一页的机制。下图展示了新的流程。
PageFunction继承自Page(即其本身也是一个Page),其主要功能是用来返回数据。Visual Studio提供了创建PageFunction的模版:
通过此模版建立的PageFunction形如:
1 <PageFunction 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:sys="clr-namespace:System;assembly=mscorlib" 5 x:Class="WpfApplication1.PageFunction1" 6 x:TypeArguments="sys:String" 7 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 8 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 9 mc:Ignorable="d" 10 d:DesignHeight="300" d:DesignWidth="300" 11 Title="PageFunction1"> 12 <Grid> 13 </Grid> 14 </PageFunction>
PageFunction是一个泛型类(PageFunction<T>),其中类型参数表示返回数据的类型。上面xaml中x:TypeArguments即定义了这个类型参数。由于PageFunction的返回值必须是字符串,所以PageFunction中x:TypeArguments值需为sys:String。通过泛型PageFunction也保证了可以类型安全的使用这个类。
而通过PageFunction实现本节初的流程方式如:
//创建PageFunction的实例PageFunction1 next = new PageFunction1<string>();//像导航到一般页面导航到这个PageFunction this.NavigationService.Navigate(next); //在源页面中处理PageFunction的Return事件得到返回值//(PageFunction中)next.Return += new ReturnEventHandler<string>(nextPage_Return); void next_Return(object sender, ReturnEventArgs<string> e) { string returnValue = e.Result;}
?
由上面代码可以看出,事件处理函数参数的Result属性与PageFunction的返回类型是一致的。在内部,返回的数据被包装在ReturnEventArgs的对象中。返回时,PageFunction基类中的OnReturn方法,触发事件,返回数据:
OnReturn(new ReturnEventArgs<string>("the data"));
?
本文完
参考:
《WPF揭秘》