前言: 這是 項目實踐系列 , 算是中高級系列博文, 用於為項目開發過程中不好解決的問題提出解決方案的. 不屬於入門級系列. 解釋起來也比較跳躍, 只講重點. 因為有網友的項目需求, 所以提前把這些解決方案做出來並分享. 問題: Blazor自己是攜帶一個簡單的路由功能的, 當切換Url的時候, 整 ...
前言:
這是 項目實踐系列 , 算是中高級系列博文, 用於為項目開發過程中不好解決的問題提出解決方案的. 不屬於入門級系列. 解釋起來也比較跳躍, 只講重點.
因為有網友的項目需求, 所以提前把這些解決方案做出來並分享.
問題:
Blazor自己是攜帶一個簡單的路由功能的, 當切換Url的時候, 整個通過把RouteData傳遞給 App.razor 載入 MainLayout , 實現頁面刷新的目的.
如果跳轉到另外一個頁面, 然後再跳回來的時候, 希望原來頁面不刷新, 保留之前的狀態 , 例如搜索條件, 那麼怎麼辦?
解決過程:
結合視頻, 圖文觀看效果最好 : https://www.bilibili.com/video/BV1g54y1R7uX/
1. 現在簡單說說, 這種情況的源頭在哪裡.
2. App.razor 文件使用了 RouteView 來實現路由
3. routeData是包含頁面類型, 以及頁面參數的.
4. 然而預設的實現里, RouteView 是不帶狀態的
5. MainLayout雖然得到了內容的 RenderFragment ,
6. 然而這個 RenderFragment是由RouteView直接綁定到routeData上面去.
7. 所以MainLayout無法得到不同的RenderFragment來顯示不同的內容.
8. 要解決這個問題, 首先第一步就是改造 RouteView
改造 RouteView
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Components.Rendering; using System.Reflection; namespace Microsoft.AspNetCore.Components //use this namepace so copy/paste this code easier { public class KeepPageStateRouteView : RouteView { protected override void Render(RenderTreeBuilder builder) { var layoutType = RouteData.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType ?? DefaultLayout; builder.OpenComponent<LayoutView>(0); builder.AddAttribute(1, "Layout", layoutType); builder.AddAttribute(2, "ChildContent", (RenderFragment)CreateBody()); builder.CloseComponent(); } RenderFragment CreateBody() { var pagetype = RouteData.PageType; var routeValues = RouteData.RouteValues; void RenderForLastValue(RenderTreeBuilder builder) { //dont reference RouteData again builder.OpenComponent(0, pagetype); foreach (KeyValuePair<string, object> routeValue in routeValues) { builder.AddAttribute(1, routeValue.Key, routeValue.Value); } builder.CloseComponent(); } return RenderForLastValue; } } }
Blazor自帶的RouteView是一個控制項. 它每次呈現, 都使用 RouteData 屬性, 所以它每次生成的 RenderFragment 都是跟著最後的 RouteData 走, 保存來沒用.
改造後的 KeepPageStateRouteView , 使用 CreateBody() 方法, 創建出綁定 pagetype 和 routevalue 的 RenderFragement , 為 MainLayout 打下基礎
改造 MainLayout
@inherits LayoutComponentBase @inject NavigationManager navmgr @code{ TimeSpan GetUrlMaxLifeSpan(string url) { if (url.Contains("/fetchdata")) // Let /fetachdata always refresh return TimeSpan.Zero; if (url.Contains("/counter")) // Let /counter expires in 10 seconds return TimeSpan.FromSeconds(10); return TimeSpan.FromSeconds(-1); //other pages never expires } class PageItem { public string Url; public RenderFragment PageBody; public DateTime StartTime = DateTime.Now; public DateTime ActiveTime = DateTime.Now; public TimeSpan MaxLifeSpan; } Dictionary<string, PageItem> bodymap = new Dictionary<string, PageItem>(); int mainRenderCount = 0; } <div class="sidebar"> <NavMenu /> </div> <div class="main"> @{ bool currurlrendered = false; string currenturl = navmgr.Uri; PageItem curritem; if (bodymap.TryGetValue(currenturl, out curritem)) { curritem.ActiveTime = DateTime.Now; } else { curritem = new PageItem { Url = currenturl, PageBody = Body }; curritem.MaxLifeSpan = GetUrlMaxLifeSpan(currenturl); if (curritem.MaxLifeSpan != TimeSpan.Zero) { bodymap[navmgr.Uri] = curritem; } } mainRenderCount++; } <div class="top-row px-4"> #@mainRenderCount , CurrentUrl : @currenturl . PageCount : @bodymap.Count , <button @onclick="StateHasChanged">StateHasChanged</button> </div> @foreach (PageItem eachitem in bodymap.Values.ToArray()) { string pageurl = eachitem.Url; RenderFragment pagebody = eachitem.PageBody; string divstyle = "display:none"; if (pageurl == currenturl) { divstyle = ""; currurlrendered = true; } else if (eachitem.MaxLifeSpan.TotalSeconds > 0 && DateTime.Now - eachitem.ActiveTime > eachitem.MaxLifeSpan) { bodymap.Remove(eachitem.Url); continue; } <div @key="pageurl" class="content px-4" style="@divstyle"> @pagebody </div> } @if (!currurlrendered) { <div class="content px-4"> @Body </div> } </div>
MainLayout 里, 最關鍵的是 Dictionary<string, PageItem> bodymap = new Dictionary<string, PageItem>();
這個字典, Key 是 Url , 而 PageItem 則儲存了這個 Url 的多個信息.
範例使用了 TimeSpan GetUrlMaxLifeSpan(string url) 函數來制定頁面的生存時間規則.
如果頁面的生存時間是 0 , 表示不加進 bodymap , 每次都要全部刷新.
生存時間不為0 , 就儲存到 bodymap 裡面去. 然後在
@foreach (PageItem eachitem in bodymap.Values.ToArray())
迴圈過程中, 把每一個頁面 Render 出來.
當前頁面, 就顯示, 不是當前頁面, 則 display:none
沒錯. 在 Blazor 的 Render 體系裡 , 只有輸出了, 才有生命. 不輸出, 就會被系統釋放.
所以, 所有要讓它活著的 Page , 都得輸出. 哪怕用display:none隱藏它.
看看視頻效果吧.
https://www.bilibili.com/video/BV1g54y1R7uX/
最後:
github : https://github.com/BlazorPlus/BlazorDemoKeepPageState
下一個版本: 支持多Tabs
https://github.com/BlazorPlus/BlazorDemoMultiPagesTab
暫時沒時間做視頻寫博客. 後面補上.
視頻雜音的確多, 求推薦一個麥克風..