Skip to content

Commit 8fa79d1

Browse files
iliglennawatsonChrisPulman
authored
Fix for MAUI RoutedViewHost (#3303)
* Fix for MAUI RoutedViewHost * Update RoutedViewHost.cs * Setting Page.Title prevents binding * Fix code style warnings * Fix cross thread issue with Title * Add missing using * Add SetTitleOnNavigate property Use ObserveOn(RxApp.MainThreadScheduler) and RxApp.MainThreadScheduler.Schedule to handle cross threading. * Add missing usings * Added missing using Co-authored-by: Glenn <[email protected]> Co-authored-by: Chris Pulman <[email protected]>
1 parent 196b8a6 commit 8fa79d1

File tree

1 file changed

+159
-107
lines changed

1 file changed

+159
-107
lines changed

src/ReactiveUI.Maui/RoutedViewHost.cs

Lines changed: 159 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
using System;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Reactive;
9+
using System.Reactive.Concurrency;
910
using System.Reactive.Disposables;
1011
using System.Reactive.Linq;
11-
using System.Reactive.Threading.Tasks;
1212
using System.Reflection;
1313
using Microsoft.Maui.Controls;
1414
using Splat;
@@ -32,6 +32,15 @@ public class RoutedViewHost : NavigationPage, IActivatableView, IEnableLogger
3232
typeof(RoutedViewHost),
3333
default(RoutingState));
3434

