feat(module: reusetabs): Add singleton page setting for reusing page instance. (#4151)

* feat(module: reusetabs): Add `NewPageForParams` to ReuseTabsPageAttribute.

* Update NewPageForParams's default value.

* rename the property

* add a demo

---------

Co-authored-by: James Yeung <shunjiey@hotmail.com>
This commit is contained in:
pankey888 2024-09-14 19:54:38 +08:00 committed by GitHub
parent 57ad7125fe
commit dc3ed2ad27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 227 additions and 58 deletions

View File

@ -12,7 +12,7 @@
{
@foreach (var item in ReuseTabsService.Pages)
{
<div class="ant-reuse-tabs-content @Class" @key="item.Url" style="@(item.Url == ReuseTabsService.CurrentUrl? "": "display:none;") @Style">
<div class="ant-reuse-tabs-content @Class" @key="item.Key" style="@(item.Key == ReuseTabsService.ActiveKey? "": "display:none;") @Style">
@item.Body
</div>
}

View File

@ -2,12 +2,12 @@
@inherits AntDomComponentBase
@inject NavigationManager _navigationManager
<Tabs Class="@Class" Style="@Style" Id="@Id" HideAdd Type="@TabType.EditableCard" @bind-ActiveKey="@ReuseTabsService.CurrentUrl" OnEdit="OnTabEdit" Draggable="@Draggable" Size="@Size">
<Tabs Class="@Class" Style="@Style" Id="@Id" HideAdd Type="@TabType.EditableCard" @bind-ActiveKey="@ReuseTabsService.ActiveKey" OnEdit="OnTabEdit" Draggable="@Draggable" Size="@Size">
@if (ReuseTabsService.Pages?.Any() == true)
{
@foreach (var item in ReuseTabsService.Pages)
{
<TabPane @key="@item.Url" Key="@item.Url" Class="@TabPaneClass" Closable="item.Closable" ForceRender>
<TabPane @key="@item.Key" Key="@item.Key" Class="@TabPaneClass" Closable="item.Closable" ForceRender>
<TabTemplate>
@item.Title
</TabTemplate>

View File

@ -109,7 +109,7 @@ namespace AntDesign
if (action != "remove")
return false;
return ReuseTabsService.ClosePage(key);
return ReuseTabsService.ClosePageByKey(key);
}
private void OnLocationChanged(object o, LocationChangedEventArgs _)

View File

@ -6,20 +6,58 @@ using System;
namespace AntDesign
{
/// <summary>
/// Attribute for ReuseTabsPage, used to set the page title and other properties.
/// </summary>
public class ReuseTabsPageAttribute : Attribute
{
/// <summary>
/// Specifies the title of the tab.
/// <para>
/// If you want to set a <see cref="Microsoft.AspNetCore.Components.RenderFragment"/>, you need implement <see cref="IReuseTabsPage.GetPageTitle()"/> in the page.
/// </para>
/// </summary>
public string Title { get; set; }
/// <summary>
/// Weather the page wont be reused.
/// </summary>
public bool Ignore { get; set; }
/// <summary>
/// Weather the tab can be closed.
/// </summary>
public bool Closable { get; set; } = true;
/// <summary>
/// Weather the tab can be pinned and opened at the first time.
/// </summary>
public bool Pin { get; set; } = false;
/// <summary>
/// The url of the pinned page. Because when the tab is clicked, it need to navigate to the page through the url.
/// </summary>
public string PinUrl { get; set; }
/// <summary>
/// Weather the page is keeping alive.
/// <para>
/// If true, the page will be kept in memory, otherwise, the page will be destroyed when it is not active.
/// </para>
/// </summary>
public bool KeepAlive { get; set; } = true;
/// <summary>
/// The order of the page, the smaller the order, the earlier the page will be displayed.
/// </summary>
public int Order { get; set; } = 999;
/// <summary>
/// Weather the page is a singleton.
/// <para>
/// If true, the page will be reused although the parameters is different, otherwise, another tab will be created.
/// </para>
/// </summary>
public bool Singleton { get; set; }
}
}

View File

@ -26,5 +26,14 @@ namespace AntDesign
public bool KeepAlive { get; set; } = true;
public int Order { get; set; } = 9999;
public string TypeName { get; set; }
public string Key { get; set; }
/// <summary>
/// Weather the page is a singleton. If true, the page will be reused although the url is different, otherwise, another tab will be created.
/// </summary>
public bool Singleton { get; set; }
}
}

View File

@ -17,8 +17,7 @@ namespace AntDesign
{
private readonly NavigationManager _navmgr;
private readonly MenuService _menusService;
private readonly Dictionary<string, ReuseTabsPageItem> _pageMap = [];
private IReadOnlyCollection<ReuseTabsPageItem> _pages;
private ICollection<ReuseTabsPageItem> _pages = new List<ReuseTabsPageItem>();
internal event Action OnStateHasChanged;
@ -38,10 +37,27 @@ namespace AntDesign
}
}
private string _activeKey;
internal string ActiveKey
{
get => _activeKey;
set
{
_activeKey = value;
var pageItem = _pages.FirstOrDefault(r => r.Key == value);
if (pageItem != null && (pageItem.Url != CurrentUrl || pageItem.Body == null))
{
CurrentUrl = pageItem.Url;
}
}
}
/// <summary>
/// The page information list of the currently opened page, which can be used for caching and recovery
/// </summary>
public IReadOnlyCollection<ReuseTabsPageItem> Pages => _pages;
public IReadOnlyCollection<ReuseTabsPageItem> Pages => [.. _pages];
public ReuseTabsService(NavigationManager navmgr, MenuService menusService)
{
@ -60,11 +76,9 @@ namespace AntDesign
/// <param name="titleTemplate">The title show on the tab</param>
public void CreateTab(string pageUrl, RenderFragment titleTemplate = null)
{
if (_pageMap.ContainsKey(pageUrl))
{
if (_pages.Any(x => x.Url == pageUrl))
return;
}
AddPage(pageUrl, new ReuseTabsPageItem() { Url = pageUrl, Title = titleTemplate ?? pageUrl.ToRenderFragment(), CreatedAt = DateTime.MinValue });
AddPage(new ReuseTabsPageItem() { Url = pageUrl, Title = titleTemplate ?? pageUrl.ToRenderFragment(), CreatedAt = DateTime.MinValue });
OnStateHasChanged?.Invoke();
}
@ -75,12 +89,7 @@ namespace AntDesign
/// <param name="title">The title show on the tab</param>
public void CreateTab(string pageUrl, string title)
{
if (_pageMap.ContainsKey(pageUrl))
{
return;
}
AddPage(pageUrl, new ReuseTabsPageItem() { Url = pageUrl, Title = title.ToRenderFragment(), CreatedAt = DateTime.MinValue });
OnStateHasChanged?.Invoke();
CreateTab(pageUrl, title.ToRenderFragment());
}
//public void Pin(string key)
@ -95,30 +104,48 @@ namespace AntDesign
//}
/// <summary>
/// Close the page corresponding to the specified key
/// Close the page corresponding to the specified url
/// </summary>
/// <param name="key">The specified page's key</param>
public bool ClosePage(string key)
/// <param name="url">The specified page's url</param>
public bool ClosePage(string url)
{
var reuseTabsPageItem = _pages?.FirstOrDefault(w => w.Url == key);
var reuseTabsPageItem = _pages?.FirstOrDefault(w => w.Url == url);
if (reuseTabsPageItem?.Closable != true)
{
return false;
}
RemovePageBase(key);
RemovePageBase(reuseTabsPageItem.Url);
StateHasChanged();
return true;
}
/// <summary>
/// Close all pages except the page with the specified key
/// Close the page corresponding to the specified key
/// </summary>
/// <param name="key">The specified page's key</param>
public void CloseOther(string key)
internal bool ClosePageByKey(string key)
{
foreach (var item in _pages?.Where(x => x.Closable && x.Url != key && !x.Pin))
var reuseTabsPageItem = _pages?.FirstOrDefault(w => w.Key == key);
if (reuseTabsPageItem?.Closable != true)
{
return false;
}
RemovePageBase(reuseTabsPageItem.Url);
StateHasChanged();
return true;
}
/// <summary>
/// Close all pages except the page with the specified url
/// </summary>
/// <param name="url">The specified page's url</param>
public void CloseOther(string url)
{
foreach (var item in _pages?.Where(x => x.Closable && x.Url != url && !x.Pin))
{
RemovePageBase(item.Url);
}
@ -154,16 +181,20 @@ namespace AntDesign
}
/// <summary>
/// Reload the page corresponding to the specified key
/// Reload the page corresponding to the specified url
/// </summary>
/// <param name="key"></param>
public void ReloadPage(string key)
/// <param name="url"></param>
public void ReloadPage(string url)
{
key ??= CurrentUrl;
_pageMap[key].Body = null;
if (CurrentUrl == key)
url ??= CurrentUrl;
var reuseTabsPageItem = _pages?.FirstOrDefault(w => w.Url == url);
if (reuseTabsPageItem != null)
{
CurrentUrl = key; // auto reload current page, and other page would be load by tab navigation.
// Reset content
reuseTabsPageItem.Body = null;
// auto reload current page, and other page would be load by tab navigation
if (ActiveKey == reuseTabsPageItem.Key)
ActiveKey = reuseTabsPageItem.Key;
}
StateHasChanged();
}
@ -198,22 +229,28 @@ namespace AntDesign
if (!reuse)
{
_pageMap.Clear();
_pages.Clear();
}
var reuseTabsPageItem = _pages?.FirstOrDefault(w => w.Url == CurrentUrl || (w.Singleton && w.TypeName == routeData.PageType?.FullName));
var reuseTabsPageItem = _pageMap.ContainsKey(CurrentUrl) ? _pageMap[CurrentUrl] : null;
if (reuseTabsPageItem == null)
{
reuseTabsPageItem = new ReuseTabsPageItem
{
Url = CurrentUrl,
CreatedAt = DateTime.Now,
TypeName = routeData.PageType.FullName
};
AddPage(CurrentUrl, reuseTabsPageItem);
AddPage(reuseTabsPageItem);
}
else
{
reuseTabsPageItem.Url = CurrentUrl;
}
reuseTabsPageItem.Body ??= CreateBody(routeData, reuseTabsPageItem);
reuseTabsPageItem.Body = CreateBody(routeData, reuseTabsPageItem);
ActiveKey = reuseTabsPageItem.Key;
OnStateHasChanged?.Invoke();
}
@ -262,6 +299,7 @@ namespace AntDesign
pageItem.Pin = attr.Pin;
pageItem.KeepAlive = attr.KeepAlive;
pageItem.Order = attr.Order;
pageItem.Singleton = attr.Singleton;
}
pageItem.Title ??= b =>
@ -300,8 +338,8 @@ namespace AntDesign
this.AddReuseTabsPageItem(pageType);
}
}
CurrentUrl ??= _pages.FirstOrDefault()?.Url;
if (CurrentUrl == null)
ActiveKey = _pages.FirstOrDefault()?.Key;
}
private void AddReuseTabsPageItem(Type pageType)
@ -310,17 +348,21 @@ namespace AntDesign
var reuseTabsAttribute = pageType.GetCustomAttribute<ReuseTabsPageAttribute>();
var url = reuseTabsAttribute?.PinUrl ?? routeAttribute.Template;
var reuseTabsPageItem = new ReuseTabsPageItem();
var reuseTabsPageItem = new ReuseTabsPageItem
{
Url = url,
CreatedAt = DateTime.MinValue,
TypeName = pageType.FullName
};
GetPageInfo(reuseTabsPageItem, pageType, url, Activator.CreateInstance(pageType));
reuseTabsPageItem.CreatedAt = DateTime.MinValue;
reuseTabsPageItem.Url = url;
AddPage(url, reuseTabsPageItem);
AddPage(reuseTabsPageItem);
}
private void AddPage(string key, ReuseTabsPageItem pageItem)
private void AddPage(ReuseTabsPageItem pageItem)
{
_pageMap.TryAdd(key, pageItem);
_pages = _pageMap.Values.Where(x => !x.Ignore)
pageItem.Key = pageItem.GetHashCode().ToString();
_pages.Add(pageItem);
_pages = _pages.Where(x => !x.Ignore)
.OrderBy(x => x.CreatedAt)
.ThenByDescending(x => x.Pin ? 1 : 0)
.ThenBy(x => x.Order)
@ -329,13 +371,12 @@ namespace AntDesign
private void RemovePageBase(string key)
{
_pageMap[key].Body = null;
_pageMap.Remove(key);
_pages = _pageMap.Values.Where(x => !x.Ignore)
.OrderBy(x => x.CreatedAt)
.ThenByDescending(x => x.Pin ? 1 : 0)
.ThenBy(x => x.Order)
.ToList();
var pageItem = _pages.Where(x => x.Url == key).FirstOrDefault();
if (pageItem != null)
{
pageItem.Body = null;
_pages.Remove(pageItem);
}
}
public void Dispose()

View File

@ -0,0 +1,54 @@
@layout AntDesign.Docs.Demos.Experimental.ReuseTabs.demo.Layout_
@inject NavigationManager navigationManager
@implements IDisposable
@implements IReuseTabsPage
@page "/reuse/Singleton"
@page "/reuse/Singleton/{Id:int}"
@attribute [ReuseTabsPage(Singleton = true)]
<PageTitle>Singleton Page</PageTitle>
<h1>Singleton Page</h1>
<p>@text</p>
<p>@dynamicText</p>
<p><a href="/reuse/Singleton/@(NextId)?query=@(Random.Shared.Next())">Navigate to next id: @NextId</a></p>
@code{
[Parameter] public int Id { get; set; }
[SupplyParameterFromQuery] public string Query { get; set; }
private int NextId => Id + 1;
string text = "";
string dynamicText = "";
protected override void OnInitialized()
{
text = "OnInitialized only be called once, Id = " + Id;
navigationManager.LocationChanged += OnLationChanged;
}
private void OnLationChanged(object sender, EventArgs e)
{
dynamicText = $"Current Id = {Id}, Query = {Query}";
InvokeAsync(StateHasChanged);
}
public RenderFragment GetPageTitle()
{
return @<span> Singleton Page with Id @Id </span>;
}
public void Dispose()
{
navigationManager.LocationChanged -= OnLationChanged;
}
}

View File

@ -0,0 +1,21 @@
---
order: 4
iframe: 360
link: /reuse/singleton
title:
zh-CN: 单例页面
en-US: Singleton Page
---
## zh-CN
如果页面是单例的,它会被不同参数复用,否则,当参数不同时会打开另一个页面。默认不是单例。
单例页面不会重新实例化,也不再执行初始化方法,因此需要监听页面导航事件来更新界面。
## en-US
If the page is Singleton, it will be reused although the parameters is different, otherwise, another tab will be created.
The singleton page is not re-instantiated, nor is the `OnInitialized{Async}` method performed, so it needs to listen for location navigation events to update the UI.

View File

@ -70,6 +70,9 @@ Used to implement in-application page tabs and page caching.
| PinUrl | Specify the Url of the loaded page, and then open the page with a route parameter, such as `/order/1` | string | - |
| KeepAlive| Whether to cache the page state | bool | true |
| Order | The sequence number | int | 999 |
| TypeName | The page's classsname | string | - |
| Key | The page's key | string | - |
| NewPageForParams | Whether to create a new page for route with different params | bool | false |
### IReuseTabsPage interface
@ -86,9 +89,9 @@ Used to control ReuseTabs in pages
| --- | --- |
| Pages | The information list of the currently opened pages can be used for caching and recovery |
| CreateTab(string pageUrl, RenderFragment? title = null) | Create a tab, but do not navigate to the page, and initialize the page when you navigate to the page. |
| ClosePage(string key) | Close the page with the specified key. |
| CloseOther(string key) | Close all pages except those that specify key, `Cloasable=false`, or `Pin=true`. |
| ClosePage(string url) | Close the page with the specified url. |
| CloseOther(string url) | Close all pages except those that specify url, `Cloasable=false`, or `Pin=true`. |
| CloseAll() | Close all pages except those that `Cloasable=false` or `Pin=true`.|
| CloseCurrent() | Close current page. |
| Update() | Update the state of current tab. When the variable referenced in `GetPageTitle()` changes, `Update()` needs to be called to update the tab display. |
| ReloadPage(key) | Reload the page for the specified label, allowing the page components to be reinitialized without refreshing the browser. If no key is passed, reload the current page . |
| ReloadPage(url) | Reload the page for the specified label, allowing the page components to be reinitialized without refreshing the browser. If no url is passed, reload the current page . |

View File

@ -71,6 +71,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/lkI2hNEDr2V/Tabs.svg
| PinUrl | 固定加载页面的 Url在打开路由有参数的页面时需要比如 `/order/1` | string | - |
| KeepAlive| 是否缓存页面状态 | bool | true |
| Order | 标签顺序 | int | 999 |
| TypeName | 当前页面对应的类别 | string | - |
| Key | 当前标签页的关键字 | string | - |
| NewPageForParams | 是否针对具有不同参数的路由创建新的页面 | bool | false |
### IReuseTabsPage 接口
@ -87,9 +90,9 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/lkI2hNEDr2V/Tabs.svg
| --- | --- |
| Pages | 当前打开过的页面信息列表,可自行用于缓存和恢复 |
| CreateTab(string pageUrl, RenderFragment? title = null) | 创建一个标签,但不导航到该页面,等导航到该页面时才初始化这个页面。|
| ClosePage(string key) | 关闭指定key的页面key 就是 url。 |
| CloseOther(string key) | 关闭除了指定key的页面,或者设置了 `Cloasable=false``Pin=true` 的页面。 |
| ClosePage(string url) | 关闭指定url的页面。 |
| CloseOther(string url) | 关闭除了指定url的页面,或者设置了 `Cloasable=false``Pin=true` 的页面。 |
| CloseAll() | 关闭除了设置了 `Cloasable=false` 或者 `Pin=true` 的页面。 |
| CloseCurrent() | 关闭当前页面。 |
| Update() | 更新 Tab 状态。当 `GetPageTitle()` 中引用的变量发生变化时,需要调用 `Update()` 来更新 tab 的显示。 |
| ReloadPage(key) | 重新加载指定标签的页面,让页面组件重新初始化,且无需刷新浏览器。不传key时重新加载当前页面。 |
| ReloadPage(url) | 重新加载指定标签的页面,让页面组件重新初始化,且无需刷新浏览器。不传url时重新加载当前页面。 |