Skip to content

Commit 1685cbc

Browse files
committed
Enhance Prism.Maui navigation and test robustness
Improves `TabbedPage` selected tab resolution by matching registered view names alongside CLR type names, addressing scenarios where they diverge. Refactors `PageNavigationService` unit tests for increased reliability: - Converts `async void` tests to `async Task`. - Explicitly asserts `INavigationResult` for navigation outcomes. - Introduces a shared `PageNavigationEventRecorder` for consistent tracking of page lifecycle events across DI-resolved instances. - Adds helper methods for navigating complex visual trees (e.g., finding tabbed or flyout pages). Updates `EventToCommandBehavior` tests to use `CollectionView` and a more flexible mock for event raising, ensuring accurate behavior simulation.
1 parent be532fd commit 1685cbc

10 files changed

Lines changed: 554 additions & 514 deletions

File tree

src/Maui/Prism.Maui/Navigation/PageNavigationService.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,23 @@ static TabbedPage GetTabbedPage(Element page) =>
375375
var parts = tabName.Split('|');
376376
Page selectedChild = null;
377377
if (parts.Length == 1)
378-
selectedChild = tabbedPage.Children.FirstOrDefault(x => ViewModelLocator.GetNavigationName(x) == tabName || (x is NavigationPage navPage && ViewModelLocator.GetNavigationName(navPage.RootPage) == tabName));
378+
{
379+
var tabRegistration = Registry.Registrations.FirstOrDefault(x => x.Name == tabName);
380+
selectedChild = tabbedPage.Children.FirstOrDefault(x =>
381+
ViewModelLocator.GetNavigationName(x) == tabName
382+
|| (x is NavigationPage navPage && ViewModelLocator.GetNavigationName(navPage.RootPage) == tabName)
383+
|| (tabRegistration is not null && x is NavigationPage np && IsPage(np.RootPage, tabRegistration, tabName))
384+
|| (tabRegistration is not null && IsPage(x, tabRegistration, tabName)));
385+
}
379386
else if (parts.Length == 2)
380-
selectedChild = tabbedPage.Children.FirstOrDefault(x => x is NavigationPage navPage && ViewModelLocator.GetNavigationName(navPage) == parts[0] && ViewModelLocator.GetNavigationName(navPage.RootPage) == parts[1]);
387+
{
388+
var rootRegistration = Registry.Registrations.FirstOrDefault(x => x.Name == parts[0]);
389+
var leafRegistration = Registry.Registrations.FirstOrDefault(x => x.Name == parts[1]);
390+
selectedChild = tabbedPage.Children.FirstOrDefault(x =>
391+
x is NavigationPage navPage
392+
&& (ViewModelLocator.GetNavigationName(navPage) == parts[0] || (rootRegistration is not null && IsPage(navPage, rootRegistration, parts[0])))
393+
&& (ViewModelLocator.GetNavigationName(navPage.RootPage) == parts[1] || (leafRegistration is not null && IsPage(navPage.RootPage, leafRegistration, parts[1]))));
394+
}
381395
else
382396
throw new NavigationException($"Invalid Tab Name: {tabName}");
383397

