最近学一下Windows Phone(接下来简称“WinPhone”)的开发,在很久很久前稍探究一下WinPhone中对一些传感器的开发,那么现在就从头来学学WinPhone的开发。先从WinPhone的页面入手,在我印象中比较深刻的那番话:一台WinPhone设备就好比一个Web的浏览器,应用上每个界面就是一个网页,可以点击“后退”来返回之前的页面。这个类比我觉得相当的形象。这番话能引出WinPhone开发中一个比较常见的操作——页面导航,由这个页面导航还引出了别的方面的内容,如下面所示
那下面的介绍将会从导航,启动其他应用,传参,生命周期,后退堆栈这个顺序一步步进行总结。
从一个页面跳转到另一个页面,局限于本应用的页面而言,则是调用Page类里面NavigationService类型并且同名的属性来进行导航,一般的跳转用到的是
public bool Navigate(Uri source)
方法,里面的Uri则是跳转到页面的路径,例如在当前的项目里面除了建立项目时默认建立的MainPage.xaml外,还另外多建了页面Page1.xaml,当我们要把页面从MainPage跳转到Page1的时候,则Navigate方法,形式如下
this.NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative))
感觉跟我们平时写Web页面时很相似,其中Uri的构造函数中有一个UriKind枚举,我们一般填Realtive,如果填Absolute则需要把Uri改成绝对地址。
NavigationService中还有其他方法
public void GoBack(); public void GoForward();
这些方法都体现着导航的特性。但在日常的使用中,一般就会使用GoBack这个方法,它的效果就如键盘上的后退键一样。但GoForward感觉没多大作用。
在Android中,例如拨打电话,查看联系人,浏览某个页面,这些操作都可以使用意图(Intert)去启动相关的Activity,在Windows Phone中,启动其他应用的操作则分为两种,
那么先看看第一种通过URI关联的,URI关联主要利用Luancher这个类,调用LaunchUriAsync方法,传入相应的URI则启动相关的应用,这种方式与平时我们打开一个doc文件就能打开word,xls文件就能打开excel类似。他是通过关联的。例如
Launcher.LaunchUriAsync(new Uri("ms-settings-wifi:"));
就能打开wifi的设置页面;下面列举一下WinPhone内置的一些URI;
但是上面ms-settings-screenrotation:这个属性是只有WinPhone8 Update3才有,所以最好用一下判断,Update3的版本号是8, 0, 10492;当前版本号是Environment.OSVersion.Version
如果要启动第三方的应用,则需要找到该应用的fileToken,按以往的情况可以解开xap文件,从里面的AppManifest.xml文件可以看到,但最近从应用商店下的xap都无法解包。这个验证不了。
再看另外一种利用启动器和选择器。这种就比较接近Android的启动方式,这里会有两个新的概念——选择器和启动器,两个概念都很相像,都是启动某个内置的应用,但区别在于启动器不会向应用程序返回数据或状态,而选择器可向应用程序返回数据和状态。在编码角度上看启动器只需要对像调用方法操作;选择器则需要注册Completed事件,再事件中进行响应地操作,Completed事件也是启动器所没有的。
看看实际的例子,要用到启动器和选择器需要加上
Microsoft.Phone.Tasks;
这个命名空间,用对象浏览器能看到里面有很多以Task后缀的类,微软官网上的例子涉及到PhoneCallTask,SmsComposeTask,EmailComposeTask,PhoneNumberChooserTask等类的例子,在这里调用起来则与Android的很类似,Android中如果要打开“联系人”这个内置应用事需要在Manifest.xml里面增加相应的权限,WinPhone的也一样,大多数都需要增加相应的权限,配置权限的可以在WMAppManifest.xml的功能选项卡里设置,例如下面这段拨打电话的代码
PhoneCallTask phoneCallTask = new PhoneCallTask(); //phoneCallTask.DisplayName = "Allen"; phoneCallTask.PhoneNumber = "10086"; phoneCallTask.Show();
需要设置ID_CAP_CONTACTS和ID_CAP_PHONEDIALER才行。每次使用启动器选择器配置完相关信息之后,调用了Show()方法,应用才会启动,主调应用进入了休眠状态或者被逻辑删除。
这里有两个怪诞的地方,如果用第一种方式URI的tel:10086则不需要获取权限;只有设置了PhoneNumber属性才会真正地可以拨打电话,否则只会显示人名DisplayName,这里他不会和人脉里面的关联起来。
这里先列举WinPhone中各个选择器和启动器,还有各个功能
选择器
启动器
功能
之所以留意这个启动应用的皆因之前用过一个Switch to iOS,它通过一个app把WinPhone换了脸一样,其实里面主要是启动各个内置应用和第三方应用的快捷方式集合。
正常页面跳转都会碰到传递参数的问题,会有从页面A跳到页面B时传递相关的参数,也有从页面B返回页面A要返回参数。这两个方式都分别尝试一下。
像第一种情况,从页面A跳到页面B时传递相关的参数的,如使用Http的GET方法请求一样,以下面的形式
/Page1.xaml?paraName=admin
传入的这些参数在页面导航的时候,会被解析处理来并存入到一个字典里面去,到了页面B,通过NavigationContext的QueryString属性可以获得,这个QueryString属性是IDictionary<string, string>类型,那么我们如果要获取值时可以用一般的
NavigationContext.QueryString[“paraName”]
来获取,但同样也可以使用比较保险的如
string value; NavigationContext.QueryString.TryGetValue("paraName", out value);
其实这里涉及到的是字典集的使用方式了。
像第二种情况,从页面B返回页面A要返回参数,但这个需要在页面A的代码中定义个公共属性用来访问,如
public string paraName { get; set; }
那么在页面B中只需要访问到页面A的实例,设置这个属性paraName则可,那么我们在页面B的代码中重写OnNavigatedFrom方法,如
protected override void OnNavigatedFrom(NavigationEventArgs e) { (e.Content as MainPage).paraName = ""; base.OnNavigatedFrom(e); }
至于OnNavigated方法会在以后介绍。
除了用这两种方式去传递数据外,还可以同过数据共享的形式,使得数据可以跨页读写,这个其实用的是全局参数的思想,在App类里面定义相应的属性,如
public string paraName { get; set; }
由于这个App类是Application的一个子类,父类Application中有一个静态属性Current,顾名思义可以获取到当前的一个实例,App的实例同样可以用这样的方式去获取,但是属性返回的是Application类型的,所以还需要进行一个类型转换才能访问到App内定义的属性。传递参数到页面可以按以下形式
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e) { //其他操作 (App.Current as App).paraName = ""; base.OnNavigatedFrom(e); }
当要获取值的时候则需要以下面的形式
protected override void OnNavigatedFrom(NavigationEventArgs e) { //其他操作 string paraName = (App.Current as App).paraName; base.OnNavigatedFrom(e); }
这里就没有分是传入参数和返回参数了,只有参数的Get和Set的时机之分。
记得在学Android的时候,有一幅很经典的活动声明周期示意图,展示了一个活动各个阶段的状态以及会触发的事件。那么在Windows Phone里面,也有类似的图片去描述一个应用的生命周期。了解这幅图目的在于了解应用在各个情况下的状态以及会触发的事件,在适当时机能借助事件的触发进行某些操作。
在上图而言绿色部分是对整个App而言的,灰色部分是对某一个页面来说的,页面的事件比Android活动的事件少了,这样显得更简单。那下面通过一个程序来验证这几个事件的触发顺序。
在App.xaml.cs的几个方法改成下面形式
private void Application_Launching(object sender, LaunchingEventArgs e) { Debug.WriteLine("Application_Launching"); } private void Application_Activated(object sender, ActivatedEventArgs e) { Debug.WriteLine("Application_Activated"); } private void Application_Deactivated(object sender, DeactivatedEventArgs e) { Debug.WriteLine("Application_Deactivated"); } private void Application_Closing(object sender, ClosingEventArgs e) { Debug.WriteLine("Application_Closing"); }
在MainPage.xaml.cs里面加入以下两个方法
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("Main_Page_OnNavigatedTo"); base.OnNavigatedTo(e); } protected override void OnNavigatedFrom(NavigationEventArgs e) { Debug.WriteLine("Main_Page_OnNavigatedFrom"); base.OnNavigatedFrom(e); }
在App.xaml里面更改的方法是上图中绿色部分的事件绑定的方法,这些都是对整个App而言的,而在MainPage.xaml里面加的方法则是上图灰色部分的方法,它只是作用于某个页面。运行一下程序,查看“输出”的结果可以看出启动app时的结果
Application_Launching
Main_Page_OnNavigatedTo
然后程序就到达了正在运行的状态,这时按开始键跳转到开始界面,“输出”界面的内容
Main_Page_OnNavigatedFrom
Application_Deactivated
这时app进入了休眠状态或者已经墓碑化,从后台重新打开app,“输出”界面的内容
Application_Activated
Main_Page_OnNavigatedTo
App被唤醒重新进入运行状态,这时按一下后退键,“输出”界面的内容
Main_Page_OnNavigatedFrom
Application_Closing
程序退出了。
如果重新打开app,用Win8 Update 2新增的在后台关闭进程的方式关闭app,“输出”界面的内容是
Main_Page_OnNavigatedFrom
Application_Deactivated
而并非是正常退出app时触发的Closing事件。可见Closing事件不一定会在结束一个app时触发。
OnNavigatedFrom和OnNavigatedTo这两个方法只是针对页面而言的,所以在一个app被激活或者被启动时最迟被执行,退出app和缩到后台时最早被执行。在一个app里各个页面的切换,对应的OnNavigatedFrom和OnNavigatedTo都一样会被执行。所以在页面跳转即将离开本页面或者即将到达本页面要附加的操作,都能在这两个方法里执行。
说到后退堆栈还是要重提本文开始提到的那番话,既然在一个app运行过程中从页A跳到页B,页B跳到页C,然后类似地跳转下去,当用户想返回页A或者退出app时(不考虑用其他快捷操作或从后台结束程序),必须按“原路”返回,一层一层地返回回去。我们平时浏览网页的时候也一样,这样的形式正式数据结构中栈的特性先进后出。
对于栈的操作,无非是查看栈顶的元素,弹栈,压栈。回到WinPhone的页面导航中,当app启动到达页A时,后退堆栈是空的,当页A跳到页B时,页A就被压倒栈中,页A是在栈顶;当页B跳到页C时,页B被压到栈里,栈B就在栈顶。
那么可以通过App类中有个名为RootFrame属性的,它是PhoneApplicationFrame类型,这个类封装了对后退堆栈的部分操作,这些操作顾及到整个后退堆栈的合理性,只提供了枚举所有栈元素以及弹栈这两个堆栈操作,压栈的就没有提供方法,因为压栈这个操作实际上就在页面跳转的时候完成的。而且若提供压栈操作会有导致导航混乱的危险。
获取后退堆栈元素的属性是一个名为BackStack的JournalEntry类型的枚举接口;弹栈操作通过RemoveBackEntry()方法来完成,该方法返回的是被弹出来的元素,是RemoveBackEntry类型的。
在学习这个后退堆栈的时候鄙人用过微软提供的例子。那下面的例子也是仿照微软的例子写出来的。
新建了一个项目并建立三个页面:BSPage1.xaml, BSPage2.xaml, BSPage3.xaml,
每个页面的ContentPanel定义如下
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions > <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button Content="Pop Top" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Left" Margin="10,21,0,0" VerticalAlignment="Top" Click="Button_Click"/> <TextBlock x:Name="lbBackStack" HorizontalAlignment="Left" Margin="33,25,0,0" Grid.Row="1" Grid.Column="0" TextWrapping="Wrap" VerticalAlignment="Top"/> <Button Content="NextPage" HorizontalAlignment="Left" Margin="150,21,0,0" Grid.Row="0" Grid.Column="1" VerticalAlignment="Top" Width="162" Click="Next_Click"/> </Grid>
但是BSPage3.xaml可以删掉NextPage那个Button。每个页面的隐藏代码都添加以下内容
string title = "BSPage1"; public BSPage1() { InitializeComponent(); this.tbTitle.Text = title; } protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); this.lbBackStack.Text = BackStackTool.GetBackStackItems(); } private void Button_Click(object sender, RoutedEventArgs e) { App.RootFrame.RemoveBackEntry(); this.lbBackStack.Text = BackStackTool.GetBackStackItems(); } private void Next_Click(object sender, RoutedEventArgs e) { this.NavigationService.Navigate(new Uri("/BSPage2.xaml",UriKind.Relative)); }
对不同的页面,BSPage的需要会有所更换,最后的BSPage3里面还可以吧Next_Click这个方法给去掉,因为它是最后一页,根本不需要在往前跳转了。在这里操控到后退堆栈的就是Button_Click的App.RootFrame.RemoveBackEntry();,作用是从后退堆栈弹出一个元素,里面调用到BackStackTool的GetBackStackItems方法,它实际上是遍历整个后退堆栈的各个元素,然后转成字符串显示到TextBlock里面
public static string GetBackStackItems() { string result=string.Empty; foreach (JournalEntry item in App.RootFrame.BackStack) { result += item.Source.ToString()+"\r\n"; } return result; }
这里没用上的一个方法则是App. RootFrame.Source,它的作用是获取当前页面的Uri,运行app可以看到效果,点击Pop Top就会看到页面从后退堆栈中弹出,被弹出的页面再也不能后退到那里去了。
在实践微软的那个例子的时候误会到那个Pop To Selected 按钮的作用了,最初一直以为是只弹出选中的元素,但这个按堆栈的特性还有后退堆栈这个性质实际上不能实现的。
上面讲了挺多的,估计会有不少错漏,恳请各位批评指正,鄙人虚心接纳,谢谢!
参考文档