You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vaadin 22:跨状态树移动节点异常,移除@PreserveOnRefresh可解决但需保留

问题:Vaadin 22 + @PreserveOnRefresh 刷新后切换标签页抛出「Can't move a node from one state tree to another」异常

基于Vaadin 22.0.28和Spring框架开发应用,所有组件均标记为@UIScope。当通过MainView下拉框变更触发getUI().ifPresent(ui -> ui.getPage().reload())刷新页面后,尝试隐藏标签页并设置选中标签时,抛出「Can't move a node from one state tree to another」异常。移除@PreserveOnRefresh注解可解决,但业务要求必须保留该注解。已定位异常触发于StateNode类的doSetTree方法,当isOwnerAttached为true时抛出。


视图组件代码

@PageTitle("Instruments")
@Route(value = "instrument", layout = MainView.class)
@RouteAlias(value = "", layout = MainView.class)
@CssImport("./views/instrument/instrument-tabs-view.css")
@PreserveOnRefresh
public class InstrumentTabsView extends VerticalLayout {
    private final Map<Tab, Component> tabsComponentCache = new HashMap<>();
    private final Tab cmdTab = new Tab(getTranslation(T_CMD));
    private final Tab crTab = new Tab(getTranslation(T_CR));
    private final Tabs tabs = new Tabs();
    private Component grid;
    private final Supplier<Universe> universeSupplier;


    public InstrumentTabsView(@Autowired CmdInstrumentsGridView cmdInstrumentsGridView,
                              @Autowired CrInstrumentsGridView crInstrumentsGridView,
                              @Autowired Supplier<Universe> universeSupplier) {
        this.universeSupplier = universeSupplier;
        setSizeFull();

        tabsComponentCache.put(cmdTab, cmdInstrumentsGridView);
        tabsComponentCache.put(crTab, crInstrumentsGridView);


        grid = cmdInstrumentsGridView;
        tabs.add(cmdTab, crTab);
        setAppropriateTabs();
        tabs.addClassName("assetClassTabs");
        tabs.addSelectedChangeListener(event -> {
            Component newGrid = tabsComponentCache.get(event.getSelectedTab());
            replace(grid, newGrid); // <- 异常触发位置
            grid = newGrid;
        });

        add(tabs, grid);

        addAttachListener(e -> {
            ((BaseGridView) grid).refreshGrid();
            setAppropriateTabs();
        });

    }

    private void setAppropriateTabs() {
        Universe universe = universeSupplier.get();
        if (universe != null && UniverseType.VAR.equals(universe.getType())) {
            cmdTab.setVisible(false);
            setSelectedTab(AssetClass.CR);
        }
        else {
            cmdTab.setVisible(true);
            setSelectedTab(AssetClass.CMD);
        }
    }

    public void setSelectedTab(AssetClass assetClass) {
        Tab selectedTab;
        switch (assetClass) {
            case CMD:
                selectedTab = cmdTab;
                break;
            case CR:
                selectedTab = crTab;
                break;
            default:
                selectedTab = null;
                break;
        }
        tabs.setSelectedTab(selectedTab);
    }
}

页面刷新触发代码

getUI().ifPresent(ui -> ui.getPage().reload());

异常触发的Vaadin源码片段

private void doSetTree(StateTree tree) {
        if (tree != this.getOwner()) {
            if (this.getOwner() instanceof StateTree) {
                boolean isOwnerAttached = ((StateTree)this.getOwner()).getRootNode().isAttached();
                boolean isNotReplaced = ComponentUtil.getData(((StateTree)this.getOwner()).getUI(), ReplacedViaPreserveOnRefresh.class) == null;
                if (isOwnerAttached || isNotReplaced) {
                    throw new IllegalStateException("Can't move a node from one state tree to another. If this is intentional, first remove the node from its current state tree by calling removeFromTree");
                }

                this.id = -1;
            }

            this.owner = tree;
        }
    }

解决方案

问题根源在于@UIScope组件在@PreserveOnRefresh刷新后,仍属于旧的UI状态树,当切换标签页尝试复用这些组件时,会触发状态树冲突。

方案1:在Attach阶段重新初始化组件缓存

修改InstrumentTabsView的attachListener,确保刷新后使用的组件属于当前UI的状态树:

addAttachListener(e -> {
    // 清除旧缓存,重新绑定当前UI的@UIScope组件实例
    tabsComponentCache.clear();
    tabsComponentCache.put(cmdTab, cmdInstrumentsGridView);
    tabsComponentCache.put(crTab, crInstrumentsGridView);
    
    // 根据当前Universe重新初始化显示的Grid
    Universe universe = universeSupplier.get();
    if (universe != null && UniverseType.VAR.equals(universe.getType())) {
        grid = crInstrumentsGridView;
    } else {
        grid = cmdInstrumentsGridView;
    }
    
    // 重建布局,确保组件关联当前状态树
    removeAll();
    tabs.removeAll();
    tabs.add(cmdTab, crTab);
    setAppropriateTabs();
    add(tabs, grid);
    
    ((BaseGridView) grid).refreshGrid();
});

方案2:切换组件前强制脱离旧状态树

在调用replace方法前,先将旧组件从父容器移除,并调用内部API脱离旧状态树(不推荐直接调用内部方法,仅作为备选):

tabs.addSelectedChangeListener(event -> {
    Component newGrid = tabsComponentCache.get(event.getSelectedTab());
    // 先移除旧组件并脱离旧状态树
    remove(grid);
    if (grid instanceof StateNodeProvider) {
        ((StateNodeProvider) grid).getStateNode().removeFromTree();
    }
    replace(grid, newGrid);
    grid = newGrid;
});

核心思路:@PreserveOnRefresh会保留旧UI的状态树,而@UIScope组件属于旧UI实例,刷新后必须确保视图使用的组件绑定到当前新状态树,避免跨树复用组件。


内容的提问来源于stack exchange,提问作者WillyM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 23:32:13