@@ -1070,7 +1084,8 @@ private static bool IsPage(Page referencePage, ViewRegistration registration, st
10701084
{
10711085
// We're allowing an empty string here for cases where someone has a manually constructed TabbedPage
10721086
var navigationName = ViewModelLocator.GetNavigationName(referencePage);
1073-
if (registration.View == referenceType && (string.IsNullOrEmpty(navigationName) || navigationName == name))
1087+
// registration.Name matches the navigation key (e.g. "Tab2") even when NavigationName still defaults to CLR type name ("Tab2Mock")
1088+
if (registration.View == referenceType && (string.IsNullOrEmpty(navigationName) || navigationName == name || registration.Name == name))
10741089
return true;
10751090

10761091
// This is an override for cases where someone may have a NavigationPage
Lines changed: 85 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Globalization;
4+
using System.Linq;
5+
using System.Reflection;
36
using Microsoft.Maui.Controls;
47
using Microsoft.Maui.Controls.Hosting;
58
using Microsoft.Maui.Dispatching;
@@ -12,21 +15,43 @@ namespace Prism.Maui.Tests.Fixtures.Behaviors;
1215

1316
public class EventToCommandBehaviorFixture
1417
{
15-
private class ItemTappedEventArgsConverter : IValueConverter
18+
/// <summary>Mirrors <see cref="SelectionChangedEventArgs"/> shape for tests (MAUI ctor is not public).</summary>
19+
private sealed class FakeSelectionChangedEventArgs : EventArgs
20+
{
21+
public FakeSelectionChangedEventArgs(IReadOnlyList<object> previousSelection, IReadOnlyList<object> currentSelection)
22+
{
23+
PreviousSelection = previousSelection;
24+
CurrentSelection = currentSelection;
25+
}
26+
27+
public IReadOnlyList<object> PreviousSelection { get; }
28+
public IReadOnlyList<object> CurrentSelection { get; }
29+
}
30+
31+
/// <summary>Minimal EventArgs for nested property-path tests (no Item on selection args).</summary>
32+
private sealed class TestNestedArgs : EventArgs
33+
{
34+
public object Item => null;
35+
}
36+
37+
private class SelectionChangedEventArgsConverter : IValueConverter
1638
{
1739
private readonly bool _returnParameter;
1840

1941
public bool HasConverted { get; private set; }
2042

21-
public ItemTappedEventArgsConverter(bool returnParameter)
43+
public SelectionChangedEventArgsConverter(bool returnParameter)
2244
{
2345
_returnParameter = returnParameter;
2446
}
2547

2648
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
2749
{
2850
HasConverted = true;
29-
return _returnParameter ? parameter : (value as ItemTappedEventArgs)?.Item;
51+
if (_returnParameter)
52+
return parameter;
53+
var prop = value?.GetType().GetRuntimeProperty(nameof(FakeSelectionChangedEventArgs.CurrentSelection));
54+
return prop?.GetValue(value) is IReadOnlyList<object> list ? list.FirstOrDefault() : null;
3055
}
3156

3257
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
@@ -48,10 +73,10 @@ public void Command_OrderOfExecution()
4873
{
4974
const string commandParameter = "ItemProperty";
5075
var executedCommand = false;
51-
var converter = new ItemTappedEventArgsConverter(false);
76+
var converter = new SelectionChangedEventArgsConverter(false);
5277
var behavior = new EventToCommandBehaviorMock
5378
{
54-
EventName = "ItemTapped",
79+
EventName = "SelectionChanged",
5580
EventArgsConverter = converter,
5681
CommandParameter = commandParameter,
5782
Command = new DelegateCommand<string>(o =>
@@ -62,9 +87,9 @@ public void Command_OrderOfExecution()
6287
Assert.False(converter.HasConverted);
6388
})
6489
};
65-
var listView = new ListView();
66-
listView.Behaviors.Add(behavior);
67-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, commandParameter, 0));
90+
var collectionView = new CollectionView();
91+
collectionView.Behaviors.Add(behavior);
92+
behavior.RaiseEvent(collectionView, new FakeSelectionChangedEventArgs(Array.Empty<object>(), new[] { commandParameter }));
6893
Assert.True(executedCommand);
6994
}
7095

@@ -75,18 +100,18 @@ public void Command_Converter()
75100
var executedCommand = false;
76101
var behavior = new EventToCommandBehaviorMock
77102
{
78-
EventName = "ItemTapped",
79-
EventArgsConverter = new ItemTappedEventArgsConverter(false),
103+
EventName = "SelectionChanged",
104+
EventArgsConverter = new SelectionChangedEventArgsConverter(false),
80105
Command = new DelegateCommand<string>(o =>
81106
{
82107
executedCommand = true;
83108
Assert.NotNull(o);
84109
Assert.Equal(item, o);
85110
})
86111
};
87-
var listView = new ListView();
88-
listView.Behaviors.Add(behavior);
89-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, item, 0));
112+
var collectionView = new CollectionView();
113+
collectionView.Behaviors.Add(behavior);
114+
behavior.RaiseEvent(collectionView, new FakeSelectionChangedEventArgs(Array.Empty<object>(), new[] { item }));
90115
Assert.True(executedCommand);
91116
}
92117

