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+ await Task . WhenAny ( loading , pageLoaded ) . ConfigureAwait ( false ) ;
150+
151+ if ( pageLoaded . IsCompleted )
152+ {
153+ return pages [ pageNumber ] ;
154+ }
155+ }
156+
157+ return null ;
158+ }
159+
160+ Task WaitPageLoaded ( int page )
161+ {
162+ var tcs = new TaskCompletionSource < bool > ( ) ;
163+ EventHandler handler = null ;
164+ handler = ( s , e ) =>
165+ {
166+ if ( nextPage > page )
167+ {
168+ tcs . SetResult ( true ) ;
169+ PageLoaded -= handler ;
170+ }
171+ } ;
172+ PageLoaded += handler ;
173+ return tcs . Task ;
174+ }
175+
176+ async Task Load ( )
177+ {
178+ OnBeginLoading ( ) ;
179+
180+ while ( nextPage <= loadTo && ! disposed )
181+ {
182+ await LoadNextPage ( ) . ConfigureAwait ( false ) ;
183+ PageLoaded ? . Invoke ( this , EventArgs . Empty ) ;
184+ }
185+
186+ OnEndLoading ( ) ;
187+ }
188+
189+ async Task LoadNextPage ( )
190+ {
191+ log . Debug ( "Loading page {Number} of {ModelType}" , nextPage , typeof ( TModel ) ) ;
192+
193+ var page = await LoadPage ( after ) . ConfigureAwait ( false ) ;
194+ pages [ nextPage ++ ] = page ;
195+ after = page . EndCursor ;
196+ }
197+ }
198+ }
0 commit comments