Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 1d801fb

Browse files
authored
Merge pull request #1737 from github/refactor/pr-list
Refactoring PR list
2 parents 3b49cdb + 7ce21a3 commit 1d801fb

File tree

58 files changed

+2807
-1050
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2807
-1050
lines changed
176 KB
Binary file not shown.

src/GitHub.Api/GitHub.Api.csproj

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@
5151
<HintPath>..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
5252
<Private>True</Private>
5353
</Reference>
54-
<Reference Include="Octokit.GraphQL, Version=0.0.4.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
55-
<HintPath>..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
56-
<Private>True</Private>
54+
<Reference Include="Octokit.GraphQL, Version=0.0.5.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
55+
<HintPath>..\..\packages\Octokit.GraphQL.0.0.5-alpha\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
5756
</Reference>
58-
<Reference Include="Octokit.GraphQL.Core, Version=0.0.4.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
59-
<HintPath>..\..\packages\Octokit.GraphQL.0.0.4-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
60-
<Private>True</Private>
57+
<Reference Include="Octokit.GraphQL.Core, Version=0.0.5.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
58+
<HintPath>..\..\packages\Octokit.GraphQL.0.0.5-alpha\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
6159
</Reference>
6260
<Reference Include="Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
6361
<HintPath>..\..\packages\Serilog.2.5.0\lib\net46\Serilog.dll</HintPath>

src/GitHub.Api/packages.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<packages>
33
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
4-
<package id="Octokit.GraphQL" version="0.0.4-alpha" targetFramework="net461" />
4+
<package id="Octokit.GraphQL" version="0.0.5-alpha" targetFramework="net461" />
55
<package id="Serilog" version="2.5.0" targetFramework="net461" />
66
</packages>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Threading.Tasks;
5+
6+
namespace GitHub.Collections
7+
{
8+
/// <summary>
9+
/// A loader for a virtualizing list.
10+
/// </summary>
11+
/// <typeparam name="T">The item type.</typeparam>
12+
/// <remarks>
13+
/// This interface is used by the <see cref="VirtualizingList{T}"/> class to load pages of data.
14+
/// </remarks>
15+
public interface IVirtualizingListSource<T> : IDisposable, INotifyPropertyChanged
16+
{
17+
/// <summary>
18+
/// Gets a value that indicates where loading is in progress.
19+
/// </summary>
20+
bool IsLoading { get; }
21+
22+
/// <summary>
23+
/// Gets the page size of the list source.
24+
/// </summary>
25+
int PageSize { get; }
26+
27+
/// <summary>
28+
/// Gets the total number of items in the list.
29+
/// </summary>
30+
/// <returns>A task returning the count.</returns>
31+
Task<int> GetCount();
32+
33+
/// <summary>
34+
/// Gets the numbered page of items.
35+
/// </summary>
36+
/// <param name="pageNumber">The page number.</param>
37+
/// <returns>A task returning the page contents.</returns>
38+
Task<IReadOnlyList<T>> GetPage(int pageNumber);
39+
}
40+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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

Comments
 (0)