1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Threading . Tasks ;
5+ using System . Windows ;
6+ using System . Windows . Threading ;
7+ using GitHub . Logging ;
8+ using GitHub . Models ;
9+ using ReactiveUI ;
10+ using Serilog ;
11+
12+ namespace GitHub . Collections
13+ {
14+ /// <summary>
15+ /// An <see cref="IVirtualizingListSource{T}"/> that loads GraphQL pages sequentially, and
16+ /// transforms items into a view model after reading.
17+ /// </summary>
18+ /// <typeparam name="TModel">The type of the model read from the remote data source.</typeparam>
19+ /// <typeparam name="TViewModel">The type of the transformed view model.</typeparam>
20+ /// <remarks>
21+ /// GraphQL can only read pages of data sequentally, so in order to read item 450 (assuming a
22+ /// page size of 100), the list source must read pages 1, 2, 3 and 4 in that order. Classes
23+ /// deriving from this class only need to implement <see cref="LoadPage(string)"/> to load a
24+ /// single page and this class will handle the rest.
25+ ///
26+ /// In addition, items will usually need to be transformed into a view model after reading. The
27+ /// implementing class overrides <see cref="CreateViewModel(TModel)"/> to carry out that
28+ /// transformation.
29+ /// </remarks>
30+ public abstract class SequentialListSource < TModel , TViewModel > : ReactiveObject , IVirtualizingListSource < TViewModel >
31+ {
32+ static readonly ILogger log = LogManager . ForContext < SequentialListSource < TModel , TViewModel > > ( ) ;
33+
34+ readonly Dispatcher dispatcher ;
35+ readonly object loadLock = new object ( ) ;
36+ Dictionary < int , Page < TModel > > pages = new Dictionary < int , Page < TModel > > ( ) ;
37+ Task loading = Task . CompletedTask ;
38+ bool disposed ;
39+ bool isLoading ;
40+ int ? count ;
41+ int nextPage ;
42+ int loadTo ;
43+ string after ;
44+
45+ /// <summary>
46+ /// Initializes a new instance of the <see cref="SequentialListSource{TModel, TViewModel}"/> class.
47+ /// </summary>
48+ public SequentialListSource ( )
49+ {
50+ dispatcher = Application . Current ? . Dispatcher ;
51+ }
52+
53+ /// <inheritdoc/>
54+ public bool IsLoading
55+ {
56+ get { return isLoading ; }
57+ private set { this . RaiseAndSetIfChanged ( ref isLoading , value ) ; }
58+ }
59+
60+ /// <inheritdoc/>
61+ public virtual int PageSize => 100 ;
62+
63+ event EventHandler PageLoaded ;
64+
65+ public void Dispose ( ) => disposed = true ;
66+
67+ /// <inheritdoc/>
68+ public async Task < int > GetCount ( )
69+ {
70+ dispatcher ? . VerifyAccess ( ) ;
71+
72+ if ( ! count . HasValue )
73+ {
74+ count = ( await EnsureLoaded ( 0 ) . ConfigureAwait ( false ) ) . TotalCount ;
75+ }
76+
77+ return count . Value ;
78+ }
79+
80+ /// <inheritdoc/>
81+ public async Task < IReadOnlyList < TViewModel > > GetPage ( int pageNumber )
82+ {
83+ dispatcher ? . VerifyAccess ( ) ;
84+
85+ var page = await EnsureLoaded ( pageNumber ) ;
86+
87+ if ( page == null )
88+ {
89+ return null ;
90+ }
91+
92+ var result = page . Items
93+ . Select ( CreateViewModel )
94+ . ToList ( ) ;
95+ pages . Remove ( pageNumber ) ;
96+ return result ;
97+ }
98+
99+ /// <summary>
100+ /// When overridden in a derived class, transforms a model into a view model after loading.
101+ /// </summary>
102+ /// <param name="model">The model.</param>
103+ /// <returns>The view model.</returns>
104+ protected abstract TViewModel CreateViewModel ( TModel model ) ;
105+
106+ /// <summary>
107+ /// When overridden in a derived class reads a page of results from GraphQL.
108+ /// </summary>
109+ /// <param name="after">The GraphQL after cursor.</param>
110+ /// <returns>A task which returns the page of results.</returns>
111+ protected abstract Task < Page < TModel > > LoadPage ( string after ) ;
112+
113+ /// <summary>
114+ /// Called when the source begins loading pages.
115+ /// </summary>
116+ protected virtual void OnBeginLoading ( )
117+ {
118+ IsLoading = true ;
119+ }
120+
121+ /// <summary>
122+ /// Called when the source finishes loading pages.
123+ /// </summary>
124+ protected virtual void OnEndLoading ( )
125+ {
126+ IsLoading = false ;
127+ }
128+
129+ async Task < Page < TModel > > EnsureLoaded ( int pageNumber )
130+ {
131+ if ( pageNumber < nextPage )
132+ {
133+ return pages [ pageNumber ] ;
134+ }
135+
136+ var pageLoaded = WaitPageLoaded ( pageNumber ) ;
137+ loadTo = Math . Max ( loadTo , pageNumber ) ;
138+
139+ while ( ! disposed )
140+ {
141+ lock ( loadLock )
142+ {
143+ if ( loading . IsCompleted )
144+ {
145+ loading = Load ( ) ;
146+ }
147+ }
148+
149+ var completed = await Task . WhenAny ( loading , pageLoaded ) . ConfigureAwait ( false ) ;
150+
151+ if ( completed . IsFaulted )
152+ {
153+ throw completed . Exception ;
154+ }
155+
156+ if ( pageLoaded . IsCompleted )
157+ {
158+ // A previous waiting task may have already returned the page. If so, return null.
159+ pages . TryGetValue ( pageNumber , out var result ) ;
160+ return result ;
161+ }
162+ }
163+
164+ return null ;
165+ }
166+
167+ Task WaitPageLoaded ( int page )
168+ {
169+ var tcs = new TaskCompletionSource < bool > ( ) ;
170+ EventHandler handler = null ;
171+ handler = ( s , e ) =>
172+ {
173+ if ( nextPage > page )
174+ {
175+ tcs . SetResult ( true ) ;
176+ PageLoaded -= handler ;
177+ }
178+ } ;
179+ PageLoaded += handler ;
180+ return tcs . Task ;
181+ }
182+
183+ async Task Load ( )
184+ {
185+ OnBeginLoading ( ) ;
186+
187+ try
188+ {
189+ while ( nextPage <= loadTo && ! disposed )
190+ {
191+ await LoadNextPage ( ) . ConfigureAwait ( false ) ;
192+ PageLoaded ? . Invoke ( this , EventArgs . Empty ) ;
193+ }
194+ }
195+ finally
196+ {
197+ OnEndLoading ( ) ;
198+ }
199+ }
200+
201+ async Task LoadNextPage ( )
202+ {
203+ log . Debug ( "Loading page {Number} of {ModelType}" , nextPage , typeof ( TModel ) ) ;
204+
205+ var page = await LoadPage ( after ) . ConfigureAwait ( false ) ;
206+ pages [ nextPage ++ ] = page ;
207+ after = page . EndCursor ;
208+ }
209+ }
210+ }
0 commit comments