35+
/// <summary>
36+
/// The Set Title on Navigate property.
37+
/// </summary>
38+
public static readonly BindableProperty SetTitleOnNavigateProperty = BindableProperty.Create(
39+
nameof(SetTitleOnNavigate),
40+
typeof(bool),
41+
typeof(RoutedViewHost),
42+
false);
43+
3544
/// <summary>
3645
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
3746
/// </summary>
@@ -40,80 +49,31 @@ public RoutedViewHost()
4049
{
4150
this.WhenActivated(disposable =>
4251
{
43-
var currentlyPopping = false;
44-
var popToRootPending = false;
45-
var userInstigated = false;
46-
47-
this.WhenAnyObservable(x => x.Router.NavigationChanged)
48-
.Where(_ => Router.NavigationStack.Count == 0)
49-
.Select(x =>
50-
{
51-
// Xamarin Forms does not let us completely clear down the navigation stack
52-
// instead, we have to delay this request momentarily until we receive the new root view
53-
// then, we can insert the new root view first, and then pop to it
54-
popToRootPending = true;
55-
return x;
56-
})
57-
.Subscribe()
58-
.DisposeWith(disposable);
52+
var currentlyNavigating = false;
5953

6054
Router?
61-
.NavigationChanged?
62-
.CountChanged()
63-
.Select(_ => Router.NavigationStack.Count)
64-
.StartWith(Router.NavigationStack.Count)
65-
.Buffer(2, 1)
66-
.Select(counts => new
55+
.NavigateBack
56+
.Subscribe(async _ =>
6757
{
68-
Delta = counts[0] - counts[1],
69-
Current = counts[1],
58+
try
59+
{
60+
currentlyNavigating = true;
61+
await PopAsync();
62+
}
63+
finally
64+
{
65+
currentlyNavigating = false;
66+
}
7067

71-
// cache current viewmodel as it might change if some other Navigation command is executed midway
72-
CurrentViewModel = Router.GetCurrentViewModel()
68+
InvalidateCurrentViewModel();
69+
SyncNavigationStacks();
7370
})
74-
.Where(_ => !userInstigated)
75-
.Where(x => x.Delta > 0)
76-
.Select(
77-
async x =>
78-
{
79-
// XF doesn't provide a means of navigating back more than one screen at a time apart from navigating right back to the root page
80-
// since we want as sensible an animation as possible, we pop to root if that makes sense. Otherwise, we pop each individual
81-
// screen until the delta is made up, animating only the last one
82-
var popToRoot = x.Current == 1;
83-
currentlyPopping = true;
84-
85-
try
86-
{
87-
if (popToRoot)
88-
{
89-
await PopToRootAsync(true);
90-
}
91-
else if (!popToRootPending)
92-
{
93-
for (var i = 0; i < x.Delta; ++i)
94-
{
95-
await PopAsync(i == x.Delta - 1);
96-
}
97-
}
98-
}
99-
finally
100-
{
101-
currentlyPopping = false;
102-
if (CurrentPage is IViewFor page && x.CurrentViewModel is not null)
103-
{
104-
page.ViewModel = x.CurrentViewModel;
105-
}
106-
}
107-
108-
return Unit.Default;
109-
})
110-
.Concat()
111-
.Subscribe()
11271
.DisposeWith(disposable);
11372

11473
Router?
11574
.Navigate
116-
.SelectMany(_ => PageForViewModel(Router.GetCurrentViewModel()))
75+
.ObserveOn(RxApp.MainThreadScheduler)
76+
.SelectMany(_ => PagesForViewModel(Router.GetCurrentViewModel()))
11777
.SelectMany(async page =>
11878
{
11979
var animated = true;
@@ -123,17 +83,18 @@ public RoutedViewHost()
12383
animated = false;
12484
}
12585

126-
if (popToRootPending && Navigation.NavigationStack.Count > 0)
86+
try
12787
{
128-
Navigation.InsertPageBefore(page, Navigation.NavigationStack[0]);
129-
await PopToRootAsync(animated);
88+
currentlyNavigating = true;
89+
await PushAsync(page, animated);
13090
}
131-
else
91+
finally
13292
{
133-
await PushAsync(page, animated);
93+
currentlyNavigating = false;
13494
}
13595

136-
popToRootPending = false;
96+
SyncNavigationStacks();
97+
13798
return page;
13899
})
139100
.Subscribe()
@@ -151,26 +112,37 @@ public RoutedViewHost()
151112
// NB: Catch when the user hit back as opposed to the application
152113
// requesting Back via NavigateBack
153114
poppingEvent
154-
.Where(_ => !currentlyPopping && Router is not null)
115+
.Where(_ => !currentlyNavigating && Router is not null)
155116
.Subscribe(_ =>
156117
{
157-
userInstigated = true;
158118

159-
try
160-
{
161-
Router?.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1);
162-
}
163-
finally
164-
{
165-
userInstigated = false;
166-
}
119+
Router!.NavigationStack.RemoveAt(Router.NavigationStack.Count - 1);
120+
121+
InvalidateCurrentViewModel();
122+
})
123+
.DisposeWith(disposable);
124+
125+
var poppingToRootEvent = Observable.FromEvent<EventHandler<NavigationEventArgs>, Unit>(
126+
eventHandler =>
127+
{
128+
void Handler(object? sender, NavigationEventArgs e) => eventHandler(Unit.Default);
129+
return Handler;
130+
},
131+
x => PoppedToRoot += x,
132+
x => PoppedToRoot -= x);
167133

168-
var vm = Router?.GetCurrentViewModel();
169-
if (CurrentPage is IViewFor page && vm is not null)
134+
// NB: Catch when the user hit back as opposed to the application
135+
// requesting Back via NavigateBack
136+
poppingToRootEvent
137+
.Where(_ => !currentlyNavigating && Router is not null)
138+
.Subscribe(_ =>
139+
{
140+
for (var i = Router!.NavigationStack.Count - 1; i > 0; i--)
170141
{
171-
// don't replace view model if vm is null
172-
page.ViewModel = vm;
142+
Router.NavigationStack.RemoveAt(i);
173143
}
144+
145+
InvalidateCurrentViewModel();
174146
})
175147
.DisposeWith(disposable);
176148
});
@@ -182,24 +154,6 @@ public RoutedViewHost()
182154
}
183155

184156
Router = screen.Router;
185-
186-
this.WhenAnyValue(x => x.Router)
187-
.SelectMany(router => router!.NavigationStack
188-
.ToObservable()
189-
.Select(x => (Page)ViewLocator.Current.ResolveView(x)!)
190-
.SelectMany(x => PushAsync(x).ToObservable())
191-
.Finally(() =>
192-
{
193-
var vm = router.GetCurrentViewModel();
194-
if (vm is null)
195-
{
196-
return;
197-
}
198-
199-
((IViewFor)CurrentPage).ViewModel = vm;
200-
CurrentPage.Title = vm.UrlPathSegment;
201-
}))
202-
.Subscribe();
203157
}
204158

205159
/// <summary>
@@ -211,13 +165,22 @@ public RoutingState Router
211165
set => SetValue(RouterProperty, value);
212166
}
213167

168+
/// <summary>
169+
/// Gets or sets a value indicating whether gets or sets the Set Title of the view model stack.
170+
/// </summary>
171+
public bool SetTitleOnNavigate
172+
{
173+
get => (bool)GetValue(SetTitleOnNavigateProperty);
174+
set => SetValue(SetTitleOnNavigateProperty, value);
175+
}
176+
214177
/// <summary>
215178
/// Pages for view model.
216179
/// </summary>
217180
/// <param name="vm">The vm.</param>
218181
/// <returns>An observable of the page associated to a <see cref="IRoutableViewModel"/>.</returns>
219182
[SuppressMessage("Design", "CA1822: Can be made static", Justification = "Might be used by implementors.")]
220-
protected IObservable<Page> PageForViewModel(IRoutableViewModel? vm)
183+
protected virtual IObservable<Page> PagesForViewModel(IRoutableViewModel? vm)
221184
{
222185
if (vm is null)
223186
{
@@ -235,8 +198,97 @@ protected IObservable<Page> PageForViewModel(IRoutableViewModel? vm)
235198
ret.ViewModel = vm;
236199

237200
var pg = (Page)ret;
238-
pg.Title = vm.UrlPathSegment;
201+
if (SetTitleOnNavigate)
202+
{
203+
pg.Title = vm.UrlPathSegment;
204+
}
239205

240206
return Observable.Return(pg);
241207
}
242-
}
208+
209+
/// <summary>
210+
/// Page for view model.
211+
/// </summary>
212+
/// <param name="vm">The vm.</param>
213+
/// <returns>An observable of the page associated to a <see cref="IRoutableViewModel"/>.</returns>
214+
[SuppressMessage("Design", "CA1822: Can be made static", Justification = "Might be used by implementors.")]
215+
protected virtual Page PageForViewModel(IRoutableViewModel vm)
216+
{
217+
if (vm is null)
218+
{
219+
throw new ArgumentNullException(nameof(vm));
220+
}
221+
222+
var ret = ViewLocator.Current.ResolveView(vm);
223+
if (ret is null)
224+
{
225+
var msg = $"Couldn't find a View for ViewModel. You probably need to register an IViewFor<{vm.GetType().Name}>";
226+
227+
throw new Exception(msg);
228+
}
229+
230+
ret.ViewModel = vm;
231+
232+
var pg = (Page)ret;
233+
234+
if (SetTitleOnNavigate)
235+
{
236+
RxApp.MainThreadScheduler.Schedule(() => pg.Title = vm.UrlPathSegment);
237+
}
238+
239+
return pg;
240+
}
241+
242+
/// <summary>
243+
/// Invalidates current page view model.
244+
/// </summary>
245+
protected void InvalidateCurrentViewModel()
246+
{
247+
var vm = Router?.GetCurrentViewModel();
248+
if (CurrentPage is IViewFor page && vm is not null)
249+
{
250+
// don't replace view model if vm is null
251+
page.ViewModel = vm;
252+
}
253+
}
254+
255+
/// <summary>
256+
/// Syncs page's navigation stack with <see cref="Router"/>
257+
/// to affect <see cref="Router"/> manipulations like Add or Clear.
258+
/// </summary>
259+
protected void SyncNavigationStacks()
260+
{
261+
if (Navigation.NavigationStack.Count != Router.NavigationStack.Count
262+
|| StacksAreDifferent())
263+
{
264+
for (var i = Navigation.NavigationStack.Count - 2; i >= 0; i--)
265+
{
266+
Navigation.RemovePage(Navigation.NavigationStack[i]);
267+
}
268+
269+
var rootPage = Navigation.NavigationStack[0];
270+
271+
for (var i = 0; i < Router.NavigationStack.Count - 1; i++)
272+
{
273+
var page = PageForViewModel(Router.NavigationStack[i]);
274+
Navigation.InsertPageBefore(page, rootPage);
275+
}
276+
}
277+
}
278+
279+
private bool StacksAreDifferent()
280+
{
281+
for (var i = 0; i < Router.NavigationStack.Count; i++)
282+
{
283+
var vm = Router.NavigationStack[i];
284+
var page = Navigation.NavigationStack[i];
285+
286+
if (page is not IViewFor view || !ReferenceEquals(view.ViewModel, vm))
287+
{
288+
return true;
289+
}
290+
}
291+
292+
return false;
293+
}
294+
}

0 commit comments

Comments
 (0)