我们都知道,在UWP里面,页面间的跳转必须通过Frame.Navigate进行,除了ContentDialog以外,Page是没法通过new去Show的。
当我们通过Frame.Navigate进行页面跳转时,我们很自然的希望,在按返回键退回之前的页面时,页面可以保留,无需再重新刷新。对于这一点,微软提供了解决方案,可以通过将Page的NavigationCacheMode属性设为Enabled或Required,就可以将页面的内容记录下来,用于返回时自动加载。(存在一点小瑕疵,比如对ListView的滚动位置记录不一定准确,有时需要通过另外的手段去记录ListView的滚动位置,在此不详述)
在一般情况下,微软提供的Cache已足以满足要求,但是在某些情况下,就不能完全达到目的了。
比如,我们的页面跳转是这样的,A->B1->C1->D1->B2->C2->D2,这里,第一次的B、C、D和第二次的B、C、D传入的参数不一样,即内容不一样,当我们按返回键时,会出现什么情况呢?
当我们返回到C2、B2时,内容正确,当我们返回到C1、B1时,发现其内容不是C1、B1,而是C2、B2。即微软的缓存并不能支持对一个页面多次进入的情况(也有可能是我不知道,但是我在网上也没搜到什么官方的解决办法)。
而我做的一个应用就有同一个页面可通过不同途径多次进入的情况,显然仅仅只是用官方提供的Cache,已经不能满足应用的正常使用了,在经过半个晚上的思考后,决定自己来实现这个页面跳转的缓存。
下面,将这个实现过程写出来,抛砖引玉,看看有没有更好的或者更官方的实现方法。
我们从两个方面来考虑这个页面缓存,一个是同一个页面需要有多个缓存,并且顺序必须保证一致;一个是页面的缓存要包含哪些数据。
针对第一个问题,我们可以将问题简化,只考虑页面跳转的New和Back两种模式,这样的话,对每个页面,创建一个Stack,即可完美的满足多个缓存和顺序要求;针对第二个问题,经过我的实践,定义了一个类来作为缓存的数据,类中包含两个object类型的对象,PageContent和PageParameter,Content代表整个页面的控件内容,Parameter代表Page中所有的类成员变量(非控件)。
类代码如下:
class PageStackContent
{
public PageStackContent()
{
}
public PageStackContent(object pageContent, object pageParameter = null)
{
this.PageContent = pageContent;
this.PageParameter = pageParameter;
}
public object PageContent { get; set; }
public object PageParameter { get; set; }
}
然后,我们定义一个static的Dictionary,用于缓存数据,代码如下:
private static readonly Dictionary<Type, Stack<PageStackContent>> DictPageContent
= new Dictionary<Type, Stack<PageStackContent>>();
同时,定义一个全局变量用于保存Frame,在App.xaml创建时,就将其赋值,代码如下:
public static Frame MainContentFrame { get; set; }
然后,写一个方法,用于在Page重写OnNavigatedTo时调用,代码如下:
public static void OnNavigatedTo(Type pageType, NavigationMode mode, Action newPageCallBack = null,
Action<object> backPageCallBack = null)
{
if (mode == NavigationMode.New || mode == NavigationMode.Refresh)
{
newPageCallBack?.Invoke();
}
else if (mode == NavigationMode.Back)
{
object pageParameter = null;
if (DictPageContent.ContainsKey(pageType) && DictPageContent[pageType].Count != 0)
{
var temp = DictPageContent[pageType].Pop();
MainContentFrame.Content = temp.PageContent;
pageParameter = temp.PageParameter;
}
backPageCallBack?.Invoke(pageParameter);
}
}
该段代码的主要作用,就是在后退到该页面时,将该页面的缓存从栈中Pop出来,Content直接赋值给Frame的Content,而Parameter传回页面的回调函数中去执行。
再写一个方法,在Page重写OnNavigatingFrom中调用,代码如下:
public static void OnNavigatingFrom(Type pageType, NavigatingCancelEventArgs e, object pageContent,
ICloneable pageParameter = null, Action newPageCallBack = null, Action backPageCallBack = null)
{
if (e.NavigationMode == NavigationMode.New)
{
if (!DictPageContent.ContainsKey(pageType))
{
DictPageContent.Add(pageType, new Stack<PageStackContent>());
}
DictPageContent[pageType].Push(new PageStackContent
(pageContent, pageParameter?.Clone()));
newPageCallBack?.Invoke();
}
else if (e.NavigationMode == NavigationMode.Back)
{
backPageCallBack?.Invoke();
}
}
该段代码的主要作用,是在通过New的模式离开该页面时,将其Content和Parameter入栈。这里有一点值得特别注意的是,我传入的parameter不是object类型,而是实现了ICloneable接口的类型,这是因为在Page中定义的Parameter必须为static的,这样就不能将其直接入栈了,否则页面那边变了,栈里面的也会跟着变,必须将其复制后再入栈。
以上静态的方法,我都写在了一个NavigateHelper类中。
然后,就是在页面中的调用了,需要先做两个准备工作。首先,将Page的NavigationCacheMode设为Disabled,这是因为如果用Enabled或Required,Page会启用单实例模式,缓存到栈中的实例仍然会在页面跳转时发生变化;第二,将Page中所有的自定义类成员变量组合成一个Parameter类,实现ICloneable接口,并在Page中定义一个static的Parameter对象。UWP中似乎没有ICloneable接口,自定义即可,代码如下:
interface ICloneable
{
object Clone();
}
当然,不定义ICloneable接口,而通过反射等方式去复制对象,应该也是可行的。
这里将Parameter定义为static的原因是,如果不定义成static,Page_Load的执行是早于OnNavigatedTo的,所以当使用了Page_Load时,会发生值不正确的问题。没有测试过重写OnNavigatingTo,或许可以。
然后,重写OnNavigatedTo和OnNavigatingFrom两个方法即可。
具体代码如下:
private static PageParameters _parameters = new PageParameters();
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NavigateHelper.OnNavigatedTo(this.GetType(), e.NavigationMode, async () =>
{
if (e.Parameter is String)
{
_parameters.ID = e.Parameter.ToString();
await LoadData();
}
}, o =>
{
if (o is PageParameters p)
{
_parameters = p;
}
});
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
NavigateHelper.OnNavigatingFrom(this.GetType(), e, Frame.Content, _parameters);
}
这里的PageParameters根据页面的不同,可能每个页面需要单独定义一个。
经过上述的经过,就能实现一个页面的多次缓存了,如果一个页面不需多次进入,那么还是可以继续用官方提供的Cache来缓存。另外,传入的参数可能也可以再优化下。