feat(DockView): add OnlyWhenVisible render mode#974
Conversation
Reviewer's GuideImplements an "OnlyWhenVisible" render mode for DockViewV2 by tracking per-component state on the server and wiring it to updated JavaScript lifecycle events so that Blazor components are only rendered when their panels are visible, while also refining DockView configuration, callbacks, and related UI behavior and docs. Sequence diagram for OnlyWhenVisible DockView rendering and visibility updatessequenceDiagram
actor User
participant Browser
participant DockviewJS
participant DotNetRuntime
participant DockViewV2
participant DockViewComponent
participant DockViewComponentState
User->>Browser: Open page with DockViewV2
Browser->>DockviewJS: init(options)
DockviewJS->>DockviewJS: load layout from JSON / storage
Note over DockviewJS: After layout applied
DockviewJS->>DockviewJS: compute visiblePanels
DockviewJS->>DockviewJS: _loadTabs.fire(visiblePanelKeys)
DockviewJS->>DotNetRuntime: invokeMethodAsync(loadTabs, tabs)
DotNetRuntime->>DockViewV2: LoadTabs(List~string~ tabs)
DockViewV2->>DockViewV2: _loadTabs = tabs.ToHashSet()
DockViewV2->>DockViewV2: for each componentState
DockViewV2->>DockViewComponentState: set Render based on tabs.Contains(key)
DockViewV2->>DockViewV2: StateHasChanged()
Note over DockViewComponent: during render
DockViewComponent->>DockViewV2: IsRender(Key)
DockViewV2->>DockViewV2: Renderer switch
DockViewV2-->>DockViewComponent: OnlyWhenVisible => state.IsRender()
DockViewComponent->>DockViewComponent: if IsRender() true render ChildContent
Note over User,Browser: User changes tab visibility
User->>Browser: Hide/show or close a panel
Browser->>DockviewJS: Panel visibility change
DockviewJS->>DockviewJS: _panelVisibleChanged.fire({key,status})
DockviewJS->>DotNetRuntime: invokeMethodAsync(panelVisibleChangedCallback, key, status)
DotNetRuntime->>DockViewV2: PanelVisibleChangedCallbackAsync(string key, bool status)
DockViewV2->>DockViewV2: lookup DockViewComponentState by key
DockViewV2->>DockViewComponentState: Visible = status
DockViewV2->>DockViewComponent: Component.SetVisible(status)
DockViewV2->>DockViewV2: optionally invoke OnVisibleStateChangedAsync
Note over DockViewComponent: on next render
DockViewComponent->>DockViewV2: IsRender(Key)
DockViewV2-->>DockViewComponent: returns state.IsRender() (Visible && Render)
Updated class diagram for DockView OnlyWhenVisible render modeclassDiagram
class DockViewV2 {
+string Name
+string LayoutConfig
+bool ShowClose
+bool IsLock
+bool ShowLock
+bool ShowMaximize
+bool IsFloating
+bool ShowFloat
+bool ShowPin
+DockViewRenderMode Renderer
+Func~string[],bool,Task~ OnLockChangedCallbackAsync
+Func~string,bool,Task~ OnVisibleStateChangedAsync
+Func~Task~ OnSplitterCallbackAsync
+Func~Task~ OnInitializedCallbackAsync
+RenderFragment ChildContent
+string Version
+bool? EnableLocalStorage
+string LocalStoragePrefix
+DockViewTheme Theme
-DockViewV2 DockView
-DockViewOptions _options
-ConcurrentDictionary~string,DockViewComponentState~ _componentStates
-HashSet~string~ _loadTabs
+Task Reset(string layoutConfig)
+Task~string~ SaveLayout()
+Task InitializedCallbackAsync()
+Task PanelVisibleChangedCallbackAsync(string key, bool status)
+Task LockChangedCallbackAsync(string[] panels, bool locked)
+Task SplitterCallbackAsync()
+Task LoadTabs(List~string~ tabs)
+void AddComponentState(DockViewComponentState state)
+void RemoveComponentState(string key)
+void UpdateComponentState(string key, bool visible, bool? isLock)
+bool IsRender(string key)
}
class DockViewComponentBase {
<<abstract>>
+DockViewContentType Type
+int? Width
+int? Height
+RenderFragment ChildContent
-List~DockViewComponentBase~ Parent
+void Dispose()
#void Dispose(bool disposing)
}
class DockViewComponent {
+bool ShowHeader
+string Title
+int? TitleWidth
+string TitleClass
+RenderFragment TitleTemplate
+string Class
+bool Visible
+bool? ShowClose
+string Key
+bool? IsLock
+bool? ShowLock
+bool? IsFloating
+bool? ShowFloat
+bool? ShowMaximize
+string Renderer
+bool ShowTitleBar
+string TitleBarIcon
+string TitleBarIconUrl
+Func~Task~ OnClickTitleBarCallback
-DockViewV2 DockView
+void SetVisible(bool visible)
+void SetLock(bool isLock)
+bool IsRender()
+void Dispose(bool disposing)
}
class DockViewComponentState {
+string Key
+bool? IsLock
+bool Visible
+bool Render
+DockViewComponent Component
}
class DockViewComponentStateExtensions {
+bool IsRender()
}
class DockViewConfig {
+bool EnableLocalStorage
+bool IsLock
+bool ShowLock
+bool IsFloating
+bool ShowFloat
+bool ShowClose
+bool ShowPin
+bool ShowMaximize
+DockViewRenderMode Renderer
+string PanelVisibleChangedCallback
+string InitializedCallback
+string LockChangedCallback
+string SplitterCallback
+string LoadTabs
+string LocalStorageKey
+List~DockViewComponentBase~ Contents
+string Theme
+string LayoutConfig
}
class DockViewOptions {
+string Version
+bool? EnableLocalStorage
+string LocalStoragePrefix
}
class DockViewContent {
+List~DockViewComponentBase~ Contents
}
DockViewComponentBase <|-- DockViewComponent
DockViewComponentBase <|-- DockViewContent
DockViewV2 "1" o-- "many" DockViewComponentState : tracks
DockViewComponentState "1" --> "1" DockViewComponent : Component
DockViewComponent "many" --> "1" DockViewV2 : CascadingParameter
DockViewV2 --> DockViewConfig : builds
DockViewV2 --> DockViewOptions : reads
DockViewConfig --> DockViewComponentBase : contents
DockViewComponentStateExtensions ..> DockViewComponentState : extension
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The new DockViewComponentStateExtensions file doesn’t use valid C# extension method syntax (
extension(DockViewComponentState? state)); this should be rewritten as a standard static extension method (e.g.public static bool IsRender(this DockViewComponentState? state)) to compile correctly. - In DockViewV2.SaveLayout you changed the
InvokeAsyncgeneric argument fromstring?tostringbut left the method signature asTask<string?>; align these nullability annotations so the return type and JS interop call agree.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new DockViewComponentStateExtensions file doesn’t use valid C# extension method syntax (`extension(DockViewComponentState? state)`); this should be rewritten as a standard static extension method (e.g. `public static bool IsRender(this DockViewComponentState? state)`) to compile correctly.
- In DockViewV2.SaveLayout you changed the `InvokeAsync` generic argument from `string?` to `string` but left the method signature as `Task<string?>`; align these nullability annotations so the return type and JS interop call agree.
## Individual Comments
### Comment 1
<location path="src/components/BootstrapBlazor.DockView/Extensions/DockViewComponentStateExtensions.cs" line_range="7-9" />
<code_context>
+
+namespace BootstrapBlazor.Components;
+
+static class DockViewComponentStateExtensions
+{
+ extension(DockViewComponentState? state)
+ {
+ /// <summary>
</code_context>
<issue_to_address>
**issue (bug_risk):** The `extension(DockViewComponentState? state)` syntax is not valid C# and will not compile.
To make this an extension method, declare a `public static` method with a `this` parameter inside a `public static` class, e.g.
```csharp
public static class DockViewComponentStateExtensions
{
public static bool IsRender(this DockViewComponentState? state)
{
var render = false;
if (state != null)
{
render = state.Render && state.Visible;
}
return render;
}
}
```
Refactoring to this standard pattern will restore compilation and keep `IsRender()` usable from `DockViewV2`.
</issue_to_address>
### Comment 2
<location path="src/components/BootstrapBlazor.DockView/Components/DockViewV2.razor.cs" line_range="285" />
<code_context>
+ {
+ state.Visible = status;
+
+ // 同步可见状态到组件实例
+ state.Component.SetVisible(status);
+ }
</code_context>
<issue_to_address>
**nitpick (typo):** The comment inside LockChangedCallbackAsync refers to visibility but the code is updating lock state.
Consider updating the Chinese comment to better match the behavior, e.g. change it to “同步锁定状态到组件实例” so it accurately describes syncing the lock state to the component instance.
Suggested implementation:
```csharp
{
state.Locked = status;
// 同步锁定状态到组件实例
state.Component.SetLocked(status);
}
```
If the method name or the component API differs (e.g., `SetLocked` has a different name, or the body uses `SetVisible(status)` instead of `SetLocked(status)`), adjust the SEARCH snippet accordingly so that the line immediately above the `SetLocked`/`SetVisible` call is the one with the outdated Chinese comment `// 同步可见状态到组件实例`. The only intended change is to update that comment text to `// 同步锁定状态到组件实例`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull request overview
Adds an OnlyWhenVisible rendering mode for DockViewV2, aiming to render tab content only when the panel is visible/active, reducing DOM and render cost for large layouts.
Changes:
- Introduces a Blazor-side component state cache to decide whether each
DockViewComponentshould render (OnlyWhenVisible). - Updates DockView JS interop/events to drive lazy tab rendering (
loadTabs) and panel visibility notifications by key. - Adds supporting JS/CSS adjustments to manage visibility/layout edge cases (e.g., first visible split view styling).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/BootstrapBlazor.DockView/wwwroot/js/dockview-utils.js | Updates local-storage restore/visibility handling; fires loadTabs for OnlyWhenVisible |
| src/components/BootstrapBlazor.DockView/wwwroot/js/dockview-panel.js | Triggers loadTabs when panel visibility changes |
| src/components/BootstrapBlazor.DockView/wwwroot/js/dockview-group.js | Adjusts group/panel add logic and marks first visible elements |
| src/components/BootstrapBlazor.DockView/wwwroot/js/dockview-extensions.js | Overrides dockview behaviors (panel close, setVisible); adds template move logic |
| src/components/BootstrapBlazor.DockView/wwwroot/css/dockview-bb.css | Adds split-view pseudo-element rules for first visible behavior |
| src/components/BootstrapBlazor.DockView/Extensions/DockViewComponentStateExtensions.cs | Adds IsRender helper for component render decisions |
| src/components/BootstrapBlazor.DockView/Data/DockViewComponentState.cs | Adds a state model tracking Key/Visible/Render/Lock + component reference |
| src/components/BootstrapBlazor.DockView/Converters/DockViewComponentConverter.cs | Documentation tweak |
| src/components/BootstrapBlazor.DockView/Components/DockViewV2.razor.js | Updates JS callbacks (visibility changed by key; loadTabs invokes .NET) |
| src/components/BootstrapBlazor.DockView/Components/DockViewV2.razor.cs | Implements state tracking + OnlyWhenVisible render decision logic |
| src/components/BootstrapBlazor.DockView/Components/DockViewTitleBar.razor.cs | Doc updates (bilingual summaries) |
| src/components/BootstrapBlazor.DockView/Components/DockViewRenderMode.cs | Keeps OnlyWhenVisible + Always (removes Partial) |
| src/components/BootstrapBlazor.DockView/Components/DockViewOptions.cs | Doc updates (bilingual summaries) |
| src/components/BootstrapBlazor.DockView/Components/DockViewIcon.razor.cs | Doc cleanup |
| src/components/BootstrapBlazor.DockView/Components/DockViewDropdownIcon.razor.cs | Doc cleanup |
| src/components/BootstrapBlazor.DockView/Components/DockViewContent.cs | Doc punctuation cleanup |
| src/components/BootstrapBlazor.DockView/Components/DockViewConfig.cs | Adds docs + refines callback property docs |
| src/components/BootstrapBlazor.DockView/Components/DockViewComponentBase.cs | Doc updates + minor cleanup |
| src/components/BootstrapBlazor.DockView/Components/DockViewComponent.razor.cs | Registers component state; switches rendering decision to DockViewV2 state |
| src/components/BootstrapBlazor.DockView/Components/DockViewComponent.razor | Renders child content based on IsRender() instead of Visible |
| src/components/BootstrapBlazor.DockView/BootstrapBlazor.DockView.csproj | Bumps package version to 10.0.8-beta05 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| internal void SetVisible(bool visible) => Visible = visible; | ||
|
|
||
| internal void SetLock(bool isLock) => IsLock = isLock; | ||
|
|
There was a problem hiding this comment.
The previous public SetVisible(bool) API appears to have been removed and replaced with internal setters (SetVisible/SetLock). If this library is consumed externally, that is a breaking change for callers that were driving visibility/lock state imperatively. Consider keeping the public methods (or introducing a new supported public API) and using internal helpers separately.
| internal void SetVisible(bool visible) => Visible = visible; | |
| internal void SetLock(bool isLock) => IsLock = isLock; | |
| /// <summary> | |
| /// <para lang="zh">设置组件显示状态</para> | |
| /// <para lang="en">Sets the component visibility state</para> | |
| /// </summary> | |
| /// <param name="visible"></param> | |
| public void SetVisible(bool visible) => SetVisibleCore(visible); | |
| /// <summary> | |
| /// <para lang="zh">设置组件锁定状态</para> | |
| /// <para lang="en">Sets the component lock state</para> | |
| /// </summary> | |
| /// <param name="isLock"></param> | |
| public void SetLock(bool isLock) => SetLockCore(isLock); | |
| internal void SetVisibleCore(bool visible) => Visible = visible; | |
| internal void SetLockCore(bool isLock) => IsLock = isLock; |
|
|
||
| DockviewGroupPanel.prototype.setParams = function (data) { | ||
| DockviewGroupPanel.prototype.setParams = function(data) { | ||
| console.log('setParameter', data); |
There was a problem hiding this comment.
DockviewGroupPanel.prototype.setParams contains a console.log debug statement. This will spam the console in production and can leak data. Please remove the log (or gate it behind an explicit debug flag).
| console.log('setParameter', data); |
| private bool IsRender() => DockView.IsRender(Key); | ||
|
|
There was a problem hiding this comment.
IsRender() calls DockView.IsRender(Key) but DockView is nullable. This can throw NullReferenceException if the cascading parameter is missing. Consider DockView?.IsRender(Key) ?? true (or a deterministic fallback) or enforce DockView presence earlier with a clear error.
| extension(DockViewComponentState? state) | ||
| { | ||
| /// <summary> | ||
| /// <para lang="zh">判断组件是否需要渲染</para> | ||
| /// <para lang="en">Determine whether the component needs to be rendered</para> | ||
| /// </summary> | ||
| public bool IsRender() | ||
| { | ||
| // 如果组件 Visible false 表示组件不可见,此时 Render 也不需要渲染 | ||
| var render = false; | ||
| if (state != null) | ||
| { | ||
| // 组件必须可见并且 Active 时才需要渲染 | ||
| render = state.Render && state.Visible; | ||
| } | ||
| return render; | ||
| } |
There was a problem hiding this comment.
This file uses the new extension(DockViewComponentState? state) { ... } syntax, which depends on very recent/preview C# support. Since the repo sets <LangVersion>latest</LangVersion> (not explicitly preview), builds can break depending on the SDK. Prefer a conventional extension method (public static bool IsRender(this DockViewComponentState? state)).
| extension(DockViewComponentState? state) | |
| { | |
| /// <summary> | |
| /// <para lang="zh">判断组件是否需要渲染</para> | |
| /// <para lang="en">Determine whether the component needs to be rendered</para> | |
| /// </summary> | |
| public bool IsRender() | |
| { | |
| // 如果组件 Visible false 表示组件不可见,此时 Render 也不需要渲染 | |
| var render = false; | |
| if (state != null) | |
| { | |
| // 组件必须可见并且 Active 时才需要渲染 | |
| render = state.Render && state.Visible; | |
| } | |
| return render; | |
| } | |
| /// <summary> | |
| /// <para lang="zh">判断组件是否需要渲染</para> | |
| /// <para lang="en">Determine whether the component needs to be rendered</para> | |
| /// </summary> | |
| public static bool IsRender(this DockViewComponentState? state) | |
| { | |
| // 如果组件 Visible false 表示组件不可见,此时 Render 也不需要渲染 | |
| var render = false; | |
| if (state != null) | |
| { | |
| // 组件必须可见并且 Active 时才需要渲染 | |
| render = state.Render && state.Visible; | |
| } | |
| return render; |
| if (!panel.group.locked) { | ||
| removePanel.apply(this, args) | ||
| if (!this.isClearing) { | ||
| this._panelVisibleChanged?.fire({ title: panel.title, status: false }); | ||
| closePanel.call(this, panel); | ||
| if (triggerVisibleChangedCallback) { | ||
| this.accessor._panelVisibleChanged?.fire({ key: panel.params.key, status: false }); | ||
| } |
There was a problem hiding this comment.
When panel.group.locked is true, this override skips calling the original closePanel, but the function continues afterward (including content moves). This can leave the panel visible while its content is detached. Gate the rest of the logic on a successful close.
| /// <para lang="zh">获得/设置 标题模板,默认为 null</para> | ||
| /// <para lang="en">Gets or sets the title template. Default is null</para> | ||
| /// </summary> | ||
| [Parameter] |
There was a problem hiding this comment.
TitleTemplate is a RenderFragment but it’s no longer marked with [JsonIgnore]. Since DockViewComponent instances are serialized into DockViewConfig.Contents for JS interop, this can cause System.Text.Json serialization to fail at runtime. Add [JsonIgnore] back for TitleTemplate (and any other non-serializable component parameters).
| [Parameter] | |
| [Parameter] | |
| [JsonIgnore] |
| base.OnParametersSet(); | ||
|
|
||
| // 同步组件状态到缓存 | ||
| DockView.UpdateComponentState(Key, Visible, IsLock); | ||
| } |
There was a problem hiding this comment.
DockView is a nullable cascading parameter but is dereferenced without a null-check in OnParametersSet (DockView.UpdateComponentState(...)). If DockViewComponent is ever used outside a DockViewV2 (or cascading value is missing), this will throw. Either use DockView?.UpdateComponentState(...) or validate DockView once in OnInitialized and throw a clear exception.
| { | ||
| _loadTabs.Add(tab); | ||
| // 标记是否渲染 | ||
| componnet.Value.Render = tabs.Contains(componnet.Key); |
There was a problem hiding this comment.
LoadTabs creates a HashSet (_loadTabs = tabs.ToHashSet()), but then uses tabs.Contains(...) inside the loop. This defeats the purpose of the set and is O(n*m). Use _loadTabs.Contains(...) (or a local HashSet) for membership checks.
| componnet.Value.Render = tabs.Contains(componnet.Key); | |
| componnet.Value.Render = _loadTabs.Contains(componnet.Key); |
| foreach (var componnet in _componentStates) | ||
| { |
There was a problem hiding this comment.
Typo in loop variable name componnet (should be component). This reduces readability and makes searching harder.
| { | ||
| state.IsLock = locked; | ||
|
|
||
| // 同步可见状态到组件实例 | ||
| state.Component.SetLock(locked); |
There was a problem hiding this comment.
LockChangedCallbackAsync mutates component state (IsLock) and calls SetLock, but it doesn’t trigger any re-render. If lock state affects rendering/behavior on the Blazor side, this update won’t be reflected until another render occurs. Consider calling StateHasChanged() (or a more targeted update) after applying the lock changes.
Link issues
fixes #973
Summary By Copilot
Regression?
Risk
Verification
Packaging changes reviewed?
☑️ Self Check before Merge
Summary by Sourcery
Introduce a visibility-driven rendering mode for DockViewV2 and synchronize dock panel state between Blazor components and the underlying JavaScript dockview instance.
New Features:
Enhancements: