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
相关产品推荐
相关产品推荐

