大多数XAML应用都会向用户展示一个具有初始页面的视图,并允许用户在该视图内跳转到其他页面。这与网站的范式非常相似,都是用户从某个网站的主页开始,然后单击链接进入到该网站的特定页面。对于后退至之前所在的页面,并在此后偶尔再前进到刚才的页面,用户也相当熟悉。Windows应用商店应用通常会提供与此一致的用户体验。当然,有些Windows应用商店应用可能仅显示一个单页面。这种情形下,导航功能将无法使用。
本节中,笔者将介绍XAML对页面导航提供的支持以及如何对此进行高效的内存管理。微软提供了一个称为Windows.UI.Xaml.Controls.Frame的类。该类的一个实例管理着一组UI页面,使得用户可在这些页面之间前进或后退。该类继承自ContentControl类,后者又派生自UIElement类,使得开发人员可为Window类的Content属性分配一个Frame对象,从而可将XAML内容放置在某个绘图图面中。Frame类的定义如下:
public class Frame : ContentControl, INavigate { // 清空从下一Page类型到末尾的栈,并在栈中追加一个新的Page类型 public Boolean Navigate(Type sourcePageType, Object parameter); public Boolean CanGoBack { get; } // 若放置在第一个Page类型之后,将返回true public void GoBack(); // 跳转至前一个Page类型 public Boolean CanGoForward { get; } // 若当前位置之后存在某个Page类型,将返回true public void GoForward(); // 跳转至下一个Page类型 // 这些成员将返回栈的内容及大小 public IList<PageStackEntry> BackStack { get; } public Int23 BackStackDepth { get; } // 用于将栈的类型或参数序列化至一个字符串或从字符串反序列化的成员 public String GetNavigationState(); public void SetNavigationState(String navigationState); // 其他成员未在这里列出 }
Frame对象保存了一个Windows.UI.Xaml.Controls.Page的派生类类型的集合。请注意,是Page类的派生类型,而非Page类的派生类型的对象。为使Frame对象跳转至一个新的Page类的派生类型的对象,你需要调用Navigate方法,并传入一个指向标识了用户希望跳转到的页面的System.Type对象的引用。Navigate方法内部将构造一个Page类的派生类型的实例,并使该对象成为Frame对象的内容,以允许用户通过用户界面与该页面进行交互。Page类的派生类型必须从Windows.UI.Xaml.Controls.Page类导出,其定义如下:
public class Page : UserControl { // 返回拥有该页面的Frame对象 public Frame Frame { get; } // 当页面被加载,并成为父Frame中的当前内容时,调用该方法 protected virtual void OnNavigatedTo(NavigationEventArgs e); // 当页面不再是其父Frame中的当前内容时,调用该方法 protected virtual void OnNavigatedFrom(NavigationEventArgs e); // 获取或设置表明页面是否被缓存以及缓存项持久化周期的导航模式 public NavigationCacheMode NavigationCacheMode { get; set; } // 其他成员未在这里列出 }
当Frame对象构造了一个Page类的派生类型的实例后,它将调用虚方法NavigatedTo。Page类的派生类型可重载该方法,从中为页面进行初始化。当调用Frame类的Navigate方法时,需要传入一个Object类型的引用。Page类的派生类型的对象可通过查询NavigationEventArgs类的只读属性Parameter来获取该参数类型的值。这提供了一种在跳转到新页面时,从代码中传入数据的途径。在第3.4节“进程生命期管理”中,你将了解到,你所传入的值应当是可序列化的。
就内存消耗而言,Page对象是十分昂贵的,因为页面中通常都含有许多控件,且当中的一些控件又为控件集合,即管理许多项的控件。当用户跳转至新页面时,将上一页面及其所有子对象都保存在内存中是十分低效的做法。这正是Frame对象之所以维护的是Page类型而非Page类型的实例的原因。当用户跳转至另一页面时,Frame对象会将所有指向上一页面对象的引用移除,使得该页面对象及其所有子对象均有机会参与垃圾回收,将其所占用的内存释放。接着,如果用户跳转回前一页面,Frame对象将构造一个新的Page对象,并调用其OnNavigatedTo方法,以便这个新Page能够对其自身初始化,并依据需要重新申请内存。
这一切看似井然有序,但如果页面需要在两次垃圾回收之间记录一些状态或重新初始化时,应如何应对?例如,用户可能已经在TextBox控件中输入了一些文字,或在ListView、GridView控件中选择了某一特定项。当页面被垃圾回收,其所有的状态将被垃圾收集器所销毁。因此,当用户跳转至其他页面时,在OnNavigatedFrom方法中,你需要保存最少数量的状态以便能够使页面恢复到用户跳转页面之前的状态。而且该状态必须保存在一个不会参与垃圾回收的位置。
一种推荐的做法是让App单例对象维护一个类似于List<Dictionary<String,Object>>的字典集合。即,Frame对象管理的每个页面都对应一个字典,且每个字典中都包含了一组键值对;每种需要持久化的页面状态都对应一个键值对。这样,由于App单例对象在进程的整个生命期中都存在,它将保证该字典集合一直存在,进而确保了所有字典也一直存在。
当跳转至新页面时,便为该列表增加了一个新的字典。当跳转回之前的页面时,可利用Frame类的BackStackDepth属性来查阅该页面的字典。图3.4展示了当应用跳转至Page_A后,内存中应有哪些对象。Frame对象在其集合中保存了一个单个的Page_A类型及其跳转参数,且字典列表中只含有一个字典。请注意,Page_A对象可对字典进行引用,但你必须确保App单例对象没有引用任何页面对象,否则对页面对象的垃圾回收将被阻止。同样,应当避免在该页面的实例方法注册任何外部事件,因为这将阻止该页面对象参与垃圾回收。或者,如果所有页面的实例方法都注册了事件,请确保在OnNavigatedFrom方法中对这些事件取消注册。
现在,如果用户跳转至Page_B页面,Frame对象将构造一个Page_B对象,使其成为Frame对象的当前内容,并调用其OnNavigatedTo方法。在这个OnNavigatedTo方法中,我们向字典列表中添加了另一个字典,Page_B实例正是在其中进行状态的持久化。图3.5展示了当用户从Page_A跳转至Page_B后,内存中应有哪些对象。
此后,用户可能会从Page_B跳转回Page_A。这样做会使Page_B被垃圾回收,同时一个新的Page_A对象将被创建,并引用列表中的第一个字典。或者,用户可能从Page_B跳转到一个新的Page_A对象,其内容依据传给OnNavigatedTo的导航参数来填充,并可通过NavigationEventArgs类的Parameter属性来提取。图3.6展示了当用户从Page_B跳转到一个新Page_A页面时,内存中应有哪些对象。
现在,当用户从新的Page_A跳转回Page_B时,Frame对象会将它对Page_A对象的全部引用删除,以使其可参与垃圾回收。字典维护了这个Page_A实例的状态,以便当用户再次从Page_B跳转至新Page_A对象时,能够恢复其原先的状态。类似地,用户可在Frame对象的集合中的所有页面之间来回跳转。跳转至某个页面时,将构造一个新页面,并依据字典恢复其状态。顺便提一句,如果用户当前处于第一个Page_A页面,应用此时决定直接跳转至Page_C,则当前页面以外的字典都必须从列表中移除(以允许它们参与垃圾回收),因为用户此时跳转到了该应用用户界面的一个完全不同的分支中。
随着该模型的就绪,应用便可高效地使用内存。在使用该模型时,还有另外一个好处,具体将在第3.4节中进行介绍。顺便提一句,用于创建Windows应用商店应用的一些Visual Studio模板会为管理页面实例状态的SuspensionManager类生成源码。该类并不是一个WinRT类,也不属于Windows;当创建它时,该类的源码将注入Visual Studio项目。
就个人而言,笔者在自己的项目中不会使用SuspensionManager类。笔者通常会创建自己的FramePageStateManager类,因为笔者认为这样做更值得推荐。它具有更清晰的接口,并利用一些将类型安全的包装器运用于每个字典的辅助类,从而为智能提示、编译时类型安全及数据绑定提供了支持。这些额外特性极大地简化了编写应用和管理其状态的工作量。用于管理这一切的代码是Process Model应用的一部分,你可从本书配套网站来下载其源码http://wintellect.com/Resource-WinRT-via-CSharp。