Skip to content

Commit f20a9d4

Browse files
authored
Merge pull request #1299 from unoplatform/dev/xygu/20241204/tabbar-nested-tbi-containerstyle
fix(TabBar): dp exceptions when using TBI as ItemTemplate root
2 parents c84aa79 + 1815d9b commit f20a9d4

File tree

4 files changed

+144
-62
lines changed

4 files changed

+144
-62
lines changed

src/Uno.Toolkit.RuntimeTests/Tests/TabBarTests.cs

+49-19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ namespace Uno.Toolkit.RuntimeTests.Tests
3636
[RunsOnUIThread]
3737
internal partial class TabBarTests // test cases
3838
{
39+
[TestMethod]
40+
public async Task TabBar1285_ICS_With_TBI_ItemTemplate()
41+
{
42+
// note: this bug doesnt happen with ItemsSource = [TBI,...]
43+
// because IsItemItsOwnContainerOverride=true. It only occurs
44+
// with the ItemTemplate>DataTemplate>TBI setup (IsUsingOwnContainerAsTemplateRoot),
45+
// which cause a ContentPresnter to be created as the item container.
46+
var source = Enumerable.Range(0, 1).ToArray();
47+
var SUT = new TabBar
48+
{
49+
ItemsSource = source,
50+
ItemTemplate = XamlHelper.LoadXaml<DataTemplate>("""
51+
<DataTemplate>
52+
<utu:TabBarItem Content="{Binding}" />
53+
</DataTemplate>
54+
"""),
55+
};
56+
await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
57+
}
58+
3959
[TestMethod]
4060
[DataRow(new int[0], null)]
4161
[DataRow(new[] { 1 }, 1)]
@@ -137,15 +157,23 @@ public async Task SetSelectedIndex()
137157
public async Task Verify_Indicator_Max_Size()
138158
{
139159
var source = Enumerable.Range(0, 3).Select(x => new TabBarItem { Content = x }).ToArray();
140-
var indicator = new Border() { Height = 5, Background = new SolidColorBrush(Colors.Red) };
141160
var SUT = new TabBar
142161
{
143162
ItemsSource = source,
144-
SelectionIndicatorContent = indicator,
163+
SelectionIndicatorContent = "asd",
164+
SelectionIndicatorContentTemplate = XamlHelper.LoadXaml<DataTemplate>("""
165+
<DataTemplate>
166+
<Border x:Name="SutIndicator" Height="5" Background="Red" />
167+
</DataTemplate>
168+
"""),
145169
};
146170

147171
await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
148172

173+
var presenter = SUT.GetFirstDescendant<TabBarSelectionIndicatorPresenter>(x => x.Visibility == Visibility.Visible);
174+
var indicator = presenter?.GetFirstDescendant<Border>("SutIndicator")!;
175+
Assert.IsNotNull(indicator, "Failed to find Border#SutIndicator");
176+
149177
source[0].IsSelected = true;
150178
await UnitTestsUIContentHelper.WaitForIdle();
151179

@@ -163,37 +191,39 @@ public async Task Verify_Indicator_Max_Size()
163191
}
164192

165193
[TestMethod]
166-
[DataRow(Orientation.Horizontal, IndicatorTransitionMode.Snap, DisplayName = "Horizontal Snap")]
167-
[DataRow(Orientation.Horizontal, IndicatorTransitionMode.Slide, DisplayName = "Horizontal Slide")]
168-
[DataRow(Orientation.Vertical, IndicatorTransitionMode.Snap, DisplayName = "Vertical Snap")]
169-
[DataRow(Orientation.Vertical, IndicatorTransitionMode.Slide, DisplayName = "Vertical Slide")]
194+
[DataRow(Orientation.Horizontal, IndicatorTransitionMode.Snap)]
195+
[DataRow(Orientation.Horizontal, IndicatorTransitionMode.Slide)]
196+
[DataRow(Orientation.Vertical, IndicatorTransitionMode.Snap)]
197+
[DataRow(Orientation.Vertical, IndicatorTransitionMode.Slide)]
170198
public async Task Verify_Indicator_Transitions(Orientation orientation, IndicatorTransitionMode transitionMode)
171199
{
172200
const int NumItems = 3;
173201
const double ItemSize = 100d;
202+
174203
var source = Enumerable.Range(0, NumItems).ToArray();
175-
var indicator = new Border() { Background = new SolidColorBrush(Colors.Red) };
176204
var SUT = new TabBar
177205
{
178206
Orientation = orientation,
179207
ItemsSource = source,
180-
SelectionIndicatorContent = indicator,
208+
Width = orientation == Orientation.Horizontal ? ItemSize * NumItems : double.NaN,
209+
Height = orientation == Orientation.Vertical ? ItemSize * NumItems : double.NaN,
210+
SelectionIndicatorContent = "asd",
211+
SelectionIndicatorContentTemplate = XamlHelper.LoadXaml<DataTemplate>($"""
212+
<DataTemplate>
213+
<Border x:Name="SutIndicator"
214+
{(orientation == Orientation.Horizontal ? "Height" : "Width")}="5"
215+
Background="Red" />
216+
</DataTemplate>
217+
"""),
181218
SelectionIndicatorTransitionMode = transitionMode,
182219
};
183220

184-
if (orientation == Orientation.Horizontal)
185-
{
186-
SUT.Width = ItemSize * NumItems;
187-
indicator.Height = 5;
188-
}
189-
else
190-
{
191-
SUT.Height = ItemSize * NumItems;
192-
indicator.Width = 5;
193-
}
194-
195221
await UnitTestUIContentHelperEx.SetContentAndWait(SUT);
196222

223+
var presenter = SUT.GetFirstDescendant<TabBarSelectionIndicatorPresenter>(x => x.Visibility == Visibility.Visible);
224+
var indicator = presenter?.GetFirstDescendant<Border>("SutIndicator")!;
225+
Assert.IsNotNull(indicator, "Failed to find Border#SutIndicator");
226+
197227
for (int i = 0; i < NumItems; i++)
198228
{
199229
SUT.SelectedIndex = i;

src/Uno.Toolkit.UI/Controls/TabBar/TabBar.cs

+80-40
Original file line numberDiff line numberDiff line change
@@ -73,32 +73,79 @@ protected override DependencyObject GetContainerForItemOverride()
7373

7474
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
7575
{
76-
base.PrepareContainerForItemOverride(element, item);
76+
if (IsUsingOwnContainerAsTemplateRoot && element is ContentPresenter cp)
77+
{
78+
// ItemsControl::PrepareContainerForItemOverride will apply the ItemContainerStyle to the element which is not something we want here,
79+
// since it can throw: The DP [WrongDP] is owned by [Control] and cannot be used on [ContentPresenter].
80+
// While this doesnt break the control or the visual, it can cause a scaling performance degradation.
81+
82+
cp.ContentTemplate = ItemTemplate;
83+
cp.ContentTemplateSelector = ItemTemplateSelector;
84+
85+
cp.DataContext = item;
86+
SetContent(cp, item);
87+
88+
#if !HAS_UNO
89+
// force template materialization
90+
cp.Measure(Size.Empty);
91+
#endif
7792

78-
void SetupTabBarItem(TabBarItem item)
93+
if (cp.GetFirstChild() is TabBarItem tbi)
94+
{
95+
ApplyContainerStyle(tbi);
96+
SetupTabBarItem(tbi);
97+
}
98+
}
99+
else
79100
{
80-
item.IsSelected = IsSelected(IndexFromContainer(element));
81-
item.Click += OnTabBarItemClick;
82-
item.IsSelectedChanged += OnTabBarIsSelectedChanged;
101+
base.PrepareContainerForItemOverride(element, item);
102+
if (element is TabBarItem tbi)
103+
{
104+
SetupTabBarItem(tbi);
105+
}
83106
}
84107

85-
if (element is TabBarItem container)
108+
void SetContent(ContentPresenter cp, object item)
86109
{
87-
SetupTabBarItem(container);
110+
if (string.IsNullOrEmpty(DisplayMemberPath))
111+
{
112+
cp.Content = item;
113+
}
114+
else
115+
{
116+
cp.SetBinding(ContentPresenter.ContentProperty, new Binding
117+
{
118+
Source = item,
119+
Path = new(DisplayMemberPath),
120+
});
121+
}
88122
}
89-
else if (IsUsingOwnContainerAsTemplateRoot &&
90-
element is ContentPresenter outerContainer)
123+
void ApplyContainerStyle(TabBarItem tbi)
91124
{
92-
var templateRoot = outerContainer.ContentTemplate.LoadContent();
93-
if (templateRoot is TabBarItem tabBarItem)
125+
var localStyleValue = tbi.ReadLocalValue(FrameworkElement.StyleProperty);
126+
var isStyleSetFromTabBar = tbi.IsStyleSetFromTabBar;
127+
128+
if (localStyleValue == DependencyProperty.UnsetValue || isStyleSetFromTabBar)
94129
{
95-
outerContainer.ContentTemplate = null;
96-
SetupTabBarItem(tabBarItem);
97-
tabBarItem.DataContext = item;
98-
tabBarItem.Style ??= ItemContainerStyle;
99-
outerContainer.Content = tabBarItem;
130+
var style = ItemContainerStyle ?? ItemContainerStyleSelector?.SelectStyle(item, tbi);
131+
if (style is { })
132+
{
133+
tbi.Style = style;
134+
tbi.IsStyleSetFromTabBar = true;
135+
}
136+
else
137+
{
138+
tbi.ClearValue(FrameworkElement.StyleProperty);
139+
tbi.IsStyleSetFromTabBar = false;
140+
}
100141
}
101142
}
143+
void SetupTabBarItem(TabBarItem tbi)
144+
{
145+
tbi.IsSelected = IsSelected(IndexFromContainer(element));
146+
tbi.Click += OnTabBarItemClick;
147+
tbi.IsSelectedChanged += OnTabBarIsSelectedChanged;
148+
}
102149
}
103150

104151
internal virtual bool IsSelected(int index)
@@ -108,30 +155,26 @@ internal virtual bool IsSelected(int index)
108155

109156
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
110157
{
111-
base.ClearContainerForItemOverride(element, item);
112-
113-
void TearDownTabBarItem(TabBarItem item)
158+
if (IsUsingOwnContainerAsTemplateRoot && element is ContentPresenter cp)
114159
{
115-
item.Click -= OnTabBarItemClick;
116-
item.IsSelectedChanged -= OnTabBarIsSelectedChanged;
117-
if (!IsUsingOwnContainerAsTemplateRoot)
160+
if (cp.GetFirstChild() is TabBarItem tbi)
118161
{
119-
item.Style = null;
162+
TearDownTabBarItem(tbi);
120163
}
121164
}
122-
if (element is TabBarItem container)
123-
{
124-
TearDownTabBarItem(container);
125-
}
126-
else if (IsUsingOwnContainerAsTemplateRoot &&
127-
element is ContentPresenter outerContainer)
165+
else
128166
{
129-
if (outerContainer.Content is TabBarItem innerContainer)
167+
base.ClearContainerForItemOverride(element, item);
168+
if (element is TabBarItem tbi)
130169
{
131-
TearDownTabBarItem(innerContainer);
132-
innerContainer.DataContext = null;
170+
TearDownTabBarItem(tbi);
133171
}
134-
outerContainer.Content = null;
172+
}
173+
174+
void TearDownTabBarItem(TabBarItem item)
175+
{
176+
item.Click -= OnTabBarItemClick;
177+
item.IsSelectedChanged -= OnTabBarIsSelectedChanged;
135178
}
136179
}
137180

@@ -420,9 +463,9 @@ private void RaiseSelectionChangedEvent(object? prevItem, object? nextItem)
420463
// Afterward, pass the resulting `ContentPresenter` as a parameter to this method.
421464
internal TabBarItem? GetInnerContainer(DependencyObject? container)
422465
{
423-
if (IsUsingOwnContainerAsTemplateRoot && container is ContentPresenter cp)
466+
if (IsUsingOwnContainerAsTemplateRoot)
424467
{
425-
return cp.Content as TabBarItem;
468+
return (container as ContentPresenter)?.GetFirstChild() as TabBarItem;
426469
}
427470

428471
return container as TabBarItem;
@@ -431,12 +474,9 @@ private void RaiseSelectionChangedEvent(object? prevItem, object? nextItem)
431474
internal DependencyObject? InnerContainerFromIndex(int index)
432475
{
433476
var container = ContainerFromIndex(index);
434-
if (IsUsingOwnContainerAsTemplateRoot && container is ContentPresenter cp)
435-
{
436-
container = cp.Content as DependencyObject;
437-
}
477+
var inner = GetInnerContainer(container);
438478

439-
return container;
479+
return inner;
440480
}
441481

442482
private TabBarItem? InnerContainerFromIndexSafe(int index)

src/Uno.Toolkit.UI/Controls/TabBar/TabBarItem.Properties.cs

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public object CommandParameter
9595
DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(TabBarItem), new PropertyMetadata(null, OnPropertyChanged));
9696
#endregion
9797

98+
internal bool IsStyleSetFromTabBar { get; set; }
99+
98100
private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
99101
{
100102
var owner = (TabBarItem)sender;

src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs

+13-3
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ void Print(object o, int depth)
135135
.OfType<T>()
136136
.FirstOrDefault();
137137

138+
public static T? GetFirstDescendant<T>(this DependencyObject reference, string name) where T : FrameworkElement => GetDescendants(reference)
139+
.OfType<T>()
140+
.FirstOrDefault(x => x.Name == name);
141+
138142
/// <summary>
139143
/// Returns the first descendant of a specified type that satisfies the <paramref name="predicate"/>.
140144
/// </summary>
@@ -146,9 +150,8 @@ void Print(object o, int depth)
146150
.OfType<T>()
147151
.FirstOrDefault(predicate);
148152

149-
public static T GetFirstDescendantOrThrow<T>(this DependencyObject reference, string name) where T : FrameworkElement => GetDescendants(reference)
150-
.OfType<T>()
151-
.FirstOrDefault(x => x.Name == name) ??
153+
public static T GetFirstDescendantOrThrow<T>(this DependencyObject reference, string name) where T : FrameworkElement =>
154+
GetFirstDescendant<T>(reference, name) ??
152155
throw new Exception($"Unable to find element: {typeof(T).Name}#{name}");
153156

154157
/// <summary>
@@ -195,6 +198,13 @@ public static IEnumerable<DependencyObject> GetChildren(this DependencyObject re
195198
.Select(x => VisualTreeHelper.GetChild(reference, x));
196199
}
197200

201+
public static DependencyObject? GetFirstChild(this DependencyObject reference)
202+
{
203+
return VisualTreeHelper.GetChildrenCount(reference) > 0
204+
? VisualTreeHelper.GetChild(reference, 0)
205+
: null;
206+
}
207+
198208
public static DependencyObject? GetTemplateRoot(this DependencyObject o) => o?.GetChildren().FirstOrDefault();
199209
}
200210
internal static partial class VisualTreeHelperEx // TreeGraph helper methods

0 commit comments

Comments
 (0)