From d5331b0f990aa2d5e439c05184e0287d0c930afb Mon Sep 17 00:00:00 2001
From: Alexander Gusev
<95075261+GusevAlexander-DevExpress@users.noreply.github.com>
Date: Mon, 14 Jul 2025 20:01:40 +0400
Subject: [PATCH 01/13] Support dynamic tabs, refactore
updated to 25.1
enabled Fluent theme
removed extra files
refactored
supported dynamic tabs
---
CS/DxBlazorApplication1.sln | 22 ---
.../Components/Layout/Drawer.razor.css | 37 ----
.../Components/Layout/MainLayout.razor | 45 -----
.../Components/Layout/MainLayout.razor.css | 165 ------------------
.../Components/Layout/NavMenu.razor | 14 --
.../Components/Layout/NavMenu.razor.css | 85 ---------
.../Components/MDI/MDIStateHelper.cs | 30 ----
.../Components/MDI/MDITab.cs | 13 --
.../Components/MDI/MDITabCollection.cs | 55 ------
.../Components/Pages/Counter.razor | 26 ---
.../Components/Pages/Counter.razor.css | 37 ----
.../Components/Pages/Error.razor | 36 ----
.../Components/Pages/Form.razor | 27 ---
.../Components/Pages/Index.razor | 146 ----------------
.../Components/Pages/Weather.razor | 34 ----
.../DxBlazorApplication1.csproj | 10 --
.../Services/WeatherForecast.cs | 11 --
.../Services/WeatherForecastService.cs | 17 --
.../wwwroot/images/form.svg | 3 -
CS/DxBlazorApplication1/wwwroot/js/mdi.js | 27 ---
CS/blazor_multi_tab_ui.sln | 25 +++
.../Components/App.razor | 14 +-
.../Components/Layout/Drawer.razor | 19 +-
.../Components/Layout/Drawer.razor.css | 86 +++++++++
.../Components/Layout/MainLayout.razor | 51 ++++++
.../Components/Layout/MainLayout.razor.css | 59 +++++++
.../Components/Layout/NavMenu.razor | 7 +
.../Components/Layout/NavMenu.razor.css | 48 +++++
.../Components/MDI/MdiTabs.razor | 105 +++++++++++
.../Components/MDI/Tabs/Chart.razor | 60 +++++++
.../Components/MDI/Tabs/Customers.razor | 36 ++++
.../Components/MDI/Tabs/Orders.razor | 24 +++
.../Components/MDI/Tabs/Unknown.razor | 5 +
.../Components/MDI/TabsContextMenu.razor | 104 +++++++++++
.../Components/MDI/TabsContextMenu.razor.js | 31 ++++
.../Components/Pages/Index.razor | 10 ++
.../Components/Pages/Index.razor.css | 0
.../Components/Routes.razor | 0
.../Components/_Imports.razor | 6 +-
.../Models/CustomerModel.cs | 13 ++
.../Models/CustomersChartPoint.cs | 9 +
.../Models/MdiStateModel.cs | 8 +
CS/blazor_multi_tab_ui/Models/MdiTabModel.cs | 50 ++++++
CS/blazor_multi_tab_ui/Models/OrderModel.cs | 10 ++
.../NOTICE.txt | 0
.../Program.cs | 23 ++-
.../Services/DataService.cs | 56 ++++++
.../Services/MdiStateService.cs | 137 +++++++++++++++
.../Services/UrlGenerator.cs | 13 +-
.../appsettings.Development.json | 0
.../appsettings.json | 0
.../blazor_multi_tab_ui.csproj | 12 ++
.../wwwroot/css/bootstrap/bootstrap.css | 0
.../wwwroot/css/bootstrap/bootstrap.min.css | 0
.../wwwroot/css/open-iconic}/FONT-LICENSE.txt | 0
.../wwwroot/css/open-iconic}/ICON-LICENSE.txt | 0
.../wwwroot/css/open-iconic}/README.md | 0
.../font/css/open-iconic-bootstrap.min.css | 0
.../open-iconic}/font/fonts/open-iconic.eot | Bin
.../open-iconic}/font/fonts/open-iconic.otf | Bin
.../open-iconic}/font/fonts/open-iconic.svg | 0
.../open-iconic}/font/fonts/open-iconic.ttf | Bin
.../open-iconic}/font/fonts/open-iconic.woff | Bin
.../wwwroot/css/site.css | 38 ++--
.../wwwroot/css/theme-bs.css | 18 ++
.../wwwroot/css/theme-fluent.css | 43 +++++
.../wwwroot/favicon.ico | Bin
.../wwwroot/images/account/log-in-fluent.svg | 3 +
.../wwwroot/images/account/log-in.svg | 0
.../wwwroot/images/account/log-out-fluent.svg | 4 +
.../wwwroot/images/account/log-out.svg | 0
.../images/account/manage-email-fluent.svg | 3 +
.../wwwroot/images/account/manage-email.svg | 0
.../images/account/manage-password-fluent.svg | 3 +
.../images/account/manage-password.svg | 0
.../images/account/manage-personal-fluent.svg | 3 +
.../images/account/manage-personal.svg | 0
.../images/account/manage-profile-fluent.svg | 3 +
.../wwwroot/images/account/manage-profile.svg | 0
.../account/manage-two-factor-fluent.svg | 3 +
.../images/account/manage-two-factor.svg | 0
.../providers/facebook-logo-fluent.svg | 4 +
.../account/providers/facebook-logo.svg | 0
.../account/providers/google-logo-fluent.svg | 6 +
.../images/account/providers/google-logo.svg | 0
.../providers/microsoft-logo-fluent.svg | 6 +
.../account/providers/microsoft-logo.svg | 0
.../account/providers/x-logo-fluent.svg | 3 +
.../images/account/providers/x-logo.svg | 0
.../images/account/settings-fluent.svg | 3 +
.../wwwroot/images/account/settings.svg | 0
.../wwwroot/images/account/user-fluent.svg | 3 +
.../wwwroot/images/account/user.svg | 0
.../wwwroot/images/back-fluent.svg | 3 +
.../wwwroot/images/back.svg | 0
.../wwwroot/images/cards.svg | 4 +-
.../wwwroot/images/close-fluent.svg | 3 +
.../wwwroot/images/close.svg | 0
.../wwwroot/images/counter-fluent.svg | 4 +
.../wwwroot/images/counter.svg | 0
.../wwwroot/images/demos-fluent.svg | 3 +
.../wwwroot/images/demos.svg | 0
.../wwwroot/images/doc-fluent.svg | 3 +
.../wwwroot/images/doc.svg | 0
.../wwwroot/images/home-fluent.svg | 3 +
.../wwwroot/images/home.svg | 0
.../wwwroot/images/logo.svg | 0
.../wwwroot/images/menu-fluent.svg | 3 +
.../wwwroot/images/menu.svg | 0
.../wwwroot/images/weather-fluent.svg | 3 +
.../wwwroot/images/weather.svg | 0
111 files changed, 1152 insertions(+), 884 deletions(-)
delete mode 100644 CS/DxBlazorApplication1.sln
delete mode 100644 CS/DxBlazorApplication1/Components/Layout/Drawer.razor.css
delete mode 100644 CS/DxBlazorApplication1/Components/Layout/MainLayout.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Layout/MainLayout.razor.css
delete mode 100644 CS/DxBlazorApplication1/Components/Layout/NavMenu.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Layout/NavMenu.razor.css
delete mode 100644 CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs
delete mode 100644 CS/DxBlazorApplication1/Components/MDI/MDITab.cs
delete mode 100644 CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Counter.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Counter.razor.css
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Error.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Form.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Index.razor
delete mode 100644 CS/DxBlazorApplication1/Components/Pages/Weather.razor
delete mode 100644 CS/DxBlazorApplication1/DxBlazorApplication1.csproj
delete mode 100644 CS/DxBlazorApplication1/Services/WeatherForecast.cs
delete mode 100644 CS/DxBlazorApplication1/Services/WeatherForecastService.cs
delete mode 100644 CS/DxBlazorApplication1/wwwroot/images/form.svg
delete mode 100644 CS/DxBlazorApplication1/wwwroot/js/mdi.js
create mode 100644 CS/blazor_multi_tab_ui.sln
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Components/App.razor (62%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Components/Layout/Drawer.razor (54%)
create mode 100644 CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor.css
create mode 100644 CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor.css
create mode 100644 CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor.css
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/Tabs/Chart.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/Tabs/Customers.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/Tabs/Orders.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/Tabs/Unknown.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor
create mode 100644 CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js
create mode 100644 CS/blazor_multi_tab_ui/Components/Pages/Index.razor
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Components/Pages/Index.razor.css (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Components/Routes.razor (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Components/_Imports.razor (76%)
create mode 100644 CS/blazor_multi_tab_ui/Models/CustomerModel.cs
create mode 100644 CS/blazor_multi_tab_ui/Models/CustomersChartPoint.cs
create mode 100644 CS/blazor_multi_tab_ui/Models/MdiStateModel.cs
create mode 100644 CS/blazor_multi_tab_ui/Models/MdiTabModel.cs
create mode 100644 CS/blazor_multi_tab_ui/Models/OrderModel.cs
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/NOTICE.txt (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Program.cs (56%)
create mode 100644 CS/blazor_multi_tab_ui/Services/DataService.cs
create mode 100644 CS/blazor_multi_tab_ui/Services/MdiStateService.cs
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/Services/UrlGenerator.cs (84%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/appsettings.Development.json (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/appsettings.json (100%)
create mode 100644 CS/blazor_multi_tab_ui/blazor_multi_tab_ui.csproj
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/css/bootstrap/bootstrap.css (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/css/bootstrap/bootstrap.min.css (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/FONT-LICENSE.txt (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/ICON-LICENSE.txt (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/README.md (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/css/open-iconic-bootstrap.min.css (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/fonts/open-iconic.eot (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/fonts/open-iconic.otf (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/fonts/open-iconic.svg (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/fonts/open-iconic.ttf (100%)
rename CS/{DxBlazorApplication1/wwwroot/css/open.iconic => blazor_multi_tab_ui/wwwroot/css/open-iconic}/font/fonts/open-iconic.woff (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/css/site.css (66%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/css/theme-bs.css
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/css/theme-fluent.css
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/favicon.ico (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/log-in-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/log-in.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/log-out-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/log-out.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/manage-email.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/manage-password.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/manage-personal.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/manage-profile.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/manage-two-factor.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/providers/facebook-logo.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/providers/google-logo.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/providers/microsoft-logo.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/providers/x-logo.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/settings-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/settings.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/account/user-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/account/user.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/back-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/back.svg (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/cards.svg (98%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/close-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/close.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/counter-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/counter.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/demos-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/demos.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/doc-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/doc.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/home-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/home.svg (100%)
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/logo.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/menu-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/menu.svg (100%)
create mode 100644 CS/blazor_multi_tab_ui/wwwroot/images/weather-fluent.svg
rename CS/{DxBlazorApplication1 => blazor_multi_tab_ui}/wwwroot/images/weather.svg (100%)
diff --git a/CS/DxBlazorApplication1.sln b/CS/DxBlazorApplication1.sln
deleted file mode 100644
index 741863f..0000000
--- a/CS/DxBlazorApplication1.sln
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.12.35527.113
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DxBlazorApplication1", "DxBlazorApplication1\DxBlazorApplication1.csproj", "{59B78194-0176-4F83-89C4-6A5F871FB9D5}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {59B78194-0176-4F83-89C4-6A5F871FB9D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {59B78194-0176-4F83-89C4-6A5F871FB9D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {59B78194-0176-4F83-89C4-6A5F871FB9D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {59B78194-0176-4F83-89C4-6A5F871FB9D5}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/CS/DxBlazorApplication1/Components/Layout/Drawer.razor.css b/CS/DxBlazorApplication1/Components/Layout/Drawer.razor.css
deleted file mode 100644
index 270840b..0000000
--- a/CS/DxBlazorApplication1/Components/Layout/Drawer.razor.css
+++ /dev/null
@@ -1,37 +0,0 @@
-::deep .navigation-drawer {
- --dxbl-drawer-panel-footer-justify-content: center;
- height: 100vh;
- max-height: 100%;
-}
-
-::deep .panel-open:not(.mobile) .menu-button {
- display: none;
-}
-
-@media (max-width: 768px) {
- ::deep .panel-open:not(.mobile) .menu-button {
- display: inline-flex;
- }
-
- .mobile-drawer-closed .shading-copy {
- display: none;
- visibility: hidden;
- }
-
- ::deep .shading-copy {
- background-color: var(--dxbl-drawer-content-shading-bg);
- height: 100%;
- position: absolute;
- transition: opacity ease var(--dxbl-drawer-animation-duration);
- visibility: visible;
- width: 100%;
- z-index: 99;
- opacity: var(--dxbl-drawer-content-shading-opacity);
- }
-
- ::deep .panel-open .shading-copy {
- opacity: 0;
- visibility: unset;
- height: unset;
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor b/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor
deleted file mode 100644
index 032dd89..0000000
--- a/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor
+++ /dev/null
@@ -1,45 +0,0 @@
-@inherits LayoutComponentBase
-@inject NavigationManager NavigationManager
-
-
-
-
-
-
-
-
-
-
- @Body
-
-
-
-
-
-@code {
- public IMenuItemInfo? ClickedMenuItemName { get; set; }
-
- private void OnItemClick(MenuItemClickEventArgs e){
- ClickedMenuItemName = e.ItemInfo;
- }
-
- [SupplyParameterFromQuery(Name = UrlGenerator.ToggleSidebarName)]
- public bool ToggledSidebar { get; set; }
-
- private RenderFragment drawerHeader => @;
-
- private RenderFragment drawerFooter => @
-
-
-
-
-
-
-
;
-
-
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor.css b/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor.css
deleted file mode 100644
index 20dfd2d..0000000
--- a/CS/DxBlazorApplication1/Components/Layout/MainLayout.razor.css
+++ /dev/null
@@ -1,165 +0,0 @@
-.page {
- height: 100%;
- font-family: var(--bs-font-sans-serif);
-}
-
-::deep .navigation-drawer > .dxbl-drawer-panel {
- background-image: linear-gradient(180deg, var(--bs-primary) 0%, var(--bs-black) 150%);
-}
-
-::deep .dxbl-drawer .dxbl-drawer-content {
- height: 100vh;
- overflow: auto;
-}
-
-::deep .dxbl-drawer > .dxbl-drawer-panel .dxbl-drawer-header {
- border-bottom: none;
- padding: 2rem 1rem;
-}
-
-::deep .dxbl-drawer-panel .dxbl-drawer-header .navigation-drawer-header {
- width: 100%;
- display: flex;
- justify-content: space-between;
-}
-
-::deep .dxbl-drawer > .dxbl-drawer-panel > .dxbl-drawer-body {
- --dxbl-drawer-panel-body-padding-x: 0;
- --dxbl-drawer-panel-body-padding-y: 1rem;
-}
-
-::deep .dxbl-drawer > .dxbl-drawer-panel .dxbl-drawer-footer {
- --dxbl-drawer-panel-footer-justify-content: center;
- border-top: none;
- padding-bottom: 1.5rem;
-}
-
-::deep .content {
- overflow: auto;
- display: flex;
- flex-direction: column;
-}
-
-::deep .icon {
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- mask-position: center center;
- -webkit-mask-mask-position: center center;
- width: 1rem;
- height: 1rem;
- background-repeat: no-repeat;
- background-color: var(--dxbl-btn-color);
-}
-
-::deep .icon-back {
- -webkit-mask-image: url("images/back.svg");
- mask-image: url("images/back.svg");
-}
-
-::deep .icon-close {
- -webkit-mask-image: url("images/close.svg");
- mask-image: url("images/close.svg");
-}
-
-::deep .icon-menu {
- -webkit-mask-image: url("images/menu.svg");
- mask-image: url("images/menu.svg");
-}
-
-::deep .icon-log-in {
- -webkit-mask-image: url("images/account/log-in.svg");
- mask-image: url("images/account/log-in.svg");
-}
-
-::deep .icon-log-out {
- -webkit-mask-image: url("images/account/log-out.svg");
- mask-image: url("images/account/log-out.svg");
-}
-
-::deep .icon-user {
- -webkit-mask-image: url("images/account/user.svg");
- mask-image: url("images/account/user.svg");
-}
-
-::deep .docs-icon {
- mask-image: url("images/doc.svg");
- -webkit-mask-image: url("images/doc.svg");
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- background-color: var(--dxbl-btn-color);
-}
-
-::deep .demos-icon {
- mask-image: url("images/demos.svg");
- -webkit-mask-image: url("images/demos.svg");
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- background-color: var(--dxbl-btn-color);
-}
-
-::deep .footer-button:hover .demos-icon {
- background-color: var(--dxbl-btn-hover-color);
-}
-
-::deep .footer-button:hover .docs-icon {
- background-color: var(--dxbl-btn-hover-color);
-}
-
-::deep .menu-button:hover .icon {
- background-color: var(--dxbl-btn-hover-color);
-}
-
-::deep .menu-button-nav:hover .icon {
- background-color: var(--dxbl-btn-hover-color);
-}
-
-.panel-open .menu-button {
- display: inline-flex;
-}
-
-.menu-button-nav {
- background-image: url("images/close.svg");
- width: 1.875rem;
- height: 1.875rem;
-}
-
-.nav-buttons-container {
- display: flex;
- gap: 10px;
- padding: 2rem 1rem;
-}
-
- .nav-buttons-container ::deep .menubutton-float-end {
- margin-left: auto;
- }
-
- .nav-buttons-container ::deep .dxbl-btn-icon-only {
- --dxbl-btn-padding-x: 0.75rem;
- --dxbl-btn-padding-y: 0.25rem;
- }
-
-::deep .navigation-drawer > .dxbl-drawer-panel {
- display: flex;
-}
-
-::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
- display: none;
-}
-
-::deep .navigation-drawer.mobile > .dxbl-drawer-shading {
- display: none;
-}
-
-@media (max-width: 768px) {
- ::deep .navigation-drawer > .dxbl-drawer-panel {
- display: none;
- }
-
- ::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
- display: flex;
- }
-
- ::deep .navigation-drawer.mobile > .dxbl-drawer-shading {
- display: block;
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor b/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor
deleted file mode 100644
index 526c61a..0000000
--- a/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-@code{
- [Parameter]
- public EventCallback ItemClick { get; set; }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor.css b/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor.css
deleted file mode 100644
index fa90228..0000000
--- a/CS/DxBlazorApplication1/Components/Layout/NavMenu.razor.css
+++ /dev/null
@@ -1,85 +0,0 @@
-#sidebar {
- min-width: 15rem;
- max-width: 15rem;
- transition: transform 0.1s ease-out;
- height: 100%;
- max-height: 100%;
- display: block;
- background: inherit;
-}
-
-.logo {
- text-align: center;
-}
-
-::deep .menu.display-mobile {
- margin-bottom: 2rem;
-}
-
-::deep .menu.display-iam {
- margin-bottom: 2rem;
-}
-
-::deep .menu {
- background-color: inherit;
-}
-
- ::deep .menu .dxbl-menu-item-list {
- gap: 0.5rem;
- }
-
-::deep .menu-item {
- color: var(--bs-white);
-}
-
-::deep .icon {
- width: 1rem;
- height: 1rem;
- background-position: center;
- background-repeat: no-repeat;
- margin-left: 0.5rem;
-}
-
-::deep .home-icon {
- background-image: url("images/home.svg");
-}
-
-::deep .weather-icon {
- background-image: url("images/weather.svg");
-}
-
-::deep .form-icon {
- background-image: url("images/form.svg");
-}
-
-::deep .counter-icon {
- background-image: url("images/counter.svg");
-}
-
-::deep .settings-icon {
- background-image: url("images/account/settings.svg");
-}
-
-::deep .log-in-icon {
- background-image: url("images/account/log-in.svg");
-}
-
-::deep .log-out-icon {
- background-image: url("images/account/log-out.svg");
-}
-
-::deep .user-icon {
- background-image: url("images/account/user.svg");
-}
-
-@media (max-width: 768px) {
- #sidebar {
- min-width: inherit;
- max-width: inherit;
- display: block;
- }
-
- .logo {
- text-align: inherit;
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs b/CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs
deleted file mode 100644
index 8119bc0..0000000
--- a/CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Microsoft.JSInterop;
-using System.Text.Json;
-
-namespace DxBlazorApplication1.Components.MDI
-{
- public class MDIStateHelper {
- private const string LOCAL_STORAGE_KEY = "MDI-Layout";
- private readonly IJSRuntime js;
-
- public MDIStateHelper(IJSRuntime js) {
- this.js = js;
- }
-
- public async Task SaveLayoutToLocalStorageAsync(MDITabCollection tabData) {
- try {
- var json = JsonSerializer.Serialize(tabData);
- await js.InvokeVoidAsync("localStorage.setItem", LOCAL_STORAGE_KEY, json);
- }
- catch { return; }
- }
-
- public async Task LoadLayoutFromLocalStorageAsync() {
- try {
- var json = await js.InvokeAsync("localStorage.getItem", LOCAL_STORAGE_KEY);
- return JsonSerializer.Deserialize(json);
- }
- catch { return null; }
- }
- }
-}
diff --git a/CS/DxBlazorApplication1/Components/MDI/MDITab.cs b/CS/DxBlazorApplication1/Components/MDI/MDITab.cs
deleted file mode 100644
index a354e7b..0000000
--- a/CS/DxBlazorApplication1/Components/MDI/MDITab.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace DxBlazorApplication1.Components.MDI
-{
- public class MDITab {
- public string Text { get; set; }
- public int VisibleIndex { get; set; }
- public bool Visible { get; set; }
- public MDITab(string text, int visibleIndex, bool visible) {
- Text = text;
- VisibleIndex = visibleIndex;
- Visible = visible;
- }
- }
-}
diff --git a/CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs b/CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs
deleted file mode 100644
index cc4bbbe..0000000
--- a/CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using DevExpress.Blazor;
-using System.Collections;
-using System.Text.Json.Serialization;
-
-namespace DxBlazorApplication1.Components.MDI
-{
- public class MDITabCollection {
- [JsonInclude]
- private List tabs;
-
- public int Count => tabs.Count;
-
- public MDITabCollection() {
- tabs = new List();
- }
-
- public MDITabCollection(IEnumerable tabs) {
- this.tabs = tabs.Select(t => new MDITab(t.Text,
- t.VisibleIndex == -1 ? tabs.ToList().IndexOf(t) : t.VisibleIndex,
- t.Visible)).ToList();
- }
-
- public void SetVisibleAllTabs(bool visible) {
- tabs.ForEach((t) => t.Visible = visible);
- }
-
- public string? GetTabTextByTabInfo(ITabInfo tabInfo) {
- return tabs.FirstOrDefault(t => t.Text == tabInfo.Text)?.Text;
- }
-
- public int GetVisibleIndexByTabText(string? text) {
- return tabs.Find(t => t.Text == text)?.VisibleIndex ?? -1;
- }
-
- public bool GetVisibleByTabText(string? text) {
- return tabs.Find(t => t.Text == text)?.Visible ?? true;
- }
-
- public void SetVisibleByTabText(string? text, bool visible) {
- var tab = tabs.Find(t => t.Text == text);
- if(tab != null)
- {
- tab.Visible = visible;
- }
- }
-
- public void SetVisibleIndexByTabText(string? text, int visibleIndex) {
- var tab = tabs.Find(t => t.Text == text);
- if(tab != null)
- {
- tab.VisibleIndex = visibleIndex;
- }
- }
- }
-}
diff --git a/CS/DxBlazorApplication1/Components/Pages/Counter.razor b/CS/DxBlazorApplication1/Components/Pages/Counter.razor
deleted file mode 100644
index d5c756a..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Counter.razor
+++ /dev/null
@@ -1,26 +0,0 @@
-@page "/counter"
-@rendermode InteractiveServer
-
-Counter
-
-
-
-
- @currentCount
-
-
- current count
-
-
-
-
Click me
-
-
-@code {
- private int currentCount = 0;
-
- private void IncrementCount()
- {
- currentCount++;
- }
-}
diff --git a/CS/DxBlazorApplication1/Components/Pages/Counter.razor.css b/CS/DxBlazorApplication1/Components/Pages/Counter.razor.css
deleted file mode 100644
index 08d0ae2..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Counter.razor.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.counter-block {
- display: flex;
- padding: 2.5rem 1.5rem 1.5rem 1.5rem;
- flex-direction: column;
- border-radius: 1rem;
- gap: 1.5rem;
- justify-content: center;
- align-items: center;
- width: 16.875rem;
- height: 17rem;
- position: relative;
-}
-
- .counter-block .counter-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
- }
-
- .counter-block .counter-count {
- font-size: 7.5rem;
- font-weight: 400;
- line-height: 7.75rem;
- }
-
- .counter-block .counter-block-back {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: var(--bs-body-color);
- opacity: 0.05;
- border-radius: 1rem;
- z-index: -2;
- }
diff --git a/CS/DxBlazorApplication1/Components/Pages/Error.razor b/CS/DxBlazorApplication1/Components/Pages/Error.razor
deleted file mode 100644
index 576cc2d..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Error.razor
+++ /dev/null
@@ -1,36 +0,0 @@
-@page "/Error"
-@using System.Diagnostics
-
-Error
-
-Error.
-An error occurred while processing your request.
-
-@if (ShowRequestId)
-{
-
- Request ID: @RequestId
-
-}
-
-Development Mode
-
- Swapping to Development environment will display more detailed information about the error that occurred.
-
-
- The Development environment shouldn't be enabled for deployed applications.
- It can result in displaying sensitive information from exceptions to end users.
- For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
- and restarting the app.
-
-
-@code{
- [CascadingParameter]
- private HttpContext? HttpContext { get; set; }
-
- private string? RequestId { get; set; }
- private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
-
- protected override void OnInitialized() =>
- RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
-}
diff --git a/CS/DxBlazorApplication1/Components/Pages/Form.razor b/CS/DxBlazorApplication1/Components/Pages/Form.razor
deleted file mode 100644
index b87fd23..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Form.razor
+++ /dev/null
@@ -1,27 +0,0 @@
-@page "/form"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-@code {
- DateTime Today = DateTime.Today;
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Pages/Index.razor b/CS/DxBlazorApplication1/Components/Pages/Index.razor
deleted file mode 100644
index a7afff0..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Index.razor
+++ /dev/null
@@ -1,146 +0,0 @@
-@page "/"
-@using System.Text.Json
-@using DxBlazorApplication1.Components.MDI
-@inject IJSRuntime JS
-@inject MDIStateHelper StateHelper
-
-Multi-Tab Interface
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-@code {
- [CascadingParameter(Name = "ClickedMenuItemName")]
- public IMenuItemInfo? ClickedMenuItem { get; set; }
-
- private ElementReference divContainer;
- private DxContextMenu? menu;
- private DxTabs? tabs;
-
- private MDITabCollection collection = new MDITabCollection();
-
- private int activeTabIndex;
- private string? clickedTabText;
- private bool isReordering;
-
- [JSInvokable]
- public async Task ShowContextMenu(MouseEventArgs e, string tabText) {
- clickedTabText = tabText;
- if(menu != null) {
- await menu.ShowAsync(e);
- }
- }
-
- #region DxTabs Event Handlers
-
- private async Task OnTabClosing(TabCloseEventArgs e) {
- collection.SetVisibleByTabText(e.TabInfo.Text, false);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
-
- private async Task OnTabReordering(TabReorderEventArgs e) {
- var vi1 = collection.GetVisibleIndexByTabText(e.FromTabInfo.Text);
- var vi2 = collection.GetVisibleIndexByTabText(e.ToTabInfo.Text);
- collection.SetVisibleIndexByTabText(e.FromTabInfo.Text, vi2);
- collection.SetVisibleIndexByTabText(e.ToTabInfo.Text, vi1);
- isReordering = true;
- }
-
- #endregion
-
- #region DxContextMenuItem.Click Event Handlers
-
- private async Task CloseTab() {
- collection.SetVisibleByTabText(clickedTabText, false);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
-
- private async Task CloseAllTabs() {
- collection.SetVisibleAllTabs(false);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
-
- private async Task CloseOtherTabs() {
- collection.SetVisibleAllTabs(false);
- collection.SetVisibleByTabText(clickedTabText, true);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
-
- private async Task RestoreAllTabs() {
- collection.SetVisibleAllTabs(true);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
-
- #endregion
-
- #region Life-Cycle Event Handlers
-
- protected override async Task OnParametersSetAsync() {
- await base.OnParametersSetAsync();
-
- if(ClickedMenuItem is null)
- return;
-
- if(!collection.GetVisibleByTabText(ClickedMenuItem.Name)) {
- collection.SetVisibleByTabText(ClickedMenuItem.Name, true);
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- }
- activeTabIndex = collection.GetVisibleIndexByTabText(ClickedMenuItem.Name);
- }
-
- protected override async Task OnAfterRenderAsync(bool firstRender) {
- if(firstRender) {
- var cacheCollection = await StateHelper.LoadLayoutFromLocalStorageAsync();
- if(cacheCollection is null || cacheCollection.Count == 0) {
- collection = new MDITabCollection(tabs!.GetOrderedTabs());
- }
- else {
- collection = cacheCollection;
- }
- await InvokeAsync(StateHasChanged);
- }
-
- var dotNetInstance = DotNetObjectReference.Create(this);
- foreach(ITabInfo tab in tabs!.GetOrderedTabs()) {
- await JS.InvokeVoidAsync("addContextMenuHandler", divContainer, tab.CssClass, collection.GetTabTextByTabInfo(tab), dotNetInstance);
- }
-
- if(isReordering) {
- await StateHelper.SaveLayoutToLocalStorageAsync(collection);
- isReordering = false;
- }
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Pages/Weather.razor b/CS/DxBlazorApplication1/Components/Pages/Weather.razor
deleted file mode 100644
index 5518c06..0000000
--- a/CS/DxBlazorApplication1/Components/Pages/Weather.razor
+++ /dev/null
@@ -1,34 +0,0 @@
-@page "/weather"
-
-@using DxBlazorApplication1.Services
-@attribute [StreamRendering(true)]
-@rendermode InteractiveServer
-@inject WeatherForecastService ForecastService
-
-Weather
-
-@if (forecasts == null)
-{
- Loading...
-}
-else
-{
-
-
-
-
-
-
-
-
-}
-
-@code {
- private WeatherForecast[]? forecasts;
-
- protected override async Task OnInitializedAsync()
- {
- forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/DxBlazorApplication1.csproj b/CS/DxBlazorApplication1/DxBlazorApplication1.csproj
deleted file mode 100644
index 965f5d0..0000000
--- a/CS/DxBlazorApplication1/DxBlazorApplication1.csproj
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- net8.0
- enable
- enable
-
-
-
-
-
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Services/WeatherForecast.cs b/CS/DxBlazorApplication1/Services/WeatherForecast.cs
deleted file mode 100644
index 7fec132..0000000
--- a/CS/DxBlazorApplication1/Services/WeatherForecast.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace DxBlazorApplication1.Services {
- public class WeatherForecast {
- public DateOnly Date { get; set; }
-
- public int TemperatureC { get; set; }
-
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-
- public string? Summary { get; set; }
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Services/WeatherForecastService.cs b/CS/DxBlazorApplication1/Services/WeatherForecastService.cs
deleted file mode 100644
index cd5e2bf..0000000
--- a/CS/DxBlazorApplication1/Services/WeatherForecastService.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace DxBlazorApplication1.Services {
- public class WeatherForecastService {
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
-
- public Task GetForecastAsync(DateOnly startDate) {
- var rng = new Random();
- return Task.FromResult(Enumerable.Range(1, 20).Select(index => new WeatherForecast {
- Date = startDate.AddDays(index),
- TemperatureC = rng.Next(-20, 55),
- Summary = Summaries[rng.Next(Summaries.Length)]
- }).ToArray());
- }
- }
-}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/wwwroot/images/form.svg b/CS/DxBlazorApplication1/wwwroot/images/form.svg
deleted file mode 100644
index c2e491e..0000000
--- a/CS/DxBlazorApplication1/wwwroot/images/form.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/CS/DxBlazorApplication1/wwwroot/js/mdi.js b/CS/DxBlazorApplication1/wwwroot/js/mdi.js
deleted file mode 100644
index eea9fae..0000000
--- a/CS/DxBlazorApplication1/wwwroot/js/mdi.js
+++ /dev/null
@@ -1,27 +0,0 @@
-function addContextMenuHandler(container, tabClass, text, dotNetObject) {
- var tabElement = container.getElementsByClassName(tabClass)[0];
- if (!tabElement || tabElement.hasAttribute("cp_ctx")) return;
- tabElement.setAttribute("cp_ctx", true);
- tabElement.addEventListener('contextmenu', (event) => {
- event.preventDefault();
- let eventArgs = {
- clientX: event.clientX,
- clientY: event.clientY,
- screenX: event.screenX,
- screenY: event.screenY,
- offsetX: event.offsetX,
- offsetY: event.offsetY,
- pageX: event.pageX,
- pageY: event.pageY,
- button: event.button,
- buttons: event.buttons,
- ctrlKey: event.ctrlKey,
- shiftKey: event.shiftKey,
- altKey: event.altKey,
- metaKey: event.metaKey,
- detail: event.detail,
- type: event.type
- };
- dotNetObject.invokeMethodAsync("ShowContextMenu", eventArgs, text);
- });
-};
\ No newline at end of file
diff --git a/CS/blazor_multi_tab_ui.sln b/CS/blazor_multi_tab_ui.sln
new file mode 100644
index 0000000..a86be68
--- /dev/null
+++ b/CS/blazor_multi_tab_ui.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36202.13 d17.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "blazor_multi_tab_ui", "blazor_multi_tab_ui\blazor_multi_tab_ui.csproj", "{8936C643-54B5-0391-8F8D-B0D3E8670BDC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8936C643-54B5-0391-8F8D-B0D3E8670BDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8936C643-54B5-0391-8F8D-B0D3E8670BDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8936C643-54B5-0391-8F8D-B0D3E8670BDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8936C643-54B5-0391-8F8D-B0D3E8670BDC}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8D1D4A02-3C87-4CFF-99EC-856B074165D4}
+ EndGlobalSection
+EndGlobal
diff --git a/CS/DxBlazorApplication1/Components/App.razor b/CS/blazor_multi_tab_ui/Components/App.razor
similarity index 62%
rename from CS/DxBlazorApplication1/Components/App.razor
rename to CS/blazor_multi_tab_ui/Components/App.razor
index 637bd5d..c39a505 100644
--- a/CS/DxBlazorApplication1/Components/App.razor
+++ b/CS/blazor_multi_tab_ui/Components/App.razor
@@ -2,25 +2,25 @@
@inject IFileVersionProvider FileVersionProvider
-
+
-
-
+ @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties => properties.AddFilePaths($"css/theme-fluent.css")))
@DxResourceManager.RegisterScripts()
-
+
-
+
-@code{
+@code {
private string AppendVersion(string path) => FileVersionProvider.AddFileVersionToPath("/", path);
-}
\ No newline at end of file
+
+}
diff --git a/CS/DxBlazorApplication1/Components/Layout/Drawer.razor b/CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor
similarity index 54%
rename from CS/DxBlazorApplication1/Components/Layout/Drawer.razor
rename to CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor
index 8ee9250..020fa2d 100644
--- a/CS/DxBlazorApplication1/Components/Layout/Drawer.razor
+++ b/CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor
@@ -2,17 +2,30 @@
@inject NavigationManager NavigationManager
-
+
@DrawerBody
-
+
@DrawerBody
-
+
@DrawerTarget
diff --git a/CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor.css b/CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor.css
new file mode 100644
index 0000000..7fd4323
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Layout/Drawer.razor.css
@@ -0,0 +1,86 @@
+::deep .navigation-drawer {
+ height: 100vh;
+ max-height: 100%;
+}
+
+::deep .panel-open:not(.mobile) .menu-button {
+ display: none;
+}
+
+::deep .navigation-drawer > .dxbl-drawer-panel {
+ background-image: linear-gradient(180deg, var(--bs-primary, var(--DS-primary-90)) 0%, var(--bs-black, #000) 150%);
+}
+
+::deep .navigation-drawer > .dxbl-drawer-content {
+ height: 100vh;
+ overflow: auto;
+}
+
+::deep .navigation-drawer > .dxbl-drawer-panel > .dxbl-drawer-header {
+ border-bottom: none;
+ padding: 2rem 1rem;
+ background: none;
+}
+
+ ::deep .navigation-drawer > .dxbl-drawer-panel > .dxbl-drawer-header > .navigation-drawer-header {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ }
+
+::deep .navigation-drawer > .dxbl-drawer-panel > .dxbl-drawer-body {
+ --dxbl-drawer-panel-body-padding-x: 0;
+ --dxbl-drawer-panel-body-padding-y: 1rem;
+}
+
+::deep .navigation-drawer > .dxbl-drawer-panel > .dxbl-drawer-footer {
+ --dxbl-drawer-panel-footer-justify-content: center;
+ border-top: none;
+ padding-bottom: 1.5rem;
+ width: 240px;
+ background: none;
+}
+
+::deep .navigation-drawer > .dxbl-drawer-panel {
+ display: flex;
+}
+
+::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
+ display: none;
+}
+
+::deep .navigation-drawer > .dxbl-drawer-content > .navigation-drawer-shading {
+ display: none;
+}
+
+@media (max-width: 768px) {
+ ::deep .panel-open:not(.mobile) .menu-button {
+ display: inline-flex;
+ }
+
+ ::deep .navigation-drawer > .dxbl-drawer-panel {
+ display: none;
+ }
+
+ ::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
+ display: flex;
+ }
+
+ ::deep .navigation-drawer > .dxbl-drawer-content > .navigation-drawer-shading {
+ background-color: var(--dxbl-drawer-content-shading-bg);
+ display: block;
+ height: 100%;
+ position: absolute;
+ transition: ease var(--dxbl-drawer-animation-duration);
+ transition-property: opacity, visibility;
+ visibility: visible;
+ width: 100%;
+ z-index: 99;
+ opacity: var(--dxbl-drawer-content-shading-opacity);
+ }
+
+ ::deep .navigation-drawer.mobile.panel-closed .navigation-drawer-shading {
+ opacity: 0;
+ visibility: hidden;
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor b/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..67093c2
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor
@@ -0,0 +1,51 @@
+@inherits LayoutComponentBase
+@using blazor_multi_tab_ui.Services;
+@inject NavigationManager NavigationManager
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (new Uri(NavigationManager.Uri).LocalPath != "/")
+ {
+
+
+
+ }
+
+
+ @Body
+
+
+
+
+
+@code {
+ [SupplyParameterFromQuery(Name = UrlGenerator.ToggleSidebarName)]
+ public bool ToggledSidebar { get; set; }
+
+ private RenderFragment drawerHeader => @;
+
+ private RenderFragment drawerFooter => @
+
+
+
+
+
+
+
;
+
+}
\ No newline at end of file
diff --git a/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor.css b/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor.css
new file mode 100644
index 0000000..1bbe602
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Layout/MainLayout.razor.css
@@ -0,0 +1,59 @@
+.page {
+ height: 100%;
+ font-family: var(--bs-font-sans-serif, var(--DS-font-family-sans-serif));
+}
+
+.logo {
+ text-align: center;
+}
+
+::deep .icon-back {
+ --icon-mask-image: var(--icon-back-mask-image);
+}
+
+::deep .icon-close {
+ --icon-mask-image: var(--icon-close-mask-image);
+}
+
+::deep .icon-menu {
+ --icon-mask-image: var(--icon-menu-mask-image);
+}
+
+
+::deep .docs-icon {
+ --icon-mask-image: var(--icon-docs-mask-image);
+}
+
+::deep .demos-icon {
+ --icon-mask-image: var(--icon-demos-mask-image);
+}
+
+::deep .footer-button:hover .demos-icon {
+ background-color: var(--dxbl-btn-hover-color);
+}
+
+::deep .footer-button:hover .docs-icon {
+ background-color: var(--dxbl-btn-hover-color);
+}
+
+::deep .menu-button:hover .icon {
+ background-color: var(--dxbl-btn-hover-color);
+}
+
+::deep .menu-button-nav:hover .icon {
+ background-color: var(--dxbl-btn-hover-color);
+}
+
+.panel-open .menu-button {
+ display: inline-flex;
+}
+
+.nav-buttons-container {
+ display: flex;
+ gap: 10px;
+ padding: 2rem 1rem;
+}
+
+ .nav-buttons-container ::deep .menubutton-float-end {
+ margin-left: auto;
+ }
diff --git a/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor b/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor
new file mode 100644
index 0000000..4404c7c
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor.css b/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor.css
new file mode 100644
index 0000000..d251645
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Layout/NavMenu.razor.css
@@ -0,0 +1,48 @@
+::deep .menu.display-mobile {
+ margin-bottom: 2rem;
+}
+
+::deep .menu {
+ --dxbl-menu-bottom-left-border-radius: 0;
+ --dxbl-menu-bottom-right-border-radius: 0;
+ --dxbl-menu-top-left-border-radius: 0;
+ --dxbl-menu-top-right-border-radius: 0;
+ background-color: inherit;
+}
+
+ ::deep .menu .dxbl-menu-item-list {
+ gap: 0.5rem;
+ }
+
+::deep .menu-item {
+ color: var(--bs-white, #fff);
+}
+
+::deep .icon {
+ margin-left: 0.5rem;
+}
+
+::deep .home-icon {
+ --icon-mask-image: var(--icon-home-mask-image);
+}
+
+::deep .weather-icon {
+ --icon-mask-image: var(--icon-weather-mask-image);
+}
+
+::deep .counter-icon {
+ --icon-mask-image: var(--icon-counter-mask-image);
+}
+
+
+@media (max-width: 768px) {
+ #sidebar {
+ min-width: inherit;
+ max-width: inherit;
+ display: block;
+ }
+
+ .logo {
+ text-align: inherit;
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor b/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
new file mode 100644
index 0000000..26586c1
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
@@ -0,0 +1,105 @@
+@using blazor_multi_tab_ui.Components.MDI.Tabs
+@using blazor_multi_tab_ui.Models
+@using blazor_multi_tab_ui.Services
+
+@inject MdiStateService stateService
+
+
+ @if (tabsState is not null && tabsState.Tabs is not null && tabsState.Tabs.Count(t => t.Visible) > 0)
+ {
+ var tabsCollection = tabsState.Tabs;
+ var activeTabIndex = tabsState.ActiveTabIndex;
+
+
+ @for (int i = 0; i < tabsCollection.Count; i++)
+ {
+ var tabModel = tabsCollection[i];
+
+
+ @if (stateService.TryGetType(tabModel.TabTypeName, out var type))
+ {
+
+ }
+ else
+ {
+
+ }
+
+
+
+ }
+
+
+
+ }
+
+
+
+@if (tabsState is not null && tabsState.Tabs is not null && tabsState.Tabs.Count(t => !t.Visible) > 0)
+{
+ !t.Visible)})")" Click="@(() => stateService.SetAllTabsVisible(true))">
+}
+
+@code {
+ DxTabs? tabsRef;
+ TabsContextMenu? contextMenu;
+ MdiStateModel? tabsState;
+ string TabCssClass = "tab-with-context-menu";
+
+ async Task ShowSales()
+ {
+ await stateService.AddTab($"Sales Chart", nameof(Chart));
+ }
+ async Task ShowDefaultTabs()
+ {
+ await stateService.AddTab($"Customers", nameof(Customers));
+ }
+
+ protected async override Task OnInitializedAsync()
+ {
+ stateService.OnTabsChanged += Redraw;
+ tabsState = await stateService.LoadState();
+ await base.OnInitializedAsync();
+ }
+ protected async override Task OnAfterRenderAsync(bool firstRender)
+ {
+ await stateService.SaveState();
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ private void OnTabClosing(TabCloseEventArgs e)
+ {
+ if (tabsState is null) return;
+ stateService.SetTabVisible(e.TabInfo.VisibleIndex, false);
+ }
+
+ private void OnTabReordering(TabReorderEventArgs e)
+ {
+
+ if (tabsState is null) return;
+ stateService.ReorderTabs(e.FromTabInfo.VisibleIndex, e.ToTabInfo.VisibleIndex);
+ }
+
+ private async Task OnActiveTabIndexChanged(int newIndex)
+ {
+ await stateService.SetActiveTabIndex(newIndex);
+ }
+
+ async void Redraw()
+ {
+ await InvokeAsync(StateHasChanged);
+ if (contextMenu is null) return;
+ await contextMenu.AddContextMenuHandler();
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Chart.razor b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Chart.razor
new file mode 100644
index 0000000..6c246c6
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Chart.razor
@@ -0,0 +1,60 @@
+@using blazor_multi_tab_ui.Models
+@using blazor_multi_tab_ui.Services
+@inject DataService dataService
+@inject MdiStateService stateService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ List? Data;
+
+ protected override Task OnInitializedAsync()
+ {
+ Data = dataService.GetCustomersChartData();
+ return base.OnInitializedAsync();
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Customers.razor b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Customers.razor
new file mode 100644
index 0000000..bc2d3ea
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Customers.razor
@@ -0,0 +1,36 @@
+@using blazor_multi_tab_ui.Models
+@using blazor_multi_tab_ui.Services
+@inject DataService dataService
+@inject MdiStateService stateService
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ List? Data;
+
+ async void OpenOrders(CustomerModel customer)
+ {
+ var Parameters = new Dictionary
+ {
+ { "CustomerId", customer.Id },
+ };
+ await stateService.AddTab($"{customer.Name} Orders", nameof(Orders), Parameters);
+ }
+
+ protected override Task OnInitializedAsync()
+ {
+ Data = dataService.GetCustomers();
+ return base.OnInitializedAsync();
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Orders.razor b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Orders.razor
new file mode 100644
index 0000000..3679fd7
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Orders.razor
@@ -0,0 +1,24 @@
+@using blazor_multi_tab_ui.Models
+@using blazor_multi_tab_ui.Services
+@inject DataService dataService
+
+
+
+
+
+
+
+
+
+@code {
+ [Parameter]
+ public int CustomerId { get; set; }
+
+ List? Data;
+
+ protected override Task OnInitializedAsync()
+ {
+ Data = dataService.GetOrdersByCustomerId(CustomerId);
+ return base.OnInitializedAsync();
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Unknown.razor b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Unknown.razor
new file mode 100644
index 0000000..a373d2c
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/Tabs/Unknown.razor
@@ -0,0 +1,5 @@
+Unknown Tab Type
+
+@code {
+
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor b/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor
new file mode 100644
index 0000000..8254175
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor
@@ -0,0 +1,104 @@
+@using blazor_multi_tab_ui.Services
+@inject IJSRuntime JS
+@inject MdiStateService stateService;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private DxContextMenu? menu;
+ private IJSObjectReference? module;
+ private int? targetTabVisibleIndex;
+
+ [Parameter]
+ public string? TabSelector { get; set; }
+
+ public async Task AddContextMenuHandler()
+ {
+ if (module is null) return;
+ await module.InvokeVoidAsync(
+ "addContextMenuHandler",
+ TabSelector,
+ DotNetObjectReference.Create(this));
+ }
+
+ protected async override Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender) module = await JS.InvokeAsync(
+ "import", "./Components/MDI/TabsContextMenu.razor.js");
+
+ await AddContextMenuHandler();
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ [JSInvokable]
+ public async Task ShowContextMenu(MouseEventArgs e, int tabVisibleIndex)
+ {
+ targetTabVisibleIndex = tabVisibleIndex;
+ if (menu != null)
+ {
+ await menu.ShowAsync(e);
+ }
+ }
+
+ private void HideTab()
+ {
+ if (targetTabVisibleIndex is not null)
+ stateService.SetTabVisible((int)targetTabVisibleIndex, false);
+ }
+
+ private async Task HideAllTabs()
+ {
+ await stateService.SetAllTabsVisible(false);
+ }
+
+ private async Task HideOtherTabs()
+ {
+ if (targetTabVisibleIndex is null) return;
+ await stateService.SetAllTabsVisible(false);
+ stateService.SetTabVisible((int)targetTabVisibleIndex, true);
+ }
+
+ private async Task RestoreHiddenTabs()
+ {
+ await stateService.SetAllTabsVisible(true);
+ }
+
+ private async Task CloseTab()
+ {
+ if (targetTabVisibleIndex is not null)
+ await stateService.RemoveTab((int)targetTabVisibleIndex);
+ }
+
+ private async Task CloseAllTabs()
+ {
+ await stateService.RemoveAllTabs();
+ }
+ private async Task CloseOtherTabs()
+ {
+ if (targetTabVisibleIndex is not null)
+ await stateService.RemoveAllButTab((int)targetTabVisibleIndex);
+ }
+
+}
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js b/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js
new file mode 100644
index 0000000..de94e6a
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js
@@ -0,0 +1,31 @@
+export function addContextMenuHandler(tabSelector, dotNetObject) {
+ var tabElements = document.querySelectorAll(tabSelector);
+ if (!tabElements) return;
+
+ tabElements.forEach(tabElement => {
+
+ tabElement.addEventListener('contextmenu', (event) => {
+ event.preventDefault();
+ let eventArgs = {
+ clientX: event.clientX,
+ clientY: event.clientY,
+ screenX: event.screenX,
+ screenY: event.screenY,
+ offsetX: event.offsetX,
+ offsetY: event.offsetY,
+ pageX: event.pageX,
+ pageY: event.pageY,
+ button: event.button,
+ buttons: event.buttons,
+ ctrlKey: event.ctrlKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ metaKey: event.metaKey,
+ detail: event.detail,
+ type: event.type
+ };
+ var index = parseInt(tabElement.getAttribute("index"));
+ dotNetObject.invokeMethodAsync("ShowContextMenu", eventArgs, index);
+ });
+ });
+};
\ No newline at end of file
diff --git a/CS/blazor_multi_tab_ui/Components/Pages/Index.razor b/CS/blazor_multi_tab_ui/Components/Pages/Index.razor
new file mode 100644
index 0000000..d7d9326
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Components/Pages/Index.razor
@@ -0,0 +1,10 @@
+@page "/"
+@using blazor_multi_tab_ui.Components.MDI
+
+Dynamic Tabbed Interface
+
+
+
+@code {
+
+}
\ No newline at end of file
diff --git a/CS/DxBlazorApplication1/Components/Pages/Index.razor.css b/CS/blazor_multi_tab_ui/Components/Pages/Index.razor.css
similarity index 100%
rename from CS/DxBlazorApplication1/Components/Pages/Index.razor.css
rename to CS/blazor_multi_tab_ui/Components/Pages/Index.razor.css
diff --git a/CS/DxBlazorApplication1/Components/Routes.razor b/CS/blazor_multi_tab_ui/Components/Routes.razor
similarity index 100%
rename from CS/DxBlazorApplication1/Components/Routes.razor
rename to CS/blazor_multi_tab_ui/Components/Routes.razor
diff --git a/CS/DxBlazorApplication1/Components/_Imports.razor b/CS/blazor_multi_tab_ui/Components/_Imports.razor
similarity index 76%
rename from CS/DxBlazorApplication1/Components/_Imports.razor
rename to CS/blazor_multi_tab_ui/Components/_Imports.razor
index 799ecfb..8f8decc 100644
--- a/CS/DxBlazorApplication1/Components/_Imports.razor
+++ b/CS/blazor_multi_tab_ui/Components/_Imports.razor
@@ -6,8 +6,8 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using static Microsoft.AspNetCore.Components.Web.RenderMode
-@using DxBlazorApplication1
-@using DxBlazorApplication1.Components
-@using DxBlazorApplication1.Components.Layout
+@using blazor_multi_tab_ui
+@using blazor_multi_tab_ui.Components
+@using blazor_multi_tab_ui.Components.Layout
@using DevExpress.Blazor
\ No newline at end of file
diff --git a/CS/blazor_multi_tab_ui/Models/CustomerModel.cs b/CS/blazor_multi_tab_ui/Models/CustomerModel.cs
new file mode 100644
index 0000000..9414a74
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Models/CustomerModel.cs
@@ -0,0 +1,13 @@
+namespace blazor_multi_tab_ui.Models
+{
+ public class CustomerModel
+ {
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ public List? Orders { get; set; }
+ public int? TotalOrders
+ {
+ get { return Orders?.Count(); }
+ }
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Models/CustomersChartPoint.cs b/CS/blazor_multi_tab_ui/Models/CustomersChartPoint.cs
new file mode 100644
index 0000000..21027ff
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Models/CustomersChartPoint.cs
@@ -0,0 +1,9 @@
+namespace blazor_multi_tab_ui.Models
+{
+ public class CustomersChartPoint
+ {
+ public string? Name { get; set; }
+ public int Orders { get; set; }
+ public double Sales { get; set; }
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Models/MdiStateModel.cs b/CS/blazor_multi_tab_ui/Models/MdiStateModel.cs
new file mode 100644
index 0000000..66f0c8e
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Models/MdiStateModel.cs
@@ -0,0 +1,8 @@
+namespace blazor_multi_tab_ui.Models
+{
+ public class MdiStateModel
+ {
+ public List? Tabs { get; set; }
+ public int ActiveTabIndex { get; set; }
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Models/MdiTabModel.cs b/CS/blazor_multi_tab_ui/Models/MdiTabModel.cs
new file mode 100644
index 0000000..76ae06b
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Models/MdiTabModel.cs
@@ -0,0 +1,50 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace blazor_multi_tab_ui.Models
+{
+ public class MdiTabModel
+ {
+ public MdiTabModel() => Id = Guid.NewGuid().ToString();
+ public string Id { get; }
+ public int VisibleIndex { get; set; }
+ public bool Visible { get; set; }
+ public string? Text { get; set; }
+ public string? TabTypeName { get; set; }
+ public Dictionary? ParsedParameters { get; set; }
+ private Dictionary? _cachedParameters;
+
+ [JsonIgnore]
+ public Dictionary? Parameters
+ {
+ get
+ {
+ if (_cachedParameters == null && ParsedParameters != null)
+ {
+ _cachedParameters = ParsedParameters.ToDictionary(
+ p => p.Key,
+ p => (object)(p.Value.ValueKind switch
+ {
+ JsonValueKind.Number when p.Value.TryGetInt32(out var i) => i,
+ JsonValueKind.Number when p.Value.TryGetInt64(out var l) => l,
+ JsonValueKind.Number when p.Value.TryGetDouble(out var d) => d,
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.String => p.Value.GetString()!,
+ _ => p.Value.ToString()!
+ })
+ );
+ }
+ return _cachedParameters;
+ }
+ set
+ {
+ ParsedParameters = value?.ToDictionary(
+ p => p.Key,
+ p => JsonSerializer.SerializeToElement(p.Value)
+ );
+ _cachedParameters = value;
+ }
+ }
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Models/OrderModel.cs b/CS/blazor_multi_tab_ui/Models/OrderModel.cs
new file mode 100644
index 0000000..22fc1d3
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Models/OrderModel.cs
@@ -0,0 +1,10 @@
+namespace blazor_multi_tab_ui.Models
+{
+ public class OrderModel
+ {
+ public int Id { get; set; }
+ public string? Description { get; set; }
+ public DateTime CreatedDate { get; set; }
+ public double Cost { get; set; }
+ }
+}
diff --git a/CS/DxBlazorApplication1/NOTICE.txt b/CS/blazor_multi_tab_ui/NOTICE.txt
similarity index 100%
rename from CS/DxBlazorApplication1/NOTICE.txt
rename to CS/blazor_multi_tab_ui/NOTICE.txt
diff --git a/CS/DxBlazorApplication1/Program.cs b/CS/blazor_multi_tab_ui/Program.cs
similarity index 56%
rename from CS/DxBlazorApplication1/Program.cs
rename to CS/blazor_multi_tab_ui/Program.cs
index 8e9defa..b9bb907 100644
--- a/CS/DxBlazorApplication1/Program.cs
+++ b/CS/blazor_multi_tab_ui/Program.cs
@@ -1,34 +1,33 @@
-using DxBlazorApplication1.Services;
-using DxBlazorApplication1.Components;
-using DxBlazorApplication1.Components.MDI;
+using blazor_multi_tab_ui.Components;
+using blazor_multi_tab_ui.Services;
var builder = WebApplication.CreateBuilder(args);
-// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
-builder.Services.AddDevExpressBlazor(options => {
- options.BootstrapVersion = DevExpress.Blazor.BootstrapVersion.v5;
+builder.Services.AddDevExpressBlazor(options =>
+{
options.SizeMode = DevExpress.Blazor.SizeMode.Medium;
});
-builder.Services.AddSingleton();
-builder.Services.AddScoped();
builder.Services.AddMvc();
-var app = builder.Build();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
-// Configure the HTTP request pipeline.
-if (!app.Environment.IsDevelopment()) {
+var app = builder.Build();
+if (!app.Environment.IsDevelopment())
+{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
+
app.UseHttpsRedirection();
-app.UseStaticFiles();
app.UseAntiforgery();
+app.MapStaticAssets();
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AllowAnonymous();
diff --git a/CS/blazor_multi_tab_ui/Services/DataService.cs b/CS/blazor_multi_tab_ui/Services/DataService.cs
new file mode 100644
index 0000000..38863e5
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Services/DataService.cs
@@ -0,0 +1,56 @@
+using blazor_multi_tab_ui.Models;
+
+namespace blazor_multi_tab_ui.Services
+{
+ public class DataService
+ {
+ private List customers;
+ public DataService() { customers = GenerateTestData(); }
+
+ public List GetCustomers() { return customers; }
+ public List? GetOrdersByCustomerId(int customerId)
+ {
+ var customer = customers.FirstOrDefault(c => c.Id == customerId);
+ if (customer == null) return null;
+ return customer.Orders;
+ }
+ public List GetCustomersChartData()
+ {
+ return customers.Select(customer => new CustomersChartPoint
+ {
+ Name = customer.Name,
+ Orders = customer?.Orders?.Count ?? 0,
+ Sales = customer?.Orders?.Sum(order => order.Cost) ?? 0
+ }).ToList();
+ }
+
+
+ private List GenerateTestData()
+ {
+ var now = DateTime.Now;
+ return new List{
+ new CustomerModel()
+ {
+ Id = 1,
+ Name = "John Doe",
+ Orders = new List
+ {
+ new OrderModel{ Id = 1, Description = $"Apples", CreatedDate = new DateTime(now.Year, now.Month, now.Day ), Cost = 100 },
+ new OrderModel{ Id = 2, Description = $"Bananas", CreatedDate = new DateTime(now.Year, now.Month, now.Day - 1 ), Cost = 200 },
+ }
+ },
+ new CustomerModel()
+ {
+ Id = 2,
+ Name = "Jane Smith",
+ Orders = new List
+ {
+ new OrderModel{ Id = 3, Description = $"Kiwi", CreatedDate = new DateTime(now.Year, now.Month, now.Day ), Cost = 150 },
+ new OrderModel{ Id = 4, Description = $"Durian", CreatedDate = new DateTime(now.Year, now.Month, now.Day - 1 ), Cost = 300 },
+ new OrderModel{ Id = 5, Description = $"Potatoes", CreatedDate = new DateTime(now.Year, now.Month, now.Day - 2 ), Cost = 42 },
+ }
+ },
+ };
+ }
+ }
+}
diff --git a/CS/blazor_multi_tab_ui/Services/MdiStateService.cs b/CS/blazor_multi_tab_ui/Services/MdiStateService.cs
new file mode 100644
index 0000000..01f5b4e
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/Services/MdiStateService.cs
@@ -0,0 +1,137 @@
+using blazor_multi_tab_ui.Components.MDI.Tabs;
+using blazor_multi_tab_ui.Models;
+using Microsoft.JSInterop;
+using System.Runtime.Intrinsics.Arm;
+using System.Text.Json;
+
+namespace blazor_multi_tab_ui.Services
+{
+ public class MdiStateService(IJSRuntime _js)
+ {
+ private MdiStateModel state = new();
+ //private List tabs;
+ private const string LOCAL_STORAGE_KEY = "MDI-Layout";
+ private readonly Dictionary _stringToTypeMap = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["customers"] = typeof(Customers),
+ ["orders"] = typeof(Orders),
+ ["chart"] = typeof(Chart),
+ };
+
+ public bool TryGetType(string? key, out Type? type) => _stringToTypeMap.TryGetValue(key ?? "unknown", out type);
+ public event Action? OnTabsChanged;
+
+ public async Task LoadState()
+ {
+ var cachedState = await LoadStateFromLocalStorageAsync();
+ if (cachedState != null) state = cachedState;
+ else state = new MdiStateModel { Tabs = GetDefaultTabs(), ActiveTabIndex = 0 };
+ return state;
+ }
+ public async Task SaveState(bool InvokeChanged = false)
+ {
+ var orderedTabs = state?.Tabs?.OrderBy(i => i.VisibleIndex).ToList();
+ if (orderedTabs is null) return;
+ for (int i = 0; i < orderedTabs.Count; i++)
+ {
+ orderedTabs[i].VisibleIndex = i;
+ }
+ if (state is null) return;
+ await SaveStateToLocalStorageAsync(state);
+ if (InvokeChanged) OnTabsChanged?.Invoke();
+ }
+ public async Task SetActiveTabIndex(int newIndex)
+ {
+ state.ActiveTabIndex = newIndex;
+ await SaveState();
+ }
+
+ public async Task AddTab(string Text, string TypeName, Dictionary? Parameters = null)
+ {
+ state?.Tabs?.Add(new MdiTabModel { Text = Text, TabTypeName = TypeName, Parameters = Parameters, Visible = true, VisibleIndex = state.Tabs.Count });
+ await SaveState(true);
+ }
+ public async Task SetAllTabsVisible(bool Visible)
+ {
+ state?.Tabs?.ForEach(t => t.Visible = Visible);
+ await SaveState(true);
+ }
+ public void SetTabVisible(int Index, bool Visible)
+ {
+ var tab = state?.Tabs?.FirstOrDefault(tab => tab.VisibleIndex == Index);
+ if (tab == null) { return; }
+ tab.Visible = Visible;
+ }
+
+ public async Task RemoveTab(int Index)
+ {
+ var tab = state?.Tabs?.FirstOrDefault(tab => tab.VisibleIndex == Index);
+ if (tab == null) { return; }
+ state?.Tabs?.Remove(tab);
+ await SaveState(true);
+ }
+ public async Task RemoveAllTabs()
+ {
+ state?.Tabs?.Clear();
+ await SaveState(true);
+ }
+ public async Task RemoveAllButTab(int Index)
+ {
+ var tab = state?.Tabs?.FirstOrDefault(tab => tab.VisibleIndex == Index);
+ if (tab == null) { return; }
+ state?.Tabs?.Clear();
+ state?.Tabs?.Add(tab);
+ await SaveState(true);
+ }
+ public void ReorderTabs(int fromIndex, int toIndex)
+ {
+ if (fromIndex == toIndex || fromIndex < 0 || toIndex < 0 ||
+ fromIndex >= state?.Tabs?.Count || toIndex >= state?.Tabs?.Count)
+ return;
+
+ var orderedTabs = state?.Tabs?.OrderBy(i => i.VisibleIndex).ToList();
+ if (orderedTabs is null) return;
+
+ var tabToMove = orderedTabs[fromIndex];
+ orderedTabs.RemoveAt(fromIndex);
+ orderedTabs.Insert(toIndex, tabToMove);
+ for (int i = 0; i < orderedTabs.Count; i++)
+ {
+ orderedTabs[i].VisibleIndex = i;
+ }
+ for (int i = 0; i < orderedTabs.Count; i++)
+ {
+ var originalTab = state?.Tabs?.First(x => x == orderedTabs[i]);
+ if (originalTab != null) originalTab.VisibleIndex = orderedTabs[i].VisibleIndex;
+ }
+ }
+
+
+
+ private List GetDefaultTabs()
+ {
+ List result = new();
+ result.Add(new MdiTabModel { Text = "Customers", Visible = true, VisibleIndex = 0, TabTypeName = "Customers" });
+ return result;
+ }
+ private async Task SaveStateToLocalStorageAsync(MdiStateModel state)
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(state);
+ await _js.InvokeVoidAsync("localStorage.setItem", LOCAL_STORAGE_KEY, json);
+ }
+ catch { return; }
+ }
+
+ private async Task LoadStateFromLocalStorageAsync()
+ {
+ try
+ {
+ var json = await _js.InvokeAsync("localStorage.getItem", LOCAL_STORAGE_KEY);
+ return JsonSerializer.Deserialize(json);
+ }
+ catch { return null; }
+ }
+ }
+}
diff --git a/CS/DxBlazorApplication1/Services/UrlGenerator.cs b/CS/blazor_multi_tab_ui/Services/UrlGenerator.cs
similarity index 84%
rename from CS/DxBlazorApplication1/Services/UrlGenerator.cs
rename to CS/blazor_multi_tab_ui/Services/UrlGenerator.cs
index c995faa..e78d361 100644
--- a/CS/DxBlazorApplication1/Services/UrlGenerator.cs
+++ b/CS/blazor_multi_tab_ui/Services/UrlGenerator.cs
@@ -1,12 +1,17 @@
using System.Web;
-namespace DxBlazorApplication1 {
- public static class UrlGenerator {
+namespace blazor_multi_tab_ui
+{
+ public static class UrlGenerator
+ {
public const string ToggleSidebarName = "toggledSidebar";
- public static string GetUrl(string baseUrl, bool toggledSidebar) {
+
+ public static string GetUrl(string baseUrl, bool toggledSidebar)
+ {
return $"{baseUrl}?{ToggleSidebarName}={toggledSidebar}";
}
- public static string GetUrl(bool toggledSidebar, string returnUrl) {
+ public static string GetUrl(bool toggledSidebar, string returnUrl)
+ {
var baseUriBuilder = new UriBuilder(returnUrl);
var query = HttpUtility.ParseQueryString(baseUriBuilder.Query);
var baseUrl = baseUriBuilder.Fragment + baseUriBuilder.Host + baseUriBuilder.Path;
diff --git a/CS/DxBlazorApplication1/appsettings.Development.json b/CS/blazor_multi_tab_ui/appsettings.Development.json
similarity index 100%
rename from CS/DxBlazorApplication1/appsettings.Development.json
rename to CS/blazor_multi_tab_ui/appsettings.Development.json
diff --git a/CS/DxBlazorApplication1/appsettings.json b/CS/blazor_multi_tab_ui/appsettings.json
similarity index 100%
rename from CS/DxBlazorApplication1/appsettings.json
rename to CS/blazor_multi_tab_ui/appsettings.json
diff --git a/CS/blazor_multi_tab_ui/blazor_multi_tab_ui.csproj b/CS/blazor_multi_tab_ui/blazor_multi_tab_ui.csproj
new file mode 100644
index 0000000..a7e40ee
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/blazor_multi_tab_ui.csproj
@@ -0,0 +1,12 @@
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/css/bootstrap/bootstrap.css b/CS/blazor_multi_tab_ui/wwwroot/css/bootstrap/bootstrap.css
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/bootstrap/bootstrap.css
rename to CS/blazor_multi_tab_ui/wwwroot/css/bootstrap/bootstrap.css
diff --git a/CS/DxBlazorApplication1/wwwroot/css/bootstrap/bootstrap.min.css b/CS/blazor_multi_tab_ui/wwwroot/css/bootstrap/bootstrap.min.css
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/bootstrap/bootstrap.min.css
rename to CS/blazor_multi_tab_ui/wwwroot/css/bootstrap/bootstrap.min.css
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/FONT-LICENSE.txt b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/FONT-LICENSE.txt
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/FONT-LICENSE.txt
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/FONT-LICENSE.txt
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/ICON-LICENSE.txt b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/ICON-LICENSE.txt
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/ICON-LICENSE.txt
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/ICON-LICENSE.txt
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/README.md b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/README.md
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/README.md
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/README.md
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/css/open-iconic-bootstrap.min.css b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/css/open-iconic-bootstrap.min.css
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.eot b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.eot
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.otf b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.otf
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.svg b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.svg
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.ttf b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.ttf
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf
diff --git a/CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.woff b/CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/css/open.iconic/font/fonts/open-iconic.woff
rename to CS/blazor_multi_tab_ui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff
diff --git a/CS/DxBlazorApplication1/wwwroot/css/site.css b/CS/blazor_multi_tab_ui/wwwroot/css/site.css
similarity index 66%
rename from CS/DxBlazorApplication1/wwwroot/css/site.css
rename to CS/blazor_multi_tab_ui/wwwroot/css/site.css
index 42faecc..c3fbbbf 100644
--- a/CS/DxBlazorApplication1/wwwroot/css/site.css
+++ b/CS/blazor_multi_tab_ui/wwwroot/css/site.css
@@ -1,4 +1,4 @@
-@import url('open.iconic/font/css/open-iconic-bootstrap.min.css');
+@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
@@ -39,12 +39,12 @@ html, body {
z-index: 1000;
}
- #blazor-error-ui .dismiss {
- cursor: pointer;
- position: absolute;
- right: 0.75rem;
- top: 0.5rem;
- }
+#blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+}
.title {
display: flex;
@@ -53,11 +53,11 @@ html, body {
padding-bottom: 0.625rem;
}
- .title.title-secondary {
- padding-top: 0.313rem;
- padding-bottom: 0;
- color: var(--bs-secondary-color);
- }
+.title.title-secondary {
+ padding-top: 0.313rem;
+ padding-bottom: 0;
+ color: var(--bs-secondary-color, var(--DS-color-content-neutral-default-rest));
+}
.title-header-text {
font-size: 2.5rem;
@@ -89,4 +89,16 @@ html, body {
flex-direction: column;
gap: 0.625rem;
max-width: 100%;
-}
\ No newline at end of file
+}
+
+.icon {
+ width: var(--icon-width);
+ height: var(--icon-height);
+ background-color: currentcolor;
+ mask-position: center center;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+ -webkit-mask-image: var(--icon-mask-image);
+ mask-image: var(--icon-mask-image);
+}
+
diff --git a/CS/blazor_multi_tab_ui/wwwroot/css/theme-bs.css b/CS/blazor_multi_tab_ui/wwwroot/css/theme-bs.css
new file mode 100644
index 0000000..ef1b457
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/css/theme-bs.css
@@ -0,0 +1,18 @@
+.nav-buttons-container .dxbl-btn.dxbl-btn-icon-only {
+ --dxbl-btn-padding-x: 0.75rem;
+ --dxbl-btn-padding-y: 0.25rem;
+}
+
+.icon {
+ --icon-width: 1rem;
+ --icon-height: 1rem;
+ --icon-back-mask-image: url("/images/back.svg");
+ --icon-close-mask-image: url("/images/close.svg");
+ --icon-menu-mask-image: url("/images/menu.svg");
+ --icon-docs-mask-image: url("/images/doc.svg");
+ --icon-demos-mask-image: url("/images/demos.svg");
+ --icon-home-mask-image: url("/images/home.svg");
+ --icon-weather-mask-image: url("/images/weather.svg");
+ --icon-counter-mask-image: url("/images/counter.svg");
+}
+
diff --git a/CS/blazor_multi_tab_ui/wwwroot/css/theme-fluent.css b/CS/blazor_multi_tab_ui/wwwroot/css/theme-fluent.css
new file mode 100644
index 0000000..06d6ee0
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/css/theme-fluent.css
@@ -0,0 +1,43 @@
+body {
+ margin: 0;
+}
+
+
+.w-100 {
+ width: 100%;
+}
+
+.text-danger {
+ color: var(--DS-color-surface-danger-default-rest);
+}
+
+.p-4 {
+ padding: 1.5rem;
+}
+
+.navigation-drawer {
+ --dxbl-drawer-separator-border-width: 0;
+}
+
+.menu-item {
+ --dxbl-menu-item-color: #fff;
+ --dxbl-menu-item-image-color: #fff;
+}
+
+[data-fluent-darkmode] .dxbl-theme-fluent .welcome-card {
+ color: var(--DS-primary-70);
+}
+
+.icon {
+ --icon-width: 1.25rem;
+ --icon-height: 1.25rem;
+ --icon-back-mask-image: url("/images/back-fluent.svg");
+ --icon-close-mask-image: url("/images/close-fluent.svg");
+ --icon-menu-mask-image: url("/images/menu-fluent.svg");
+ --icon-docs-mask-image: url("/images/doc-fluent.svg");
+ --icon-demos-mask-image: url("/images/demos-fluent.svg");
+ --icon-home-mask-image: url("/images/home-fluent.svg");
+ --icon-weather-mask-image: url("/images/weather-fluent.svg");
+ --icon-counter-mask-image: url("/images/counter-fluent.svg");
+}
+
diff --git a/CS/DxBlazorApplication1/wwwroot/favicon.ico b/CS/blazor_multi_tab_ui/wwwroot/favicon.ico
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/favicon.ico
rename to CS/blazor_multi_tab_ui/wwwroot/favicon.ico
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/log-in-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-in-fluent.svg
new file mode 100644
index 0000000..bc795c7
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-in-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/log-in.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-in.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/log-in.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/log-in.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/log-out-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-out-fluent.svg
new file mode 100644
index 0000000..528d338
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-out-fluent.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/log-out.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/log-out.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/log-out.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/log-out.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email-fluent.svg
new file mode 100644
index 0000000..8e25a2c
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/manage-email.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/manage-email.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/manage-email.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password-fluent.svg
new file mode 100644
index 0000000..57cce63
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/manage-password.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/manage-password.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/manage-password.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal-fluent.svg
new file mode 100644
index 0000000..a8aaddc
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/manage-personal.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/manage-personal.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/manage-personal.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile-fluent.svg
new file mode 100644
index 0000000..3da67b0
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/manage-profile.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/manage-profile.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/manage-profile.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor-fluent.svg
new file mode 100644
index 0000000..574f211
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/manage-two-factor.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/manage-two-factor.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/manage-two-factor.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo-fluent.svg
new file mode 100644
index 0000000..a7dac6d
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo-fluent.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/providers/facebook-logo.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/providers/facebook-logo.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/providers/facebook-logo.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo-fluent.svg
new file mode 100644
index 0000000..bea8c68
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo-fluent.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/providers/google-logo.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/providers/google-logo.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/providers/google-logo.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo-fluent.svg
new file mode 100644
index 0000000..6dde382
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo-fluent.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/providers/microsoft-logo.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/providers/microsoft-logo.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/providers/microsoft-logo.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo-fluent.svg
new file mode 100644
index 0000000..6d25991
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/providers/x-logo.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/providers/x-logo.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/providers/x-logo.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/settings-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/settings-fluent.svg
new file mode 100644
index 0000000..8456fd6
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/settings-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/settings.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/settings.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/settings.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/settings.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/account/user-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/user-fluent.svg
new file mode 100644
index 0000000..0887fad
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/account/user-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/account/user.svg b/CS/blazor_multi_tab_ui/wwwroot/images/account/user.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/account/user.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/account/user.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/back-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/back-fluent.svg
new file mode 100644
index 0000000..9b596d0
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/back-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/back.svg b/CS/blazor_multi_tab_ui/wwwroot/images/back.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/back.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/back.svg
diff --git a/CS/DxBlazorApplication1/wwwroot/images/cards.svg b/CS/blazor_multi_tab_ui/wwwroot/images/cards.svg
similarity index 98%
rename from CS/DxBlazorApplication1/wwwroot/images/cards.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/cards.svg
index 5ca6db7..cb273c8 100644
--- a/CS/DxBlazorApplication1/wwwroot/images/cards.svg
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/cards.svg
@@ -7,10 +7,10 @@
-
+
-
+
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/close-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/close-fluent.svg
new file mode 100644
index 0000000..38064af
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/close-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/close.svg b/CS/blazor_multi_tab_ui/wwwroot/images/close.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/close.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/close.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/counter-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/counter-fluent.svg
new file mode 100644
index 0000000..8ed0cae
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/counter-fluent.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/counter.svg b/CS/blazor_multi_tab_ui/wwwroot/images/counter.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/counter.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/counter.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/demos-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/demos-fluent.svg
new file mode 100644
index 0000000..f9f4953
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/demos-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/demos.svg b/CS/blazor_multi_tab_ui/wwwroot/images/demos.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/demos.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/demos.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/doc-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/doc-fluent.svg
new file mode 100644
index 0000000..e746057
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/doc-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/doc.svg b/CS/blazor_multi_tab_ui/wwwroot/images/doc.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/doc.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/doc.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/home-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/home-fluent.svg
new file mode 100644
index 0000000..0919bd5
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/home-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/home.svg b/CS/blazor_multi_tab_ui/wwwroot/images/home.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/home.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/home.svg
diff --git a/CS/DxBlazorApplication1/wwwroot/images/logo.svg b/CS/blazor_multi_tab_ui/wwwroot/images/logo.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/logo.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/logo.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/menu-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/menu-fluent.svg
new file mode 100644
index 0000000..d9cd93e
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/menu-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/menu.svg b/CS/blazor_multi_tab_ui/wwwroot/images/menu.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/menu.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/menu.svg
diff --git a/CS/blazor_multi_tab_ui/wwwroot/images/weather-fluent.svg b/CS/blazor_multi_tab_ui/wwwroot/images/weather-fluent.svg
new file mode 100644
index 0000000..f24c5c3
--- /dev/null
+++ b/CS/blazor_multi_tab_ui/wwwroot/images/weather-fluent.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CS/DxBlazorApplication1/wwwroot/images/weather.svg b/CS/blazor_multi_tab_ui/wwwroot/images/weather.svg
similarity index 100%
rename from CS/DxBlazorApplication1/wwwroot/images/weather.svg
rename to CS/blazor_multi_tab_ui/wwwroot/images/weather.svg
From 3cf85407f20445ddca08fb1e8058a13ac2f7ca39 Mon Sep 17 00:00:00 2001
From: Alexander Gusev
<95075261+GusevAlexander-DevExpress@users.noreply.github.com>
Date: Thu, 17 Jul 2025 16:27:38 +0400
Subject: [PATCH 02/13] minor changes and updated readme and
Cosmetic changes in main files and updated documentation
---
.../Components/MDI/MdiTabs.razor | 4 +-
CS/blazor_multi_tab_ui/Models/MdiTabModel.cs | 8 +-
.../Services/MdiStateService.cs | 1 -
README.md | 116 ++++++++++++------
images/blazor-tabbed-ui.png | Bin 26755 -> 37694 bytes
5 files changed, 82 insertions(+), 47 deletions(-)
diff --git a/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor b/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
index 26586c1..29bec3b 100644
--- a/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
+++ b/CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor
@@ -10,8 +10,7 @@
var tabsCollection = tabsState.Tabs;
var activeTabIndex = tabsState.ActiveTabIndex;
- ? ParsedParameters { get; set; }
+ public Dictionary? UntypedParameters { get; set; }
private Dictionary? _cachedParameters;
[JsonIgnore]
@@ -19,9 +19,9 @@ public Dictionary? Parameters
{
get
{
- if (_cachedParameters == null && ParsedParameters != null)
+ if (_cachedParameters == null && UntypedParameters != null)
{
- _cachedParameters = ParsedParameters.ToDictionary(
+ _cachedParameters = UntypedParameters.ToDictionary(
p => p.Key,
p => (object)(p.Value.ValueKind switch
{
@@ -39,7 +39,7 @@ JsonValueKind.Number when p.Value.TryGetDouble(out var d) => d,
}
set
{
- ParsedParameters = value?.ToDictionary(
+ UntypedParameters = value?.ToDictionary(
p => p.Key,
p => JsonSerializer.SerializeToElement(p.Value)
);
diff --git a/CS/blazor_multi_tab_ui/Services/MdiStateService.cs b/CS/blazor_multi_tab_ui/Services/MdiStateService.cs
index 01f5b4e..a9032f0 100644
--- a/CS/blazor_multi_tab_ui/Services/MdiStateService.cs
+++ b/CS/blazor_multi_tab_ui/Services/MdiStateService.cs
@@ -9,7 +9,6 @@ namespace blazor_multi_tab_ui.Services
public class MdiStateService(IJSRuntime _js)
{
private MdiStateModel state = new();
- //private List tabs;
private const string LOCAL_STORAGE_KEY = "MDI-Layout";
private readonly Dictionary _stringToTypeMap = new(StringComparer.OrdinalIgnoreCase)
{
diff --git a/README.md b/README.md
index b62c3f0..f9a7287 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,8 @@
# Blazor Tabs - Create a Dynamic Tabbed Interface
-The example creates an interactive, multi-tab web interface using DevExpress Blazor [Tabs](https://docs.devexpress.com/Blazor/405074/components/layout/tabs) and [Context Menu](https://docs.devexpress.com/Blazor/405060/components/navigation-controls/context-menu) components.
+The example creates an interactive, multi-tab web interface using DevExpress Blazor [Tabs](https://docs.devexpress.com/Blazor/405074/components/layout/tabs), [Context Menu](https://docs.devexpress.com/Blazor/405060/components/navigation-controls/context-menu) components.
+

@@ -16,71 +17,108 @@ It illustrates how end users can create personalized workspaces and multitask ef
### Organize Content Into Tabs
-Place [DxTabs](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxTabs) container on the page ([Index.razor](CS/DxBlazorApplication1/Components/Pages/Index.razor)) and add a [DxTabPage](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxTabPage) for each tab.
+The [MdiTabs](CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor) component is based on [DxTabs](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxTabs) control.
+Obtain the tabs state and render tabs in a loop. Use [DxTabPage](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxTabPage) for each tab. Insert your custom Blazor components or content directly into each `DxTabPage`.
-Insert your custom Blazor components or content directly into each `DxTabPage`.
+In this example the tab content is rendered dynamically: [DynamicComponent](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.dynamiccomponent?view=aspnetcore-9.0)
```razor
-
-
-
-
-
-
+ RenderMode="TabsRenderMode.Default">
+ @for (int i = 0; i < tabsCollection.Count; i++)
+ {
+ var tabModel = tabsCollection[i];
+
+
+ @if (stateService.TryGetType(tabModel.TabTypeName, out var type))
+ {
+
+ }
+ else
+ {
+
+ }
+
+
+
+ }
+
```
-The `CssClass` property of a tab page serves as a unique identifier, allowing client-side scripts to interact with specific tabs.
-
### Persist Tab State
-Implement a custom `MDITab` class ([MDITab.cs](CS/DxBlazorApplication1/Components/MDI/MDITab.cs)) to encapsulate properties associated with each individual tab. `MDITabCollection` class ([MDITabCollection.cs](CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs)) will control visibility, display order, and titles used for all tabs. The title links an underlying object to the visual tab representation in the UI.
+Implement a custom `MdiTabModel` class ([MdiTabModel.cs](CS/blazor_multi_tab_ui/Models/MdiTabModel.cs)) to encapsulate properties associated with each individual tab. The `MdiStateModel` class ([MdiStateModel.cs](CS/blazor_multi_tab_ui/Models/MdiStateModel.cs)) represents the overall state of the [MdiTabs](CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor) component — including visibility, display order, active tab index, and other properties of all tabs. The `VisibleIndex` property links an underlying object to its visual tab representation in the UI.
-Bind these properties to the visual tab elements in the UI. To ensure the `MDITabCollection` accurately reflects the live interface, implement event handlers for `TabReorder` and `TabClosing`. These handlers will listen for user actions and dynamically update the collection to match current tab state.
+Bind these properties to the visual tab elements in the UI. To ensure that the state model accurately reflects the live interface, implement event handlers for `TabReorder`, `TabClosing`, and `ActiveTabIndexChanged`. These handlers listen for user actions and dynamically update the state to match the current tab layout.
-To maintain tab layout across sessions, serialize the collection to JSON and save it to the browser's local storage with `MDIStateHelper` class ([MDIStateHelper.cs](CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs)) every time the UI layout changes. This will maintain tab visibility and order even after the user closes and reopens the browser. Tab state is restored in the `OnAfterRenderAsync` event handler.
+The [MdiStateService](CS/blazor_multi_tab_ui/Services/MdiStateService.cs) service contains all methods required for managing state — including loading, saving, and updating properties. To maintain the tab layout across sessions, the state can be stored in local storage, session storage, cookies, or a database. In this example, `MdiStateService` stores the state in local storage. The service serializes the entire `MdiStateModel` to JSON and saves it to the browser's local storage whenever the UI layout changes. This preserves tab visibility, order, and the active tab even after the browser is closed and reopened. Tab state is restored in the `OnInitializedAsync` method of the `MdiTabs` component.
### Add Context Menu to Tabs
-Create a context menu that allows users to manage tabs as needed:
+Add a context menu that lets users manage tabs more flexibly. The available operations are:
-- Close the current tab.
-- Close all tabs.
-- Close all tabs except for the current tab.
-- Restore closed tabs.
+- **Close** the current tab.
+- **Close** all tabs.
+- **Close** all tabs except the current one.
+- **Hide** the current tab.
+- **Hide** all tabs.
+- **Hide** all tabs except the current one.
+- **Restore** previously hidden tabs.
-Place [DxContextMenu](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenu) on the page ([Index.razor](CS/DxBlazorApplication1/Components/Pages/Index.razor)) and add a [DxContextMenuItem](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenuItem) for each menu action.
+Embed the [`TabsContextMenu`](CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor) component inside [`MdiTabs`](CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor).
+`TabsContextMenu` renders a [DxContextMenu](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenu) with one [DxContextMenuItem](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenuItem) per action:
```razor
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
```
-Implement a client-side script ([mdi.js](CS/DxBlazorApplication1/wwwroot/js/mdi.js)) to handle right-clicks on specific tabs (identified by their `CssClass` property). This script should prevent the default browser context menu. Capture the mouse position, and invoke a .NET `[JSInvokable]` method.
+Use the `TabSelector` parameter to find the `MdiTabs` elemnt and its tab-header elements to target.
+At runtime, the companion script [`TabsContextMenu.razor.js`](CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js) performs the following:
+
+- Finds the matching elements via the selector.
+- Suppresses the browser’s default context menu.
+- Captures the mouse position and invokes a `[JSInvokable]` .NET method that opens the DevExpress menu at the pointer location.
+
+When a menu item is clicked, the handler calls the corresponding method of [`MdiStateService`](CS/blazor_multi_tab_ui/Services/MdiStateService.cs) to update the tab state (close, hide, or restore) and persist the change if required.
+
## Files to Review
-- [Index.razor](CS/DxBlazorApplication1/Components/Pages/Index.razor)
-- [NavMenu.razor](CS/DxBlazorApplication1/Components/Layout/NavMenu.razor)
-- [MainLayout.razor](CS/DxBlazorApplication1/Components/Layout/MainLayout.razor.css)
-- [MDITab.cs](CS/DxBlazorApplication1/Components/MDI/MDITab.cs)
-- [MDITabCollection.cs](CS/DxBlazorApplication1/Components/MDI/MDITabCollection.cs)
-- [MDIStateHelper.cs](CS/DxBlazorApplication1/Components/MDI/MDIStateHelper.cs)
-- [mdi.js](CS/DxBlazorApplication1/wwwroot/js/mdi.js)
+- [`MdiTabs.razor`](CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor) – main tabbed UI component
+- [`MdiTabModel.cs`](CS/blazor_multi_tab_ui/Models/MdiTabModel.cs) – defines individual tab structure
+- [`MdiStateModel.cs`](CS/blazor_multi_tab_ui/Models/MdiStateModel.cs) – represents the overall state of all tabs
+- [`MdiStateService.cs`](CS/blazor_multi_tab_ui/Services/MdiStateService.cs) – manages tab state, persistence, and updates
+- [`TabsContextMenu.razor`](CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor) – context menu UI and logic
+- [`TabsContextMenu.razor.js`](CS/blazor_multi_tab_ui/Components/MDI/TabsContextMenu.razor.js) – JS interop for right-click and menu activation
+- [`Tabs`](CS/blazor_multi_tab_ui/Components/MDI/MdiTabs.razor/Tabs) – folder with tab content components (one per tab type)
## Documentation
diff --git a/images/blazor-tabbed-ui.png b/images/blazor-tabbed-ui.png
index 3e1a23f684d4d2a36636245c56a6d181e5c2c256..6cc05fee29fafe4547bb20914fa3a677b573afd8 100644
GIT binary patch
literal 37694
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 jDI6Ah1DWmVYDI+J!&
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 V%gQ3`~Q?@f?K5<5Zc+Z%#;%VTGm&&RubRQTHV
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
literal 26755
zcmaHT2{@Gf_qS4MvlK#Nv{@@V*~S(k`%ad!%a-g6N+Ro&A-f^_o_(ieW*BSPVk|Kj
zJ7XOT-h1>s&;R%Q-v9e{U2@Gm_kEx9-Ol-(&pAh!rn(a4#p@Tz$jB&_A3xG2BO?cs
zk)0Age-6094igptew}jDR+1wt>AA50d^lq(t0qfERtCL9e0~=AeBtF|BR4WKs_&$q
zQz(}_D>5=WM&(Dcy543hO;I&03aM*N#~*#>Y^37--rkbss}rDQEJ^qw^Pch1*)W0E
zU)w%@_tto1cIM7)?sF8YGyLrz$KJJevDM1Ge*T%!k|y%P&EWTqvoXPK!SQTs&vYx(
z_6MKrmKGG67xsdiGyjK_^^^6S6f_~onReD(Jo
z3G|>O!l}lWa)TD=G1kbrMB(Cz^2Cg0abFCFxPr3>A&xLdFv`sbq!icsqH;1A6
zgoal1MxST$XNW-Oa|r1*WJ%BUA$ArD`Arp(BqjN&vE-!7eUYce70gt*(r_i&o^c{;XnUlpm3MG4tnjZ#|^2igcC7BmNH%+2eZ&)T8gYb
zGY!cuodn0aX=8jM4=cY|=WUDG6bA(QKyB#D+QB5{K-9bFZBy4_Gg#_rGyiS9vRAVz
zar=GM#{}r{`A=&1{Sa-f@`y*R2DltEIr;YEr|T-
zoDtRgOB}{>N^#Qz*tvWxO!v|{7k(D8bt9Q6MF3+hXQ%DYbtQoHH`CzpA8OSz7+
z8V_K20c?eAq!{$ueflLe^*Q?y}eDYb96xbPp9QEK*0#U;cte1DsPdsl?85W8HhzVlktQFn->Cu#s!q{^oQz`*vlEq<
z<~IfsT;**R%Xch)!!}kmkP(qk5uVXnyB*bg0_Q^e4sv{28E-T=)dYoiEj4U-0;ySw*zDeT*ja
zK4H}d(F$rPr+qa#q!#I;tZ$iMZ&WiNC>(6`L(4pXMnJ7GrTsdKbsADM!Ng5*>F$Yz&~s-fr7>tx;!2+HRTI0$a8JN)bV(?|i-~*3!Uri2toTqt1rE
z*C2vl34C->jhP3m?yoL^M8P95PUC5@IA-{IIQ3v@#dLofes2?gT(pJeDSiLcrAZI6
z;$TiYd9+glbK0sISM+%b3X(fb_FCjM{!RK7Z48pbC>zhpqZ2ZJ`M@qwc5wC_7eZ9T
z7s1|2$lH)~>yO4~OhTt}P_5n6OqtGF&mdhm4>TAn$b{q&(yFS*rTN|Hw?qJ6-ZncY
zt9OQs%;O^x=P<3ac94{!+o6l@IN&0kX0H1#B8iX~c*4nyi$eSatX>%EIo3^M?zEi#fmcc05fc!}?aEyV)4B
z=&wyW{}2^&X)>Kta6{vMd9O+Hz(WsVCjW)(_yG|zcG^GBg-bP@KC?+qmKPK-5XG*?
zu5e>1|rR$u18dwjEv?q5BTxNXw8dQ@|+;aez*1C
zvL3k9>Qj8GoO7Hzoadegkdf*AKvD#yf24_|PPhmGj6?V2@zL(ISIYs5#Z^@%IzPI%
zbP;qg!2Z4#I0yHgG(aq9d;GM