diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml
index 0d175858cc..5f2ea8db1c 100644
--- a/.github/workflows/admin-sample.cd.yml
+++ b/.github/workflows/admin-sample.cd.yml
@@ -115,7 +115,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --module Admin --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads
+ cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --module Admin --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml
index 40e410bd07..2143f22a84 100644
--- a/.github/workflows/bit.full.ci.yml
+++ b/.github/workflows/bit.full.ci.yml
@@ -354,13 +354,13 @@ jobs:
- name: Build sample configuration 1
run: |
- dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant
+ dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant --brouter
dotnet build TestProject/TestProject.sln -p:InvariantGlobalization=false -p:Environment=Staging -p:EnforceCodeStyleInBuild=true
rm -r "TestProject"
- name: Build sample configuration 2
run: |
- dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true --redis false --multitenant false
+ dotnet new bit-bp --name TestProject2 --database Other --filesStorage S3 --api Standalone --captcha None --pipeline None --module None --offlineDb false --appInsights false --sentry false --signalR false --notification false --cloudflare false --ads false --aspire true --redis false --multitenant false --brouter false
dotnet build TestProject2/TestProject2.slnx -p:InvariantGlobalization=true -p:Environment=Development -p:EnforceCodeStyleInBuild=true
rm -r "TestProject2"
diff --git a/.github/workflows/sales-module-demo.cd.yml b/.github/workflows/sales-module-demo.cd.yml
index eac993269a..b7252be890 100644
--- a/.github/workflows/sales-module-demo.cd.yml
+++ b/.github/workflows/sales-module-demo.cd.yml
@@ -41,7 +41,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --appInsights --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads
+ cd ../../../ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --appInsights --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
@@ -106,7 +106,7 @@ jobs:
cd src\Templates\Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ..\..\..\ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --sentry --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads
+ cd ..\..\..\ && dotnet new bit-bp --name SalesModule --database PostgreSQL --module Sales --sentry --apiServerUrl ${{ env.SERVER_ADDRESS }} --webAppUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml
index f5eb1142f1..29ed510d91 100644
--- a/.github/workflows/todo-sample.cd.yml
+++ b/.github/workflows/todo-sample.cd.yml
@@ -42,7 +42,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --api Standalone --redis --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --api Standalone --redis --multitenant false --brouter false
- name: Use Bit.ResxTranslator
run: |
@@ -95,7 +95,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --api Standalone --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --api Standalone --multitenant false --brouter false
- name: Use Bit.ResxTranslator
run: |
@@ -158,7 +158,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
@@ -207,7 +207,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --appInsights --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --offlineDb --signalR --ads --multitenant false --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
@@ -261,7 +261,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --ads --multitenant false --brouter false
- name: Update core appsettings.json
uses: devops-actions/variable-substitution@ae7b1f3676ae374dc70282e6b9650bc50b611222 # v1.2
@@ -314,7 +314,7 @@ jobs:
cd src\Templates\Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --multitenant false
+ cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --ads --multitenant false --brouter false
- name: Use Bit.ResxTranslator
run: |
@@ -376,7 +376,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false --brouter false
- name: Extract Android signing key from env
uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c # v2
@@ -477,7 +477,7 @@ jobs:
cd src/Templates/Boilerplate && dotnet build -c Release
dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0
dotnet new install Bit.Boilerplate.0.0.0.nupkg
- cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false
+ cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample --sentry --apiServerUrl ${{ env.SERVER_API_ADDRESS }} --webAppUrl ${{ env.SERVER_WEB_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --ads --multitenant false --brouter false
- name: Use Bit.ResxTranslator
run: |
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
index 078f6639f3..c3592cf4b1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json
@@ -62,6 +62,9 @@
{
"id": "ads"
},
+ {
+ "id": "brouter"
+ },
{
"id": "apiServerUrl",
"isVisible": false
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
index a519821f89..561cc0796b 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json
@@ -241,6 +241,13 @@
"defaultValue": "false",
"description": "Adds GoogleAds integration."
},
+ "brouter": {
+ "displayName": "Use bit Brouter?",
+ "type": "parameter",
+ "datatype": "bool",
+ "defaultValue": "true",
+ "description": "Replaces the built-in Blazor Router with bit Brouter, bringing advanced routing features - nested routes, route guards, data loaders, keep-alive pages and view transitions - inspired by the routers of Vue.js, React and Angular."
+ },
"nameToLower": {
"type": "generated",
"generator": "casing",
@@ -690,6 +697,12 @@
"src/Client/Boilerplate.Client.Core/Infrastructure/Services/Contracts/IAdsService.cs",
"src/Client/Boilerplate.Client.Core/Infrastructure/Services/AdsService.cs"
]
+ },
+ {
+ "condition": "(brouter != true)",
+ "exclude": [
+ "src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs"
+ ]
}
]
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
index fb3fa68fad..855f249cd4 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj
@@ -18,6 +18,8 @@
+
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs
index efe22dfcc4..3847194201 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppRouteDataPublisher.cs
@@ -3,11 +3,17 @@ namespace Boilerplate.Client.Core.Components;
public partial class AppRouteDataPublisher : AppComponentBase
{
+ private RouteData? lastPublishedRouteData;
+
[Parameter] public RouteData? RouteData { get; set; }
- protected override async Task OnInitAsync()
+ protected override async Task OnParamsSetAsync()
{
- await base.OnInitAsync();
+ await base.OnParamsSetAsync();
+
+ // RouteData is a fresh instance per navigation; skip redundant publishes on plain re-renders.
+ if (ReferenceEquals(lastPublishedRouteData, RouteData)) return;
+ lastPublishedRouteData = RouteData;
PubSubService.Publish(ClientAppMessages.ROUTE_DATA_UPDATED, RouteData);
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs
new file mode 100644
index 0000000000..ed31aca188
--- /dev/null
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.BrouterLifecycles.cs
@@ -0,0 +1,125 @@
+namespace Boilerplate.Client.Core.Components.Pages;
+
+///
+/// bit Brouter route-lifecycle hooks for pages deriving from .
+///
+///
+/// Brouter treats routing as more than "match a URL and mount a component". A route can be marked
+/// KeepAlive (see Routes.razor): instead of being torn down when you navigate away, the
+/// page's component instance is retained and merely hidden, then revealed again on
+/// return - so scroll position, grid paging/sorting/filtering, unsaved input, in-flight state, etc. all
+/// survive the round-trip. And whether a page is kept alive or not, Brouter gives it a much richer
+/// lifecycle than Blazor's bare OnInitialized/Dispose pair.
+///
+///
+public abstract partial class AppPageBase : IBrouterRoute
+{
+ ///
+ ///
+ ///
+ protected virtual async ValueTask OnActivated(BrouterRouteActivation activation) { }
+ public async ValueTask OnActivatedAsync(BrouterRouteActivation activation)
+ {
+ try
+ {
+ await OnActivated(activation);
+ }
+ catch (Exception exp)
+ {
+ ExceptionHandler.Handle(exp);
+ }
+ }
+
+ ///
+ ///
+ ///
+ protected virtual async ValueTask OnDeactivated(BrouterRouteDeactivation deactivation) { }
+ public async ValueTask OnDeactivatedAsync(BrouterRouteDeactivation deactivation)
+ {
+ try
+ {
+ await OnDeactivated(deactivation);
+ }
+ catch (Exception exp)
+ {
+ ExceptionHandler.Handle(exp);
+ }
+ }
+
+ ///
+ ///
+ ///
+ protected virtual async ValueTask OnDeactivating(BrouterRouteDeactivatingContext context) { }
+ public async ValueTask OnDeactivatingAsync(BrouterRouteDeactivatingContext context)
+ {
+ try
+ {
+ await OnDeactivating(context);
+ }
+ catch (Exception exp)
+ {
+ ExceptionHandler.Handle(exp);
+ }
+ }
+
+ ///
+ ///
+ ///
+ protected virtual async ValueTask OnRenavigated(BrouterRouteRenavigation renavigation) { }
+ public async ValueTask OnRenavigatedAsync(BrouterRouteRenavigation renavigation)
+ {
+ try
+ {
+ await OnRenavigated(renavigation);
+ }
+ catch (Exception exp)
+ {
+ ExceptionHandler.Handle(exp);
+ }
+ }
+
+ ///
+ ///
+ ///
+ protected virtual async ValueTask OnRenavigating(BrouterRouteRenavigatingContext context) { }
+ public async ValueTask OnRenavigatingAsync(BrouterRouteRenavigatingContext context)
+ {
+ try
+ {
+ await OnRenavigating(context);
+ }
+ catch (Exception exp)
+ {
+ ExceptionHandler.Handle(exp);
+ }
+ }
+
+ // The following code enables Brouter route-lifecycle hooks for pages inheriting from AppPageBase.
+
+ [CascadingParameter] private BrouterRouteContext? routeContext { get; set; }
+ private bool brouterRegistered;
+
+ public override Task SetParametersAsync(ParameterView parameters)
+ {
+ var task = base.SetParametersAsync(parameters);
+
+ if (brouterRegistered is false && routeContext is not null)
+ {
+ routeContext.Register(this);
+ brouterRegistered = true;
+ }
+
+ return task;
+ }
+
+ protected override ValueTask DisposeAsync(bool disposing)
+ {
+ if (brouterRegistered)
+ {
+ routeContext?.Unregister(this);
+ brouterRegistered = false;
+ }
+
+ return base.DisposeAsync(disposing);
+ }
+}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs
index 13bdd90665..d66146a8b4 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/AppPageBase.cs
@@ -1,4 +1,3 @@
-
namespace Boilerplate.Client.Core.Components.Pages;
public abstract partial class AppPageBase : AppComponentBase
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor
index 177e52e078..c2f965967a 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Categories/CategoriesPage.razor
@@ -33,8 +33,8 @@
Class="categories-grid"
OnRead="LoadCategories"
Loading="isLoading"
- Pageable Filterable Resizable
- ShowToolbar ShowCsvExport ShowExcelExport
+ Pageable Filterable Resizable Reorderable
+ ShowToolbar ShowColumnChooser ShowCsvExport ShowExcelExport
PageSize="10"
Strings="DataGridStrings">
- {
- NavigationManager.RefreshCurrentPage();
- });
- //#endif
- }
-
- //#if (signalR == true)
- protected override async ValueTask DisposeAsync(bool disposing)
- {
- await base.DisposeAsync(disposing);
-
- unsubscribe?.Invoke();
- }
- //#endif
-}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs
index 4cddcdbc84..31c1dda1d1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/OverallStatsWidget.razor.cs
@@ -1,3 +1,4 @@
+//+:cnd:noEmit
using Boilerplate.Shared.Features.Dashboard;
namespace Boilerplate.Client.Core.Components.Pages.Dashboard;
@@ -8,17 +9,26 @@ public partial class OverallStatsWidget
private bool isLoading;
private OverallAnalyticsStatsDataResponseDto dto = new();
+ //#if (signalR == true)
+ private Action? unsubscribe;
+ //#endif
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
+ //#if (signalR == true)
+ // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes.
+ unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData));
+ //#endif
+
await GetData();
}
private async Task GetData()
{
isLoading = true;
+ StateHasChanged();
try
{
@@ -27,6 +37,16 @@ private async Task GetData()
finally
{
isLoading = false;
+ StateHasChanged();
}
}
+
+ //#if (signalR == true)
+ protected override async ValueTask DisposeAsync(bool disposing)
+ {
+ await base.DisposeAsync(disposing);
+
+ unsubscribe?.Invoke();
+ }
+ //#endif
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs
index 0d09c6e22d..336d0412ef 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsCountPerCategoryWidget.razor.cs
@@ -1,3 +1,4 @@
+//+:cnd:noEmit
using Boilerplate.Shared.Features.Dashboard;
namespace Boilerplate.Client.Core.Components.Pages.Dashboard;
@@ -8,26 +9,18 @@ public partial class ProductsCountPerCategoryWidget
private bool isLoading;
private BitChartConfig config = default!;
+ //#if (signalR == true)
+ private Action? unsubscribe;
+ //#endif
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
- config = new BitChartConfig
- {
- Type = BitChartType.Bar,
- Options = new BitChartOptions
- {
- Plugins = new BitChartPluginOptions
- {
- Legend = new BitChartLegendOptions { Display = false }
- },
- Scales =
- {
- ["y"] = new BitChartScaleOptions { Id = "y", Type = BitChartScaleType.Linear, BeginAtZero = true }
- }
- }
- };
+ //#if (signalR == true)
+ // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes.
+ unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData));
+ //#endif
await GetData();
}
@@ -35,9 +28,27 @@ protected override async Task OnInitAsync()
private async Task GetData()
{
isLoading = true;
+ StateHasChanged();
try
{
+ // A fresh config is built on each load so re-fetches replace the previous data instead of appending to it.
+ config = new BitChartConfig
+ {
+ Type = BitChartType.Bar,
+ Options = new BitChartOptions
+ {
+ Plugins = new BitChartPluginOptions
+ {
+ Legend = new BitChartLegendOptions { Display = false }
+ },
+ Scales =
+ {
+ ["y"] = new BitChartScaleOptions { Id = "y", Type = BitChartScaleType.Linear, BeginAtZero = true }
+ }
+ }
+ };
+
var data = await dashboardController.GetProductsCountPerCategoryStats(CurrentCancellationToken);
config.Data.Labels.AddRange(data.Select(d => d.CategoryName ?? string.Empty));
@@ -50,6 +61,16 @@ private async Task GetData()
finally
{
isLoading = false;
+ StateHasChanged();
}
}
+
+ //#if (signalR == true)
+ protected override async ValueTask DisposeAsync(bool disposing)
+ {
+ await base.DisposeAsync(disposing);
+
+ unsubscribe?.Invoke();
+ }
+ //#endif
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs
index 772b985c42..9a685962d1 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/ProductsPercentageWidget.razor.cs
@@ -1,3 +1,4 @@
+//+:cnd:noEmit
using Boilerplate.Shared.Features.Dashboard;
namespace Boilerplate.Client.Core.Components.Pages.Dashboard;
@@ -8,22 +9,18 @@ public partial class ProductsPercentageWidget
private bool isLoading;
private BitChartConfig config = default!;
+ //#if (signalR == true)
+ private Action? unsubscribe;
+ //#endif
protected override async Task OnInitAsync()
{
await base.OnInitAsync();
- config = new BitChartConfig
- {
- Type = BitChartType.Pie,
- Options = new BitChartOptions
- {
- Plugins = new BitChartPluginOptions
- {
- Legend = new BitChartLegendOptions { Position = BitChartPosition.Right }
- }
- }
- };
+ //#if (signalR == true)
+ // Instead of reloading the whole app, refresh only this widget's data when the dashboard changes.
+ unsubscribe = PubSubService.Subscribe(SharedAppMessages.DASHBOARD_DATA_CHANGED, async _ => await InvokeAsync(GetData));
+ //#endif
await GetData();
}
@@ -31,9 +28,23 @@ protected override async Task OnInitAsync()
private async Task GetData()
{
isLoading = true;
+ StateHasChanged();
try
{
+ // A fresh config is built on each load so re-fetches replace the previous data instead of appending to it.
+ config = new BitChartConfig
+ {
+ Type = BitChartType.Pie,
+ Options = new BitChartOptions
+ {
+ Plugins = new BitChartPluginOptions
+ {
+ Legend = new BitChartLegendOptions { Position = BitChartPosition.Right }
+ }
+ }
+ };
+
var data = await dashboardController.GetProductsPercentagePerCategoryStats(CurrentCancellationToken);
config.Data.Labels.AddRange(data.Select(d => d.CategoryName ?? string.Empty));
@@ -46,6 +57,16 @@ private async Task GetData()
finally
{
isLoading = false;
+ StateHasChanged();
}
}
+
+ //#if (signalR == true)
+ protected override async ValueTask DisposeAsync(bool disposing)
+ {
+ await base.DisposeAsync(disposing);
+
+ unsubscribe?.Invoke();
+ }
+ //#endif
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor
index 08491926e8..6a0bfade9f 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Products/ProductsPage.razor
@@ -44,8 +44,8 @@
Class="products-grid"
OnRead="LoadProducts"
Loading="isLoading"
- Pageable Filterable Resizable
- ShowToolbar ShowCsvExport ShowExcelExport
+ Pageable Filterable Resizable Reorderable
+ ShowToolbar ShowColumnChooser ShowCsvExport ShowExcelExport
PageSize="10"
Strings="DataGridStrings">
-
+
+ @*#if (brouter == true)*@
+
+ @*#if (module == "Admin")*@
+ @* Keep-alive: pages that host a BitDataGrid whose add/edit lives on a separate page
+ (e.g. ProductsPage -> AddOrEditProductPage) keep their grid state - current page,
+ sorting and filtering - when the user navigates to the detail page and back, instead
+ of resetting to page 1. These hand-declared routes shadow the discovered @@page routes
+ of ProductsPage; authorization keeps flowing through the Found template below. *@
+
+
+ @*#endif*@
+
+ @*#endif*@
@@ -22,7 +36,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
index 156c13a96c..f0dc7bdaf7 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor.cs
@@ -1,3 +1,4 @@
+//+:cnd:noEmit
namespace Boilerplate.Client.Core.Components;
public partial class Routes
@@ -35,3 +36,21 @@ await Task.Run(async () =>
});
}
}
+
+///
+/// This class is only a workaround for limitations we faced in razor files inside .NET project templates.
+/// You could simply delete it in your own project and use the base class directly in Routes.razor file.
+///
+public class AppRouter :
+ //#if (brouter == true)
+ Brouter
+//#else
+//#if (IsInsideProjectTemplate == true)
+/*
+//#endif
+Microsoft.AspNetCore.Components.Routing.Router
+//#if (IsInsideProjectTemplate == true)
+*/
+//#endif
+//#endif
+{ }
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor
index 605d7594ee..71bfca0266 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/_Imports.razor
@@ -14,6 +14,9 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Bit.Butil
+@*#if (brouter == true)*@
+@using Bit.Brouter
+@*#endif*@
@using Bit.BlazorUI
@using Boilerplate
@using Boilerplate.Shared
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs
index 14c5e1aa14..3f11b1b7a0 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/IClientCoreServiceCollectionExtensions.cs
@@ -82,6 +82,9 @@ public IServiceCollection AddClientCoreProjectServices(IConfiguration configurat
.ValidateOnStart();
services.AddBitButilServices();
+ //#if (brouter == true)
+ services.AddBitBrouterServices();
+ //#endif
services.AddBitBlazorUIServices();
services.AddBitBlazorUIExtrasServices(trySingleton: AppPlatform.IsBlazorHybrid);
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs
index 2c5adad35a..e2127cb9cb 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Infrastructure/Extensions/NavigationManagerExtensions.cs
@@ -25,7 +25,7 @@ public string GetRelativePath()
///
public void RefreshCurrentPage()
{
- navigationManager.NavigateTo(navigationManager.GetUriPath(), forceLoad: false, replace: true);
+ navigationManager.NavigateTo(navigationManager.GetUriPath(), forceLoad: true, replace: true);
}
}
}
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs
index e22508dd1c..050562497e 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.Services.cs
@@ -39,10 +39,8 @@ public void ConfigureServices()
{
BaseAddress = new Uri(configuration.GetServerAddress(), UriKind.Absolute)
};
- if (sp.GetRequiredService().WebAppUrl is Uri origin)
- {
- httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString());
- }
+ var origin = sp.GetRequiredService().WebAppUrl ?? httpClient.BaseAddress;
+ httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString());
return httpClient;
});
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
index 734dd76c92..eaba5c8a66 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs
@@ -34,10 +34,8 @@ public void AddClientWindowsProjectServices(IConfiguration configuration)
{
BaseAddress = new Uri(configuration.GetServerAddress(), UriKind.Absolute)
};
- if (sp.GetRequiredService().WebAppUrl is Uri origin)
- {
- httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString());
- }
+ var origin = sp.GetRequiredService().WebAppUrl ?? httpClient.BaseAddress;
+ httpClient.DefaultRequestHeaders.Add("X-Origin", origin.ToString());
return httpClient;
});
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
index 29f53fff68..e2cc79e817 100644
--- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
+++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props
@@ -3,6 +3,7 @@
+
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor
index d4f45c2ba5..1e3fcefd33 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor
@@ -1,4 +1,4 @@
-@page "/templates/create-project"
+@page "/templates/create-project"
@page "/boilerplate/create-project"
@inherits AppComponentBase
@@ -408,6 +408,21 @@
Helps you earn money by displaying reward-based Google ads. Demo
+
+
+ bit Brouter
+
+
+
+
+ @GetBrouterCommand()
+
+ bit Brouter replaces Blazor's built-in Router and brings the powerful routing features that developers enjoy in Vue.js, React and Angular to the Blazor ecosystem: nested routing, async route guards, data loaders, keep-alive pages that preserve their state across navigation, named and typed routes, and animated view transitions.
+
+
+ It stays a drop-in replacement, so your existing @@page routes, layouts and authorization keep working unchanged while you opt into the advanced features where you need them.
+
+
diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs
index 032c7b7f2a..fbb8cfca27 100644
--- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs
+++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs
@@ -17,6 +17,7 @@ public partial class Templates05CreateProjectPage
private Parameter redis = new() { Value = false, Default = false };
private Parameter aspire = new() { Value = true, Default = true };
private Parameter multiTenant = new() { Value = true, Default = true };
+ private Parameter brouter = new() { Value = true, Default = true };
private Parameter captcha = new()
{
@@ -180,6 +181,11 @@ private string GetFinalCommand()
finalCommand.Append(GetMultiTenantCommand());
}
+ if (brouter.IsModified)
+ {
+ finalCommand.Append(GetBrouterCommand());
+ }
+
return finalCommand.ToString();
}
@@ -273,6 +279,11 @@ private string GetMultiTenantCommand()
return $"--multitenant{(multiTenant.Value ? string.Empty : " false")} ";
}
+ private string GetBrouterCommand()
+ {
+ return $"--brouter{(brouter.Value ? string.Empty : " false")} ";
+ }
+
private class Parameter
{
public T? Value { get; set; }