When using DockManager with OnStateInit="@OnStateInit", the FloatingTopChanged and FloatingLeftChanged events dont work.

1 Answer 51 Views
DockManager
Sen
Top achievements
Rank 1
Sen asked on 29 Jan 2026, 08:38 PM | edited on 29 Jan 2026, 09:08 PM

https://blazorrepl.telerik.com/cKuvmXwu53TrfnCx19

During the initial execution, the code behaves as expected, but after invoking setState, the FloatingTopChanged and FloatingLeftChanged events stop firing. Removing the DockPaneDemo item from localStorage and reloading the page restores the expected event behavior.

sample project attached.

1 Answer, 1 is accepted

Sort by
0
Ivan Danchev
Telerik team
answered on 03 Feb 2026, 05:13 PM

Hello Sen,

When you provide a DockManagerState object via OnStateInit or call SetState(), the DockManager renders from that state instead of your declarative markup, which breaks the event handler bindings defined in your Razor code.

In your case, we recommend not setting  args.DockManagerState in OnStateInit when using declarative floating panes. Instead:
1. Load saved positions into DynamicPanes before rendering (in OnInitializedAsync)
2. Let Blazor's declarative rendering handle the DockManager markup with event bindings intact
3. Update DynamicPanes collection when restoring state, not the DockManager state directly

I've attached the modified project. 

Key Changes
•  OnInitializedAsync: load saved state into DynamicPanes before render
•  OnStateInit: do nothing (let declarative markup control rendering)
•  SetDockManagerState(): update DynamicPanes from saved state instead of calling SetState()
•  Added null checks for ContainerBounds and element references
•  Added error handling for JavaScript interop

As a result, the events fire consistently because declarative markup with event bindings is always used for rendering.

Regards,
Ivan Danchev
Progress Telerik

Love the Telerik and Kendo UI products and believe more people should try them? Invite a fellow developer to become a Progress customer and each of you can get a $50 Amazon gift voucher.

Sen
Top achievements
Rank 1
commented on 04 Feb 2026, 03:58 PM | edited

Hi @Ivan ,

Thanks, your code is working fine. but I found a new issue, using your code, we can't save the state for docked the pane, always popup as FloatingPanes. 

Ivan Danchev
Telerik team
commented on 09 Feb 2026, 01:08 PM

Hi Sen, you may try the following workaround:

@using Telerik.Blazor.Components.DockManager.Internal
@using System.Text.Json

@if (!isLoading)
{
<div style="display: flex; gap: 10px;">
    <TelerikButton OnClick="@GetDockManagerState">Get State</TelerikButton>
    <TelerikButton OnClick="@SetDockManagerState">Set State</TelerikButton>
    <TelerikButton OnClick="@ClearDockManagerState" ThemeColor="@ThemeConstants.Button.ThemeColor.Error">Clear State</TelerikButton>
    <div style="width: 120px;">
        <TelerikMenu Data="@MenuList">
            <ItemTemplate Context="item">
                @{
                    var showCheckBox = !string.IsNullOrEmpty(item.Id);
                    if (showCheckBox)
                    {
                        <span @onclick:stopPropagation>
                            <TelerikCheckBox Value="@item.IsVisible"
                                             ValueChanged="@((bool val) => OnCheckChanged(val, item))" />
                            @item.Content
                        </span>
                    }
                    else
                    {
                        <span class="k-menu-link-text">@item.Content</span>
                    }
                }
            </ItemTemplate>
        </TelerikMenu>
    </div>
</div>
<div @ref="myDivRef" >
<TelerikDockManager @ref="DockManagerRef"
                    Height="750px"
                    OnStateInit="@OnStateInit"
                    OnStateChanged="@OnStateChanged" >
                    @*  *@
    <DockManagerPanes>
        <DockManagerSplitPane>
            <Panes>
                @foreach (var pane in DynamicPanes.Where(p => p.IsDocked))
                {
                    <DockManagerContentPane Id="@pane.Id"
                                            Visible="@pane.IsVisible"
                                            Dockable="true"
                                            VisibleChanged="@((bool val) => OnPaneVisibleChanged(val, index: pane.Id))"
                                            HeaderText="@pane.Id">
                        <Content>
                            <p>Docked Pane: @pane.Id</p>
                        </Content>
                    </DockManagerContentPane>
                }
            </Panes>
        </DockManagerSplitPane>
    </DockManagerPanes>

    <DockManagerFloatingPanes>
        @foreach (var pane in DynamicPanes.Where(p => !p.IsDocked))
        {
            <DockManagerSplitPane FloatingTop=@pane.Top
                                  FloatingLeft=@pane.Left
                                  FloatingWidth="@pane.Width" 
                                  FloatingHeight="@pane.Height"
                                  FloatingTopChanged="@((val) => OnFloatingTopChanged(val, pane.Id))"
                                  FloatingLeftChanged="@((val) => OnFloatingLeftChanged(val, pane.Id))">
                <Panes>
                    <DockManagerContentPane Id="@pane.Id"
                                            Visible="@pane.IsVisible"
                                            Dockable="true"
                                            VisibleChanged="@((bool val) => OnPaneVisibleChanged(val, index: pane.Id))"
                                            HeaderText="@pane.Id">
                        <Content>
                            <p>Floating Pane: @pane.Id - Left: @pane.Left, Top: @pane.Top</p>
                        </Content>
                    </DockManagerContentPane>
                </Panes>
            </DockManagerSplitPane>
        }

    </DockManagerFloatingPanes>
</TelerikDockManager>
</div>
}
else
{
    <p>Loading...</p>
}

