66using System ;
77using System . Diagnostics . CodeAnalysis ;
88using System . Reactive ;
9+ using System . Reactive . Concurrency ;
910using System . Reactive . Disposables ;
1011using System . Reactive . Linq ;
11- using System . Reactive . Threading . Tasks ;
1212using System . Reflection ;
1313using Microsoft . Maui . Controls ;
1414using 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