UWP自行實現Frame.Navigate的頁面緩存

我們都知道,在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來緩存。另外,傳入的參數可能也可以再優化下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章