@code {
    private string lsKey = "DockPaneDemo";
    private bool isLoading = true;
    public List<PaneItem> DynamicPanes { get; set; } = new();

    private List<PaneItem> MenuList { get; set; } = new();

    [Inject]
    private IJSRuntime _jsRuntime { get; set; }
    private TelerikDockManager? DockManagerRef { get; set; }
    private DockManagerState? CurrentState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        isLoading = true;
        DynamicPanes = new List<PaneItem>()
        {
            new PaneItem { Id = "V1001", Content = "View1001", IsVisible = true, IsDocked = false, Left = "100px", Top="100px", Width ="250px", Height="200px" },
            new PaneItem { Id = "V1002", Content = "View1002", IsVisible = true, IsDocked = false, Left = "120px", Top="120px", Width ="250px", Height="200px" },
            new PaneItem { Id = "V1003", Content = "View1003", IsVisible = true, IsDocked = false, Left = "140px", Top="140px", Width ="250px", Height="200px" },
            new PaneItem { Id = "V1004", Content = "View1004", IsVisible = true, IsDocked = false, Left = "160px", Top="160px", Width ="250px", Height="200px" },
            new PaneItem { Id = "V1005", Content = "View1005", IsVisible = true, IsDocked = false, Left = "180px", Top="180px", Width ="250px", Height="200px" },
        };

        MenuList.Add(new PaneItem() { Id = string.Empty, Content = "Views", Items = DynamicPanes });

        // Load saved state and update DynamicPanes BEFORE rendering
        await LoadSavedState();

        // Log the positions after loading
        foreach (var pane in DynamicPanes)
        {
            Console.WriteLine($"After LoadSavedState - {pane.Id}: Left={pane.Left}, Top={pane.Top}, IsDocked={pane.IsDocked}, IsVisible={pane.IsVisible}");
        }

        Console.WriteLine("Component initialized");
        isLoading = false;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Add a small delay to ensure the DOM is fully ready
            await Task.Delay(100);
            await GetBound();
        }
    }

    private BoundingClientRect? ContainerBounds;
    private ElementReference myDivRef;
    private int ParsePixels(string value) =>
    double.TryParse(value.Replace("px", ""), out var d) ? (int)d : 0;
    private async Task GetBound()
    {
        try
        {
            ContainerBounds = await _jsRuntime.InvokeAsync<BoundingClientRect>("getElementPosition", myDivRef);

            // Validate that we got valid bounds
            if (ContainerBounds != null && ContainerBounds.Width > 0 && ContainerBounds.Height > 0)
            {
                Console.WriteLine($"Container bounds: Top={ContainerBounds.Top}, Left={ContainerBounds.Left}, Width={ContainerBounds.Width}, Height={ContainerBounds.Height}");
            }
            else
            {
                Console.WriteLine("Invalid container bounds received, will retry on next render");
                ContainerBounds = null;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error getting container bounds: {ex.Message}");
            ContainerBounds = null;
        }
    }

    private void GetDockManagerState()
    {
        CurrentState = DockManagerRef!.GetState();
        Console.WriteLine("State captured");
    }

    private void SetDockManagerState()
    {
        if (CurrentState != null)
        {
            // First, mark all panes as not docked
            foreach (var pane in DynamicPanes)
            {
                pane.IsDocked = false;
            }

            // Update DynamicPanes from the floating panes state
            if (CurrentState.FloatingPanesState != null)
            {
                foreach (var floatingPane in CurrentState.FloatingPanesState)
                {
                    if (floatingPane.Panes?.Count > 0)
                    {
                        var contentPane = floatingPane.Panes[0];
                        var matchingPane = DynamicPanes.FirstOrDefault(p => p.Id == contentPane.Id);

                        if (matchingPane != null)
                        {
                            matchingPane.Left = floatingPane.FloatingLeft;
                            matchingPane.Top = floatingPane.FloatingTop;
                            matchingPane.Width = floatingPane.FloatingWidth;
                            matchingPane.Height = floatingPane.FloatingHeight;
                            matchingPane.IsVisible = contentPane.Visible;
                            matchingPane.IsDocked = false;
                        }
                    }
                }
            }

            // Mark docked panes from RootPaneState
            if (CurrentState.RootPaneState != null)
            {
                MarkDockedPane(CurrentState.RootPaneState);
            }

            Console.WriteLine("State restored to DynamicPanes");
            StateHasChanged();
        }
    }

    private async Task ClearDockManagerState()
    {
        await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", lsKey);
        // Optionally reload the page to see fresh state
        await _jsRuntime.InvokeVoidAsync("location.reload");
    }

    public async Task AddItem(string key, string value)
    {
        await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
    }

    public async Task<string> GetItem(string key)
    {
        return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
    }

    public async Task OnStateInit(DockManagerStateEventArgs args)
    {
        // Don't set args.DockManagerState - let declarative markup control rendering
        // This keeps event bindings (FloatingTopChanged, FloatingLeftChanged) working
        // The saved positions are already loaded into DynamicPanes in OnInitializedAsync
        Console.WriteLine("DockManager -> OnStateInit (using declarative markup)");
    }

    private async Task LoadSavedState()
    {
        try
        {
            var state = await GetItem(lsKey);
            if (!string.IsNullOrEmpty(state))
            {
                var deserializedState = JsonSerializer.Deserialize<DockManagerState>(state);
                if (deserializedState != null)
                {
                    UpdateDynamicPanesFromState(deserializedState);
                    Console.WriteLine("Saved state loaded into DynamicPanes");
                }
            }
        }
        catch (Exception ex) 
        { 
            Console.WriteLine($"Error loading saved state: {ex.Message}");
        }
    }

    private void MarkDockedPane(DockManagerSplitPaneState paneState)
    {
        // Recursively check child panes
        if (paneState.Panes != null && paneState.Panes.Count > 0)
        {
            foreach (var childPane in paneState.Panes)
            {
                // Check if this is a content pane
                if (!string.IsNullOrEmpty(childPane.Id))
                {
                    var matchingPane = DynamicPanes.FirstOrDefault(p => p.Id == childPane.Id);
                    if (matchingPane != null)
                    {
                        matchingPane.IsDocked = true;
                        matchingPane.IsVisible = childPane.Visible;
                        Console.WriteLine($"Marked {matchingPane.Id} as docked");
                    }
                }

                // If this is a split pane, recursively check its children
                if (childPane is DockManagerSplitPaneState splitPane)
                {
                    MarkDockedPane(splitPane);
                }
            }
        }
    }

    public async Task OnStateChanged(DockManagerStateEventArgs args)
    {
        // Save state to localStorage
        await AddItem(lsKey, JsonSerializer.Serialize(args.DockManagerState));

        // Update DynamicPanes to track docked vs floating panes
        UpdateDynamicPanesFromState(args.DockManagerState);

        Console.WriteLine("DockManager state saved");
    }

    private void UpdateDynamicPanesFromState(DockManagerState state)
    {
        // First, mark all panes as not docked
        foreach (var pane in DynamicPanes)
        {
            pane.IsDocked = false;
        }

        // Update floating pane info
        if (state.FloatingPanesState != null)
        {
            foreach (var floatingPane in state.FloatingPanesState)
            {
                if (floatingPane.Panes?.Count > 0)
                {
                    var contentPane = floatingPane.Panes[0];
                    var matchingPane = DynamicPanes.FirstOrDefault(p => p.Id == contentPane.Id);

                    if (matchingPane != null)
                    {
                        matchingPane.Left = floatingPane.FloatingLeft;
                        matchingPane.Top = floatingPane.FloatingTop;
                        matchingPane.Width = floatingPane.FloatingWidth;
                        matchingPane.Height = floatingPane.FloatingHeight;
                        matchingPane.IsVisible = contentPane.Visible;
                        matchingPane.IsDocked = false;
                    }
                }
            }
        }

        // Mark docked panes from RootPaneState
        if (state.RootPaneState != null)
        {
            MarkDockedPane(state.RootPaneState);
        }
    }


    private void OnCheckChanged(bool newValue, PaneItem item)
    {
        item.IsVisible = newValue;
    }

    private void OnPaneVisibleChanged(bool newValue, string index)
    {
        var _pane = DynamicPanes.Single(x => x.Id == index);
        _pane.IsVisible = newValue;
    }

    private void OnFloatingTopChanged(string newTop, string paneId)
    {
        var pane = DynamicPanes.FirstOrDefault(x => x.Id == paneId);
        if (pane != null)
        {
            pane.Top = newTop;
            Console.WriteLine($"Pane {paneId} top changed to: {newTop}");
        }
    }

    private void OnFloatingLeftChanged(string newLeft, string paneId)
    {
        var pane = DynamicPanes.FirstOrDefault(x => x.Id == paneId);
        if (pane != null)
        {
            pane.Left = newLeft;
            Console.WriteLine($"Pane {paneId} left changed to: {newLeft}");
        }
    }
}