@@ -97,8 +122,8 @@ public void Command_ConverterWithConverterParameter()
97122
var executedCommand = false;
98123
var behavior = new EventToCommandBehaviorMock
99124
{
100-
EventName = "ItemTapped",
101-
EventArgsConverter = new ItemTappedEventArgsConverter(true),
125+
EventName = "SelectionChanged",
126+
EventArgsConverter = new SelectionChangedEventArgsConverter(true),
102127
EventArgsConverterParameter = item,
103128
Command = new DelegateCommand<string>(o =>
104129
{
@@ -107,9 +132,9 @@ public void Command_ConverterWithConverterParameter()
107132
Assert.Equal(item, o);
108133
})
109134
};
110-
var listView = new ListView();
111-
listView.Behaviors.Add(behavior);
112-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, null, 0));
135+
var collectionView = new CollectionView();
136+
collectionView.Behaviors.Add(behavior);
137+
behavior.RaiseEvent(collectionView, new FakeSelectionChangedEventArgs(Array.Empty<object>(), Array.Empty<object>()));
113138
Assert.True(executedCommand);
114139
}
115140

@@ -120,7 +145,7 @@ public void Command_ExecuteWithParameter()
120145
var executedCommand = false;
121146
var behavior = new EventToCommandBehaviorMock
122147
{
123-
EventName = "ItemTapped",
148+
EventName = "SelectionChanged",
124149
CommandParameter = item,
125150
Command = new DelegateCommand<string>(o =>
126151
{
@@ -129,9 +154,9 @@ public void Command_ExecuteWithParameter()
129154
Assert.Equal(item, o);
130155
})
131156
};
132-
var listView = new ListView();
133-
listView.Behaviors.Add(behavior);
134-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, null, 0));
157+
var collectionView = new CollectionView();
158+
collectionView.Behaviors.Add(behavior);
159+
behavior.RaiseEvent(collectionView, new FakeSelectionChangedEventArgs(Array.Empty<object>(), Array.Empty<object>()));
135160
Assert.True(executedCommand);
136161
}
137162

@@ -142,18 +167,19 @@ public void Command_EventArgsParameterPath()
142167
var executedCommand = false;
143168
var behavior = new EventToCommandBehaviorMock
144169
{
145-
EventName = "ItemTapped",
146-
EventArgsParameterPath = "Item",
147-
Command = new DelegateCommand<string>(o =>
170+
EventName = "SelectionChanged",
171+
EventArgsParameterPath = "CurrentSelection",
172+
Command = new DelegateCommand<IReadOnlyList<object>>(o =>
148173
{
149174
executedCommand = true;
150175
Assert.NotNull(o);
151-
Assert.Equal(item, o);
176+
Assert.Single(o);
177+
Assert.Equal(item, o[0]);
152178
})
153179
};
154-
var listView = new ListView();
155-
listView.Behaviors.Add(behavior);
156-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, item, 0));
180+
var collectionView = new CollectionView();
181+
collectionView.Behaviors.Add(behavior);
182+
behavior.RaiseEvent(collectionView, new FakeSelectionChangedEventArgs(Array.Empty<object>(), new[] { item }));
157183
Assert.True(executedCommand);
158184
}
159185

@@ -167,7 +193,7 @@ public void Command_EventArgsParameterPath()
167193
// var executedCommand = false;
168194
// var behavior = new EventToCommandBehaviorMock
169195
// {
170-
// EventName = "ItemTapped",
196+
// EventName = "SelectionChanged",
171197
// EventArgsParameterPath = "Item.AProperty",
172198
// Command = new DelegateCommand<object>(o =>
173199
// {
@@ -176,9 +202,9 @@ public void Command_EventArgsParameterPath()
176202
// Assert.Equal("Value", o);
177203
// })
178204
// };
179-
// var listView = new ListView();
180-
// listView.Behaviors.Add(behavior);
181-
// behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, item));
205+
// var collectionView = new CollectionView();
206+
// collectionView.Behaviors.Add(behavior);
207+
// behavior.RaiseEvent(collectionView, new TestNestedArgs());
182208
// Assert.True(executedCommand);
183209
//}
184210

@@ -189,17 +215,17 @@ public void Command_EventArgsParameterPath_Nested_When_ChildIsNull()
189215
var executedCommand = false;
190216
var behavior = new EventToCommandBehaviorMock
191217
{
192-
EventName = "ItemTapped",
218+
EventName = "SelectionChanged",
193219
EventArgsParameterPath = "Item.AProperty",
194220
Command = new DelegateCommand<object>(o =>
195221
{
196222
executedCommand = true;
197223
Assert.Null(o);
198224
})
199225
};
200-
var listView = new ListView();
201-
listView.Behaviors.Add(behavior);
202-
behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, null, 0));
226+
var collectionView = new CollectionView();
227+
collectionView.Behaviors.Add(behavior);
228+
behavior.RaiseEvent(collectionView, new TestNestedArgs());
203229
Assert.True(executedCommand);
204230
}
205231

