From 999b4561f24be0f0de7a6cc5b74c9e3d61d34f68 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 17 Aug 2026 15:42:16 -0300 Subject: [PATCH 1/2] fix(segmented-bar): detach items from a previous parent before adopting them `onItemsChanged` adopted every new item with `_addView`, which throws "View already has a parent" when the item is still parented elsewhere. Nothing ever un-parents items: `disposeNativeView` leaves `items` untouched, so items keep pointing at a SegmentedBar even after it is destroyed. Binding one `SegmentedBarItem[]` to a SegmentedBar inside a ListView item template therefore crashed on the second cell, since every cell got a fresh bar bound to the same array. Items are now detached through `_removeView` before being adopted, so the item is properly unloaded and torn down and can be re-attached to the new parent's native visual tree. --- .../ui/segmented-bar/segmented-bar-tests.ts | 33 +++++++++++++++++++ .../ui/segmented-bar/segmented-bar-common.ts | 8 ++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/automated/src/ui/segmented-bar/segmented-bar-tests.ts b/apps/automated/src/ui/segmented-bar/segmented-bar-tests.ts index 2bc5983ca2..a5d9267c2c 100644 --- a/apps/automated/src/ui/segmented-bar/segmented-bar-tests.ts +++ b/apps/automated/src/ui/segmented-bar/segmented-bar-tests.ts @@ -274,6 +274,39 @@ export function test_SettingNumberAsTitleFromXML_DoesNotThrow() { }); } +export function test_SettingTheSameItemsToAnotherSegmentedBar_DoesNotThrow() { + const items = _createItems(3); + const firstBar = _createSegmentedBar(); + + buildUIAndRunTest(firstBar, function (views: Array) { + firstBar.items = items; + + const secondBar = _createSegmentedBar(); + secondBar.items = items; + + items.forEach((item, i) => TKUnit.assertEqual(item.parent, secondBar, `Item ${i} should be parented to the SegmentedBar it was last assigned to.`)); + }); +} + +export function test_SettingTheItemsOfATornDownSegmentedBarToANewOne_DoesNotThrow() { + const items = _createItems(3); + const firstBar = _createSegmentedBar(); + + buildUIAndRunTest(firstBar, function (views: Array) { + firstBar.items = items; + }); + + // The test above leaves the page without content, so firstBar is torn down while its items still point at it. + const secondBar = _createSegmentedBar(); + + buildUIAndRunTest(secondBar, function (views: Array) { + secondBar.items = items; + + items.forEach((item, i) => TKUnit.assertEqual(item.parent, secondBar, `Item ${i} should be parented to the SegmentedBar it was last assigned to.`)); + TKUnit.assertEqual(segmentedBarTestsNative.getNativeItemsCount(secondBar), items.length, 'Native items should be created for the SegmentedBar the items were re-assigned to.'); + }); +} + /*export function testBackgroundColorUpdatedAfterItemSelected() { let segmentedBar = new segmentedBarModule.SegmentedBar(); let item1 = new segmentedBarModule.SegmentedBarItem(); diff --git a/packages/core/ui/segmented-bar/segmented-bar-common.ts b/packages/core/ui/segmented-bar/segmented-bar-common.ts index e71651e17c..e685f90f27 100644 --- a/packages/core/ui/segmented-bar/segmented-bar-common.ts +++ b/packages/core/ui/segmented-bar/segmented-bar-common.ts @@ -84,7 +84,13 @@ export abstract class SegmentedBarBase extends View implements SegmentedBarDefin if (newItems) { for (let i = 0, count = newItems.length; i < count; i++) { - this._addView(newItems[i]); + const item = newItems[i]; + // A view can only have one parent, and _addView throws if it already has one. + if (item.parent && item.parent !== this) { + item.parent._removeView(item); + } + + this._addView(item); } } } From 682aebac551cf1ac0a4c8214c0f6b0ca7b241e07 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 17 Aug 2026 15:42:26 -0300 Subject: [PATCH 2/2] fix(tab-view): detach items from a previous parent before adopting them `onItemsChanged` adopted every new item with `_addView`, which throws "View already has a parent" when the item is still parented elsewhere, and nothing ever un-parents items when a TabView is destroyed. Binding one `TabViewItem[]` array to more than one TabView therefore crashed. Items are now detached through `_removeView` before being adopted, so they are unloaded and torn down properly and can be re-attached to the new parent's native visual tree. --- .../src/ui/tab-view/tab-view-tests.ts | 30 +++++++++++++++++++ packages/core/ui/tab-view/tab-view-common.ts | 5 ++++ 2 files changed, 35 insertions(+) diff --git a/apps/automated/src/ui/tab-view/tab-view-tests.ts b/apps/automated/src/ui/tab-view/tab-view-tests.ts index 7288cbd95d..17c645552b 100644 --- a/apps/automated/src/ui/tab-view/tab-view-tests.ts +++ b/apps/automated/src/ui/tab-view/tab-view-tests.ts @@ -213,6 +213,36 @@ export class TabViewTest extends UITest { }, 'Binding TabView to a TabViewItem with null view should throw.'); }; + public test_SettingTheSameItemsToAnotherTabView_DoesNotThrow = function () { + var tabView = this.testView; + var items = this._createItems(2); + tabView.items = items; + this.waitUntilTestElementIsLoaded(); + + var secondTabView = new tabViewModule.TabView(); + secondTabView.items = items; + + items.forEach((item, i) => TKUnit.assertEqual(item.parent, secondTabView, `Item ${i} should be parented to the TabView it was last assigned to.`)); + }; + + public test_SettingTheItemsOfATornDownTabViewToANewOne_DoesNotThrow = function () { + var tabView = this.testView; + var items = this._createItems(2); + tabView.items = items; + this.waitUntilTestElementIsLoaded(); + + // Detaching the TabView tears it down, but its items still point at it. + this.testPage.content = null; + + var secondTabView = new tabViewModule.TabView(); + secondTabView.items = items; + this.testPage.content = secondTabView; + TKUnit.waitUntilReady(() => secondTabView.isLoaded, 1); + + items.forEach((item, i) => TKUnit.assertEqual(item.parent, secondTabView, `Item ${i} should be parented to the TabView it was last assigned to.`)); + TKUnit.assertEqual(tabViewTestsNative.getNativeTabCount(secondTabView), items.length, 'Native tabs should be created for the TabView the items were re-assigned to.'); + }; + public testWhenSelectingATabNativelySelectedIndexIsUpdatedProperly = function () { var tabView = this.testView; tabView.items = this._createItems(2); diff --git a/packages/core/ui/tab-view/tab-view-common.ts b/packages/core/ui/tab-view/tab-view-common.ts index 9c8f529059..043f5434a0 100644 --- a/packages/core/ui/tab-view/tab-view-common.ts +++ b/packages/core/ui/tab-view/tab-view-common.ts @@ -207,6 +207,11 @@ export class TabViewBase extends View implements TabViewDefinition, AddChildFrom throw new Error(`TabViewItem must have a view.`); } + // A view can only have one parent, and _addView throws if it already has one. + if (item.parent && item.parent !== this) { + item.parent._removeView(item); + } + this._addView(item); }); }