[Asp.Net Core] Blazor Server Side 项目实践 - 切换页面时保留状态
前言:
这是 项目实践系列 , 算是中高级系列博文, 用于为项目开发过程中不好解决的问题提出解决方案的. 不属于入门级系列. 解释起来也比较跳跃, 只讲重点.
因为有网友的项目需求, 所以提前把这些解决方案做出来并分享.
问题:
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
暂时没时间做视频写博客. 后面补上.
视频杂音的确多, 求推荐一个麦克风..