@@ -208,12 +234,12 @@ public void Command_CanExecute()
208234
{
209235
var behavior = new EventToCommandBehaviorMock
210236
{
211-
EventName = "ItemTapped",
237+
EventName = "SelectionChanged",
212238
Command = new DelegateCommand(() => Assert.True(false), () => false)
213239
};
214-
var listView = new ListView();
215-
listView.Behaviors.Add(behavior);
216-
behavior.RaiseEvent(listView, null);
240+
var collectionView = new CollectionView();
241+
collectionView.Behaviors.Add(behavior);
242+
behavior.RaiseEvent(collectionView, null);
217243
}
218244

219245
[Fact]
@@ -223,17 +249,17 @@ public void Command_CanExecuteWithParameterShouldExecute()
223249
var executedCommand = false;
224250
var behavior = new EventToCommandBehaviorMock
225251
{
226-
EventName = "ItemTapped",
252+
EventName = "SelectionChanged",
227253
CommandParameter = shouldExeute,
228254
Command = new DelegateCommand<string>(o =>
229255
{
230256
executedCommand = true;
231257
Assert.True(true);
232258
}, o => o.Equals(bool.TrueString))
233259
};
234-
var listView = new ListView();
235-
listView.Behaviors.Add(behavior);
236-
behavior.RaiseEvent(listView, null);
260+
var collectionView = new CollectionView();
261+
collectionView.Behaviors.Add(behavior);
262+
behavior.RaiseEvent(collectionView, null);
237263
Assert.True(executedCommand);
238264
}
239265

@@ -243,13 +269,13 @@ public void Command_CanExecuteWithParameterShouldNotExeute()
243269
var shouldExeute = bool.FalseString;
244270
var behavior = new EventToCommandBehaviorMock
245271
{
246-
EventName = "ItemTapped",
272+
EventName = "SelectionChanged",
247273
CommandParameter = shouldExeute,
248274
Command = new DelegateCommand<string>(o => Assert.True(false), o => o.Equals(bool.TrueString))
249275
};
250-
var listView = new ListView();
251-
listView.Behaviors.Add(behavior);
252-
behavior.RaiseEvent(listView, null);
276+
var collectionView = new CollectionView();
277+
collectionView.Behaviors.Add(behavior);
278+
behavior.RaiseEvent(collectionView, null);
253279
}
254280

255281
[Fact]
@@ -258,16 +284,16 @@ public void Command_Execute()
258284
var executedCommand = false;
259285
var behavior = new EventToCommandBehaviorMock
260286
{
261-
EventName = "ItemTapped",
287+
EventName = "SelectionChanged",
262288
Command = new DelegateCommand(() =>
263289
{
264290
executedCommand = true;
265291
Assert.True(true);
266292
})
267293
};
268-
var listView = new ListView();
269-
listView.Behaviors.Add(behavior);
270-
behavior.RaiseEvent(listView, null);
294+
var collectionView = new CollectionView();
295+
collectionView.Behaviors.Add(behavior);
296+
behavior.RaiseEvent(collectionView, null);
271297
Assert.True(executedCommand);
272298
}
273299

@@ -276,20 +302,20 @@ public void EventName_InvalidEventShouldThrow()
276302
{
277303
var behavior = new EventToCommandBehavior
278304
{
279-
EventName = "OnItemTapped"
305+
EventName = "OnSelectionChanged"
280306
};
281-
var listView = new ListView();
282-
Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
307+
var collectionView = new CollectionView();
308+
Assert.Throws<ArgumentException>(() => collectionView.Behaviors.Add(behavior));
283309
}
284310

285311
[Fact]
286312
public void EventName_Valid()
287313
{
288314
var behavior = new EventToCommandBehavior
289315
{
290-
EventName = "ItemTapped"
316+
EventName = "SelectionChanged"
291317
};
292-
var listView = new ListView();
293-
listView.Behaviors.Add(behavior);
318+
var collectionView = new CollectionView();
319+
collectionView.Behaviors.Add(behavior);
294320
}
295321
}

0 commit comments

Comments
 (0)