从第一篇开始我们就看到了页面的导航切换,上一篇文章则介绍了框架实现导航的原理和过程。真正的导航功能是NavigationService类来实现的。而Frame是Page的载体,是负责导航,历史记录等功能的,相当于一个指挥官。这一篇就主要介绍一下导航的操作和相关的一些方法。
一 导航时发生错误
默认的我们建立一个Windows Phone程序,使用导航功能是不会出现这个问题的。我们先看一个列子:
private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e) { // Set the root visual to allow the application to render if (RootVisual != RootFrame) //RootVisual = RootFrame; RootVisual = new MainPage(); // Remove this handler since it is no longer needed RootFrame.Navigated -= CompleteInitializePhoneApplication; } |
我们新建一个项目后,不把Frame设置到RootVsual,而是用MainPage。我们从Mainpage导航到Page1.
使用两种方法:
private void Button_Click(object sender, RoutedEventArgs e) { 方法1:this.NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); 方法2:(Application.Current as App).RootFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); } |
第一种方法是使用Page页面的NavigationSevice属性来导航,第二种方法是使用Frame对象来导航。从上一篇我们知道,两种方法是相同的。结果是使用第一种方法发生了NullReferenceException错误,为什么NavigationService对象是空?我们不是在App构造函数中就创建过NavigationServie对象的实例吗,并且在加载了XAML文件后设置到了依赖属性中。
obj2.SetValue(NavigationServiceProperty, this); |
上一篇文章介绍过,Page的NavigationService属性是通过依赖属性获得的。其实这里我要了解依赖属性的特点,虽然是一个static字段来维护,但是内部是有Hash表来存放不同对象的属性值。我们在构造函数的Load方法中,加载了配置文件中指定的MainPage,并生成了他的实例,然后设置了NavigationServiceProperty。而这里我们使用,MainPage的是一个新的实例,他的NavigationServiceProperty为Null。所以这里当然会报错了。
而且NavigationServiceProperty是internal属性,说以我们不能手动设置了。
从这个异常我们能发现,每次导航的新的页面,在加载完成Page之后,都会设置Page的NavigationServiceProperty的属性。所以并不是创建了Frame,我们就能使用NavigationService来导航。而是需要Frame来吧Page和NavigationService关联起来。而这里,我们new的Mainpage没有通过Frame加载显示的,所以无法导航。
接下来看看第二种方法。这里使用的是我们创建的RootFrame对象,因为此时Frame已经完全构造好了,NavigationSevice也创建好了。所以此时调用Navigate方法是不会报错的,而且返回的是true。因为这里不存在延迟导航,所以说明是成果了,但是为什么没显示呢?我们点击【<-】退出程序,但是点了2下才退出,这说明导航了吗?我们在做个试验,我们在Page1中加入以下的代码
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { base.OnNavigatedTo(e); if (e.NavigationMode != System.Windows.Navigation.NavigationMode.Back) { bool ret = (Application.Current as App).RootFrame.Navigate(new Uri("/Page2.xaml", UriKind.Relative)); } } |
我们在从MainPage用方法2导航到Page1后,在点【<-】退出程序,现在要点3下了。我们也可以设置断点来查看,实际完成了MainPage->Page1->Page2的导航。当时为什么没有显示呢?很简单了,因为Page要通过RootVisual来显示,导航的话Page需要Frame来加载的,当我们把Frame设置到RootVisual时,就能显示,而这里我们设置的MainPage,虽然Frame导航了,但没有被显示出来。但是RootVisual只能设置一次,所以这里没有办法让他显示出来了。
好了,到这里我们导航的原理就研究到这里,通过2个错误我们进步不理解了Frame和Page的关系。也明确了Frame导航和加载Page的这一功能。这也说明为什么必须有Frame才能导航。
二 导航的过程
在上面的列子中,我们看到了OnNavigatedTo方法,当导航到Page1时自动导航到了Page2。我们知道一个Page导航到另一个Page实际是一个卸载和装载的过程。在这个过程中会触发一系列的事件和方法,下面我们就来看下这些方法。
public class Page : UserControl { // Methods internal Page(); internal void InternalOnFragmentNavigation(FragmentNavigationEventArgs e); internal virtual void InternalOnNavigatedFrom(NavigationEventArgs e); internal virtual void InternalOnNavigatedTo(NavigationEventArgs e); internal void InternalOnNavigatingFrom(NavigatingCancelEventArgs e); protected virtual void OnFragmentNavigation(FragmentNavigationEventArgs e); protected virtual void OnNavigatedFrom(NavigationEventArgs e); protected virtual void OnNavigatedTo(NavigationEventArgs e); protected virtual void OnNavigatingFrom(NavigatingCancelEventArgs e); } |
在Page类中,我们看到了和导航相关的4个protected虚方法。对应有4个internal的方法,他们是在事件发生的时候触发的(系统已经给我们绑定了),他们实现很简单,就是调用这里的虚方法。我们可以通过重写这些虚方法来控制。而在PhoneApplicationPage类中没有重写这些方法,也没有加入其它相关的方法。
在我们Demo3的PhoneApp3中我们在MainPage和Page1重写这几个方法:
protected override void OnFragmentNavigation(System.Windows.Navigation.FragmentNavigationEventArgs e) { Debug.WriteLine("{0}:OnFragmentNavigation", this.ToString()); base.OnFragmentNavigation(e); } protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedTo", this.ToString()); base.OnNavigatedTo(e); } protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e) { Debug.WriteLine("{0}:OnNavigatingFrom", this.ToString()); base.OnNavigatingFrom(e); } protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedFrom", this.ToString()); base.OnNavigatedFrom(e); } |
启动程序,从MainPage导航到Page1: 输出如下:
PhoneApp3.MainPage:OnNavigatedTo PhoneApp3.MainPage:OnNavigatingFrom PhoneApp3.MainPage:OnNavigatedFrom PhoneApp3.Page1:OnNavigatedTo |
点击【<-】按钮,输出如下:
PhoneApp3.Page1:OnNavigatingFrom PhoneApp3.Page1:OnNavigatedFrom PhoneApp3.MainPage:OnNavigatedTo |
在点击【<-】按钮退出程序,输出如下:
PhoneApp3.MainPage:OnNavigatingFrom PhoneApp3.MainPage:OnNavigatedFrom |
好吧,这些方法的执行顺序很清楚了,我就不用什么介绍了。不清楚可以看MSDN。不过这里命名很容易让人产生误解。我开始就弄反了。这里To和From相对的对象都是当前的页面。OnFragmentNavigation
方法,在导航到包括片断的统一资源标识符 (URI) 时会发生。一个片断是片断分隔符 (#) 后的值。这个后面在介绍。
三 页面传值
和Web一样,我们在导航过程中可能需要传递值,对于WinForm程序来说,我们可以通过Form的构造函数传值,可以定义定义全局变量等多种方法,对于Web,我们可以有QueryString,Cookies,Session等方法,在这里我们可以用类似的方法传递,这里我们结合导航到过程来进行值的传递。这里我们导航时不能通过构造函数传递,也无法使用Cookies和Session。
我在Demo3的PhoneApp4中,MainPage和Page1放置了5个TextBox,每个对应一种传递方式:
1 全局变量传递
这个方法在WinForm很常见,我们在App类中定义一个变量,App是一个全局对象,所以可以定义在这里。
public partial class App : Application { public string StaticVar { get; set; } //全局变量 public PhoneApplicationFrame RootFrame { get; private set; } public App() { } |
在Mainpage的OnNavigtedFrom方法中,把s1的值传递给全局变量,s1的值来源于TextBox1。当然你可以在导航前任意时刻获取TextBox的值,这里只是演示导航方法。
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedFrom", this.ToString()); base.OnNavigatedFrom(e); s1 = textBox1.Text; //全局变量传递 (Application.Current as App).StaticVar = s1; } |
在Page1的OnNavigatedTo方法中接受并显示。同样,你可以在导航到Page1后的任意时刻来获取并显示,这里也是为了掩饰导航方法。
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedTo", this.ToString()); base.OnNavigatedTo(e); //从全局变量接受 s1 = (Application.Current as App).StaticVar; textBox1.Text = s1; } |
使用全局变量很方便,但是占用内存空间,并且存在丢失的可能。这个以后文件会介绍。
2 页面QueryString传值
因为这里导航和Web相似,所以我们也可以用Uri后加上QueryString的传值方式。我们这里使用TextBox2.
首先修改导航按钮代码,我们必须在导航前,构造好导航的Uri,并且要获取TextBox2的值。QueryString格式为
Name1=Value1&Name2=Value2,多个参数用&间隔开。
private void button1_Click(object sender, RoutedEventArgs e) { //构造QueryString s2 = textBox2.Text; string uri = "/Page1.xaml?s2=" + s2; //导航 this.NavigationService.Navigate(new Uri(uri, UriKind.Relative)); } |
而在Page1页面,我们通过NavigationContext属性的QueryString获得传递的值。这里是返回的是IDictionary类型,在使用前,必须检查Name对应的变量是否存在。
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedTo", this.ToString()); base.OnNavigatedTo(e); //从Uri接受 IDictionary<string, string> queryString = this.NavigationContext.QueryString; if (queryString.ContainsKey("s2")) { s2 = queryString["s2"]; } //显示到UI textBox2.Text = s2; } |
使用QueryString可以传递少量数据,因为Uri长度是有限制的,并且只能传递简单类型,如果要传递自定义类对象,就不适用了。另外传的是string类型,所以可能需要进行类型转化。在我们导航的时候,会建立一个JournalEntry对象,当新的xaml页面加载完后,会去解析URI中的QueryString数据存入到其中。
JournalEntry.SetNavigationContext(obj2, new NavigationContext(UriParsingHelper.InternalUriParseQueryStringToDictionary |
3 PhoneApplicationService类
如果我们需要传递自定义类型数据时,就需要用到这个对象。其实这个对象我们并不是很陌生,在前面我们就介绍过他实现了IApplicationService就接口,为Silverlight程序供了扩展功能,比如前面App中启动时Application_Launching,Application_Activated等四个方法。在这里他在内部提供了一个字典来维护一些全局的数据:
public IDictionary<string, object> State { get; } |
所以我们不需要自己去定义全局变量,而可以直接使用他,通过PhoneApplicationService.Current.State就能访问到这个字典。我们这里使用TextBox3来演示。在MainPage中把TextBox3获得的值,设置到字典中,注意,这里的Key需要是唯一的。另外我们使用时要引入以下命名空间。
using Microsoft.Phone.Shell; |
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedFrom", this.ToString()); base.OnNavigatedFrom(e); s3 = textBox3.Text; //使用PhoneApplicationService传递 PhoneApplicationService.Current.State["s3"] = s3; } |
我们在Page1中,我们取得s3的值2,这里我们采用了TryGetValue来获得值,当然也能使用QueryString中的方法。两者都能实现。TryGetValue性能更高。
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedTo", this.ToString()); base.OnNavigatedTo(e); //从PhoneApplicationService获得数据 object objS3; PhoneApplicationService.Current.State.TryGetValue("s3", out objS3); if (objS3 != null) { s3 = Convert.ToString(objS3); } //显示到UI textBox3.Text = s3; } |
这里我们要注意,这里是object类型,所以在读取数据时,存在类型转换。另外,这里传递的对象,必须是可以序列化的对象。
4 独立存储传递
使用独立存储IsolatedStorageSettings类时,需要引用以下命名空间。我们在MainPage中使用她来保存TextBox4的值。
using System.IO.IsolatedStorage; |
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedFrom", this.ToString()); base.OnNavigatedFrom(e); s4 = textBox4.Text; //使用独立存储 IsolatedStorageSettings.ApplicationSettings["s4"] = s4; } |
在Page1页面,我们获取S4的值,这里我们采用TryGetValue<>,这是一个泛型方法,从安全和性能上都比较好。
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("{0}:OnNavigatedTo", this.ToString()); base.OnNavigatedTo(e); //从独立存储获得数据 IsolatedStorageSettings.ApplicationSettings.TryGetValue<string>("s4", out s4); textBox4.Text = s4; } |
看起来也没有什么特别。我们在MainPage的OnNavigatedTo也加入上面Page1中的代码,我们从MainPage导航到Page,让后在关闭程序,在重新启动。嘿嘿。竟然在Mainpage中就显示了TextBox4之前的值。这是因为独立存储把值存到了本地,所以即便程序关闭也不会丢失,而前面则存在丢失的问题。对于全局变量和QueryString,即便程序不完全关闭,也是存在丢失的可能。所以一般来说,我们使用IsolatedStorageSettings来存储程序的一些配置信息。当然我们还能选择File和Database等方式来传递数据,当然对于页面传值来说就有点不太适合。
以上是各种方式传值的结果。
四 总结
这一篇文章首先继续谈了Frame和Page的关系, 然后介绍了在导航时会触发的事件方法,我们可以通过重写这些方法来控制页面的显示,比如进行值的传递。也详细介绍了值传递的四种方法和使用的环境。在下一篇文章将继续介绍导航的其他常用方法以及回退键,还有特殊页面的回退处理。
|