Skip to content

Commit 92cb037

Browse files
authored
Locate MAUI View for PlatformView (#16463)
* Locate xplat view from platformview * - dispatcher * - fix layout comparison on xunit * - PR review comments * - tizen fix
1 parent d257304 commit 92cb037

File tree

20 files changed

+754
-130
lines changed

20 files changed

+754
-130
lines changed

src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,30 @@ IWindow CreateWindowForContent(IElement view)
9797
return window;
9898
}
9999

100+
protected Task CreateHandlerAndAddToWindow(IElement view, Func<Task> action)
101+
{
102+
return CreateHandlerAndAddToWindow<IWindowHandler>(CreateWindowForContent(view), handler =>
103+
{
104+
return action();
105+
}, MauiContext, null);
106+
}
107+
108+
protected Task CreateHandlerAndAddToWindow<THandler>(IElement view, Func<THandler, Task> action)
109+
where THandler : class, IElementHandler
110+
{
111+
return CreateHandlerAndAddToWindow<THandler>(view, handler =>
112+
{
113+
return action(handler);
114+
}, MauiContext, null);
115+
}
116+
100117
protected Task CreateHandlerAndAddToWindow(IElement view, Action action)
101118
{
102119
return CreateHandlerAndAddToWindow<IWindowHandler>(CreateWindowForContent(view), handler =>
103120
{
104121
action();
105122
return Task.CompletedTask;
106-
});
123+
}, MauiContext, null);
107124
}
108125

109126
protected Task CreateHandlerAndAddToWindow<THandler>(IElement view, Action<THandler> action)
@@ -113,7 +130,7 @@ protected Task CreateHandlerAndAddToWindow<THandler>(IElement view, Action<THand
113130
{
114131
action(handler);
115132
return Task.CompletedTask;
116-
});
133+
}, MauiContext, null);
117134
}
118135