On Page Load:

1. OnInitializedAsync loads the saved state from localStorage into DynamicPanes
2. UpdateDynamicPanesFromState updates positions and marks panes as docked/floating
3. Component renders declaratively with panes in the correct sections
4. OnStateInit does nothing - lets declarative rendering handle everything

When the user interacts:
1. User moves a floating pane → FloatingTopChanged/FloatingLeftChanged fire → Update DynamicPanes
2. User docks a pane → OnStateChanged fires → Updates IsDocked flag → State saved to localStorage
3. On next page load → Pane appears in docked section

Sen
Top achievements
Rank 1
commented on 10 Feb 2026, 01:53 PM

Hi @Ivan, thanks, I tested the code, but it doesn’t work.
Ivan Danchev
Telerik team
commented on 11 Feb 2026, 03:58 PM

Hi Sen,

After further testing of various approaches, I can conclude that when

1. args.DockManagerState is set in OnStateInit:

  • completely ignores all declarative markup in <DockManagerPanes> and <DockManagerFloatingPanes>
  • Even if we clear FloatingPanesState in the state object, the declarative <DockManagerFloatingPanes> section is never rendered
  • Result: no declarative components → No event handler bindings → FloatingTopChanged/FloatingLeftChanged do not fire after setting the state and reloading the page

2. On the other hand, if we don't set args.DockManagerState in OnStateInit:

  • All panes render declaratively from markup
  • Event handlers work
  • however,  docked panes are only tracked in our IsDocked flag
  • When we try to render them declaratively in <DockManagerPanes>, we get duplicates (DockManager already docked them internally)
  • If we DON'T render them declaratively, they disappear on reload (no state restoration)

In summary:
1.  Scenario A: Fully declarative

  • Events work
  • Can't persist docked panes (they're in DockManager's internal state, not our markup)

2.  Scenario B: Set state in OnStateInit

  • Docked panes persist
  • Events don't fire (declarative markup ignored)

Since we don't have a viable workaround to suggest, consider voting for this feature request: https://feedback.telerik.com/blazor/1709526-data-drvien-dockmanager 

If implemented, it would add a more robust state management to the DockManager, which would make saving and restoring the state much easier.

Tags
DockManager
Asked by
Sen
Top achievements
Rank 1
Answers by
Ivan Danchev
Telerik team
Share this question
or