119136
static SemaphoreSlim _takeOverMainContentSempahore = new SemaphoreSlim(1);
@@ -175,6 +192,8 @@ await SetupWindowForTests<THandler>(window, async () =>
175192

176193
await OnLoadedAsync(content as VisualElement);
177194

195+
// Gives time for the measure/layout pass to settle
196+
await Task.Yield();
178197
if (view is VisualElement veBeingTested)
179198
await OnLoadedAsync(veBeingTested);
180199

src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(tabbedPage), asy
113113

114114
BottomNavigationView GetBottomNavigationView(TabbedViewHandler tabViewHandler)
115115
{
116-
var layout = (tabViewHandler.PlatformView as Android.Views.IViewParent).FindParent((view) => view is CoordinatorLayout)
116+
var layout = tabViewHandler.PlatformView.FindParent((view) => view is CoordinatorLayout)
117117
as CoordinatorLayout;
118118

119119
return layout.GetFirstChildOfType<BottomNavigationView>();
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Microsoft.Maui.Controls;
8+
using Xunit.Abstractions;
9+
10+
namespace Microsoft.Maui.DeviceTests
11+
{
12+
public class FindVisualTreeElementInsideTestCase : IXunitSerializable
13+
{
14+
15+
public string TestCaseName { get; private set; }
16+
17+
public FindVisualTreeElementInsideTestCase() { }
18+
19+
public FindVisualTreeElementInsideTestCase(string testCaseName)
20+
{
21+
TestCaseName = testCaseName;
22+
}
23+
24+
public void Deserialize(IXunitSerializationInfo info)
25+
{
26+
TestCaseName = info.GetValue<string>(nameof(TestCaseName));
27+
}
28+
29+
public void Serialize(IXunitSerializationInfo info)
30+
{
31+
info.AddValue(nameof(TestCaseName), TestCaseName, typeof(string));
32+
}
33+
34+
public override string ToString()
35+
{
36+
return TestCaseName;
37+
}
38+
39+
public (VisualElement rootView, VisualElement testView) CreateVisualElement()
40+
{
41+
switch (TestCaseName)
42+
{
43+
case "CollectionView":
44+
{
45+
var cv = new CollectionView();
46+
NestingView view = new NestingView();
47+
cv.ItemTemplate = new DataTemplate(() =>
48+
{
49+
return view;
50+
});
51+
cv.ItemsSource = new[] { 0 };
52+
return (cv, view);
53+
}
54+
case "ContentView":
55+
{
56+
var contentView = new ContentView();
57+
NestingView view = new NestingView();
58+
contentView.ControlTemplate = new ControlTemplate(() =>
59+
{
60+
return view;
61+
});
62+
return (contentView, view);
63+
}
64+
}
65+
66+
throw new Exception(String.Concat(TestCaseName, " ", "Not Found"));
67+
}
68+
}
69+
70+
public class FindVisualTreeElementInsideTestCases : IEnumerable<object[]>
71+
{
72+
private readonly List<object[]> _data = new()
73+
{
74+
new object[] { new FindVisualTreeElementInsideTestCase("CollectionView") },
75+
new object[] { new FindVisualTreeElementInsideTestCase("ContentView") }
76+
};
77+
78+
public IEnumerator<object[]> GetEnumerator()
79+
=> _data.GetEnumerator();
80+
81+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
82+
}
83+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using Microsoft.Maui.Controls;
4+
using Microsoft.Maui.Controls.Handlers;
5+
using Microsoft.Maui.Handlers;
6+
using Microsoft.Maui.Hosting;
7+
using Microsoft.Maui.Platform;
8+
using Xunit;
9+
using System.Collections.Generic;
10+
using ContentView = Microsoft.Maui.Controls.ContentView;
11+
using Microsoft.Maui.Controls.Handlers.Items;
12+
#if ANDROID || IOS || MACCATALYST
13+
using ShellHandler = Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer;
14+
#endif
15+
16+
namespace Microsoft.Maui.DeviceTests
17+
{
18+
[Category(TestCategory.VisualElementTree)]
19+
#if ANDROID || IOS || MACCATALYST
20+
[Collection(ControlsHandlerTestBase.RunInNewWindowCollection)]
21+
#endif
22+
public partial class VisualElementTreeTests : ControlsHandlerTestBase
23+
{
24+
void SetupBuilder()
25+
{
26+
EnsureHandlerCreated(builder =>
27+
{
28+
builder.SetupShellHandlers();
29+
30+
builder.ConfigureMauiHandlers(handlers =>
31+
{
32+
#if IOS || MACCATALYST
33+
handlers.AddHandler(typeof(Controls.NavigationPage), typeof(Controls.Handlers.Compatibility.NavigationRenderer));
34+
#else
35+
handlers.AddHandler(typeof(Controls.NavigationPage), typeof(NavigationViewHandler));
36+
#endif
37+
handlers.AddHandler<NestingView, NestingViewHandler>();
38+
handlers.AddHandler<ContentView, ContentViewHandler>();
39+
handlers.AddHandler<CollectionView, CollectionViewHandler>();
40+
});
41+
});
42+
}
43+
44+
[Fact]
45+
public async Task GetVisualTreeElements()
46+
{
47+
SetupBuilder();
48+
var label = new Label() { Text = "Find Me" };
49+
var page = new ContentPage() { Title = "Title Page" };
50+
page.Content = new VerticalStackLayout()
51+
{
52+
label
53+
};
54+
55+
var rootPage = await InvokeOnMainThreadAsync(() =>
56+
new NavigationPage(page)
57+
);
58+
59+
await CreateHandlerAndAddToWindow<IWindowHandler>(rootPage, async handler =>
60+
{
61+
await OnFrameSetToNotEmpty(label);
62+
var locationOnScreen = label.GetLocationOnScreen().Value;
63+
var labelFrame = label.Frame;
64+
var window = rootPage.Window;
65+
66+
// Find label at the top left corner
67+
var topLeft = new Graphics.Point(locationOnScreen.X + 1, locationOnScreen.Y + 1);
68+
69+
Assert.True(window.GetVisualTreeElements(topLeft).Contains(label), $"Unable to find label using top left coordinate: {topLeft} with label location: {label.GetBoundingBox()}");
70+
71+
// find label at the bottom right corner
72+
var bottomRight = new Graphics.Point(
73+
locationOnScreen.X + labelFrame.Width - 1,
74+
locationOnScreen.Y + labelFrame.Height - 1);
75+
76+
Assert.True(window.GetVisualTreeElements(bottomRight).Contains(label), $"Unable to find label using bottom right coordinate: {bottomRight} with label location: {label.GetBoundingBox()}");
77+
78+
// Ensure that the point directly outside the bounds of the label doesn't
79+
// return the label
80+
Assert.DoesNotContain(label, window.GetVisualTreeElements(
81+
locationOnScreen.X + labelFrame.Width + 1,
82+
locationOnScreen.Y + labelFrame.Height + 1
83+
));
84+
85+
});
86+
}
87+
88+
[Fact]
89+
public async Task FindPlatformViewInsideLayout()
90+
{
91+
SetupBuilder();
92+
var button = new Button();
93+
VerticalStackLayout views = new VerticalStackLayout()
94+
{
95+
new VerticalStackLayout()
96+
{
97+
button
98+
}
99+
};
100+
101+
await CreateHandlerAndAddToWindow(views, () =>
102+
{
103+
var platformView = button.ToPlatform();
104+
var foundTreeElement = button.ToPlatform().GetVisualTreeElement();
105+
106+
Assert.Equal(button, foundTreeElement);
107+
});
108+
}
109+
110+
[Fact]
111+
public async Task FindPlatformViewInsideScrollView()
112+
{
113+
SetupBuilder();
114+
var button = new Button();
115+
ScrollView view = new ScrollView()
116+
{
117+
Content = button
118+
};
119+
120+
await CreateHandlerAndAddToWindow(view, () =>
121+
{
122+
var platformView = button.ToPlatform();
123+
var foundTreeElement = button.ToPlatform().GetVisualTreeElement();
124+
125+
Assert.Equal(button, foundTreeElement);
126+
});
127+
}
128+
129+
[Fact]
130+
public async Task FindPlatformViewViaDefaultContainer()
131+
{
132+
SetupBuilder();
133+
var button = new Button();
134+
NestingView view = new NestingView();
135+
view.AddLogicalChild(button);
136+
137+
await CreateHandlerAndAddToWindow(view, () =>
138+
{
139+
var platformView = button.ToPlatform();
140+
var foundTreeElement = button.ToPlatform().GetVisualTreeElement();
141+
142+
Assert.Equal(button, foundTreeElement);
143+
});
144+
}
145+
146+
[Fact]
147+
public async Task FindVisualTreeElementWithArbitraryPlatformViewsAdded()
148+
{
149+
SetupBuilder();
150+
var button = new Button();
151+
NestingView view = new NestingView();
152+
153+
await CreateHandlerAndAddToWindow<NestingViewHandler>(view, (handler) =>
154+
{
155+
handler
156+
.PlatformView
157+
.AddChild()
158+
.AddChild()
159+
.AddChild()
160+
.AddChild(button, view);
161+
162+
var platformView = button.ToPlatform();
163+
var foundTreeElement = button.ToPlatform().GetVisualTreeElement();
164+
165+
Assert.Equal(button, foundTreeElement);
166+
});
167+
}
168+
169+
[Theory]
170+
[InlineData(false)]
171+
[InlineData(true)]
172+
public async Task FindFirstMauiParentElement(bool searchAncestors)
173+
{
174+
SetupBuilder();
175+
var viewToLocate = new NestingView();
176+
NestingView view = new NestingView();
177+
178+
await CreateHandlerAndAddToWindow<NestingViewHandler>(view, (handler) =>
179+
{
180+
var nestedChild =
181+
handler.PlatformView
182+
.AddChild<NestingViewPlatformView>(viewToLocate, view)
183+
.AddChild()
184+
.AddChild()
185+
.AddChild();
186+
187+
var foundTreeElement = nestedChild.GetVisualTreeElement(searchAncestors);
188+
189+
if (searchAncestors)
190+
Assert.Equal(viewToLocate, foundTreeElement);
191+
else
192+
Assert.Null(foundTreeElement);
193+
});
194+
}
195+
196+
[Theory]
197+
[ClassData(typeof(FindVisualTreeElementInsideTestCases))]
198+
public async Task FindPlatformViewInsideView(FindVisualTreeElementInsideTestCase testCase)
199+
{
200+
SetupBuilder();
201+
202+
VisualElement rootView;
203+
VisualElement viewToLocate;
204+
205+
(rootView, viewToLocate) = testCase.CreateVisualElement();
206+
await CreateHandlerAndAddToWindow(rootView, () =>
207+
{
208+
var platformView = viewToLocate.ToPlatform();
209+
var foundTreeElement = platformView.GetVisualTreeElement();
210+
Assert.Equal(viewToLocate, foundTreeElement);
211+
});
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)