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

Commit 48874bf

Browse files
Merge remote-tracking branch 'remotes/origin/refactor/pr-list' into refactor/pr-list-0.6-update
2 parents 82c24a8 + 7ce21a3 commit 48874bf

32 files changed

+810
-325
lines changed
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+
}

src/GitHub.App/Collections/SequentialListSource.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@
1111

1212
namespace GitHub.Collections
1313
{
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>
1430
public abstract class SequentialListSource<TModel, TViewModel> : ReactiveObject, IVirtualizingListSource<TViewModel>
1531
{
1632
static readonly ILogger log = LogManager.ForContext<SequentialListSource<TModel, TViewModel>>();
@@ -26,23 +42,29 @@ public abstract class SequentialListSource<TModel, TViewModel> : ReactiveObject,
2642
int loadTo;
2743
string after;
2844

45+
/// <summary>
46+
/// Initializes a new instance of the <see cref="SequentialListSource{TModel, TViewModel}"/> class.
47+
/// </summary>
2948
public SequentialListSource()
3049
{
3150
dispatcher = Application.Current?.Dispatcher;
3251
}
3352

53+
/// <inheritdoc/>
3454
public bool IsLoading
3555
{
3656
get { return isLoading; }
3757
private set { this.RaiseAndSetIfChanged(ref isLoading, value); }
3858
}
3959

60+
/// <inheritdoc/>
4061
public virtual int PageSize => 100;
4162

4263
event EventHandler PageLoaded;
4364

4465
public void Dispose() => disposed = true;
4566

67+
/// <inheritdoc/>
4668
public async Task<int> GetCount()
4769
{
4870
dispatcher?.VerifyAccess();
@@ -55,6 +77,7 @@ public async Task<int> GetCount()
5577
return count.Value;
5678
}
5779

80+
/// <inheritdoc/>
5881
public async Task<IReadOnlyList<TViewModel>> GetPage(int pageNumber)
5982
{
6083
dispatcher?.VerifyAccess();
@@ -73,14 +96,31 @@ public async Task<IReadOnlyList<TViewModel>> GetPage(int pageNumber)
7396
return result;
7497
}
7598

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>
76104
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>
77111
protected abstract Task<Page<TModel>> LoadPage(string after);
78112

113+
/// <summary>
114+
/// Called when the source begins loading pages.
115+
/// </summary>
79116
protected virtual void OnBeginLoading()
80117
{
81118
IsLoading = true;
82119
}
83120

121+
/// <summary>
122+
/// Called when the source finishes loading pages.
123+
/// </summary>
84124
protected virtual void OnEndLoading()
85125
{
86126
IsLoading = false;

src/GitHub.App/Collections/VirtualizingList.cs

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,42 @@
77
using System.Threading.Tasks;
88
using System.Windows;
99
using System.Windows.Threading;
10+
using GitHub.Logging;
11+
using Serilog;
1012

1113
namespace GitHub.Collections
1214
{
15+
/// <summary>
16+
/// A virtualizing list that loads data only when needed.
17+
/// </summary>
18+
/// <typeparam name="T">The list item type.</typeparam>
19+
/// <remarks>
20+
/// This class exposes a read-only list where the data is fetched as needed. When the indexer
21+
/// getter is called, if the requested item is not yet available it calls the associated
22+
/// <see cref="IVirtualizingListSource{T}"/> to load the page of data containing the requested
23+
/// item. While the data is being read, <see cref="Placeholder"/> is returned and when the
24+
/// data is read <see cref="CollectionChanged"/> is raised.
25+
///
26+
/// Note that this implementation currently represents the minimum required for interaction
27+
/// with WPF and as such many members are not yet implemented. In addition, if filtering is
28+
/// required in the UI then the collection can be wrapped in a
29+
/// <see cref="VirtualizingListCollectionView{T}"/>.
30+
/// </remarks>
1331
public class VirtualizingList<T> : IReadOnlyList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged
1432
{
33+
static readonly ILogger log = LogManager.ForContext<VirtualizingList<T>>();
1534
readonly Dictionary<int, IReadOnlyList<T>> pages = new Dictionary<int, IReadOnlyList<T>>();
1635
readonly IVirtualizingListSource<T> source;
1736
readonly IList<T> emptyPage;
1837
readonly IReadOnlyList<T> placeholderPage;
1938
readonly Dispatcher dispatcher;
2039
int? count;
2140

41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="VirtualizingList{T}"/> class.
43+
/// </summary>
44+
/// <param name="source">The list source.</param>
45+
/// <param name="placeholder">The placeholder item.</param>
2246
public VirtualizingList(
2347
IVirtualizingListSource<T> source,
2448
T placeholder)
@@ -30,6 +54,11 @@ public VirtualizingList(
3054
dispatcher = Application.Current?.Dispatcher;
3155
}
3256

57+
/// <summary>
58+
/// Gets an item by index.
59+
/// </summary>
60+
/// <param name="index">The index of the item.</param>
61+
/// <returns>The item, or <see cref="Placeholder"/> if the item is not yet loaded.</returns>
3362
public T this[int index]
3463
{
3564
get
@@ -60,6 +89,13 @@ public T this[int index]
6089
set { throw new NotImplementedException(); }
6190
}
6291

92+
/// <summary>
93+
/// Gets the total count of the collection, including not-yet-loaded items.
94+
/// </summary>
95+
/// <remarks>
96+
/// If the count has not yet been loaded, this will return 0 and then raise a
97+
/// <see cref="PropertyChanged"/> event when the count is loaded.
98+
/// </remarks>
6399
public int Count
64100
{
65101
get
@@ -74,8 +110,19 @@ public int Count
74110
}
75111
}
76112

113+
/// <summary>
114+
/// Gets the placeholder item that will be displayed while an item is loading.
115+
/// </summary>
77116
public T Placeholder { get; }
117+
118+
/// <summary>
119+
/// Gets the loaded pages of data.
120+
/// </summary>
78121
public IReadOnlyDictionary<int, IReadOnlyList<T>> Pages => pages;
122+
123+
/// <summary>
124+
/// Gets the page size of the associated <see cref="IVirtualizingListSource{T}"/>.
125+
/// </summary>
79126
public int PageSize => source.PageSize;
80127

81128
object IList.this[int index]
@@ -99,45 +146,14 @@ public IEnumerator<T> GetEnumerator()
99146
while (i < Count) yield return this[i++];
100147
}
101148

102-
int IList.Add(object value)
103-
{
104-
throw new NotImplementedException();
105-
}
106-
107-
void IList.Clear()
108-
{
109-
throw new NotImplementedException();
110-
}
111-
112-
bool IList.Contains(object value)
113-
{
114-
throw new NotImplementedException();
115-
}
116-
117-
int IList.IndexOf(object value)
118-
{
119-
throw new NotImplementedException();
120-
}
121-
122-
void IList.Insert(int index, object value)
123-
{
124-
throw new NotImplementedException();
125-
}
126-
127-
void IList.Remove(object value)
128-
{
129-
throw new NotImplementedException();
130-
}
131-
132-
void IList.RemoveAt(int index)
133-
{
134-
throw new NotImplementedException();
135-
}
136-
137-
void ICollection.CopyTo(Array array, int index)
138-
{
139-
throw new NotImplementedException();
140-
}
149+
int IList.Add(object value) => throw new NotImplementedException();
150+
void IList.Clear() => throw new NotImplementedException();
151+
bool IList.Contains(object value) => throw new NotImplementedException();
152+
int IList.IndexOf(object value) => throw new NotImplementedException();
153+
void IList.Insert(int index, object value) => throw new NotImplementedException();
154+
void IList.Remove(object value) => throw new NotImplementedException();
155+
void IList.RemoveAt(int index) => throw new NotImplementedException();
156+
void ICollection.CopyTo(Array array, int index) => throw new NotImplementedException();
141157

142158
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
143159

@@ -166,9 +182,9 @@ void LoadCount()
166182
}, TaskScheduler.FromCurrentSynchronizationContext());
167183
}
168184
}
169-
catch (Exception)
185+
catch (Exception ex)
170186
{
171-
// Handle exception.
187+
log.Error(ex, "Error loading virtualizing list count");
172188
}
173189
}
174190

@@ -187,9 +203,9 @@ async void LoadPage(int number)
187203
SendReset();
188204
}
189205
}
190-
catch (Exception)
206+
catch (Exception ex)
191207
{
192-
// Handle exception.
208+
log.Error(ex, "Error loading virtualizing list page {Number}", number);
193209
}
194210
}
195211

src/GitHub.App/Collections/VirtualizingListCollectionView.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,39 @@
66

77
namespace GitHub.Collections
88
{
9+
/// <summary>
10+
/// A <see cref="CollectionView"/> that adds filtering to a <see cref="VirtualizingList{T}"/>.
11+
/// </summary>
12+
/// <typeparam name="T">The item type.</typeparam>
913
public class VirtualizingListCollectionView<T> : CollectionView, IList
1014
{
1115
List<int> filtered;
1216

17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="VirtualizingListCollectionView{T}"/> class.
19+
/// </summary>
20+
/// <param name="inner">The inner virtualizing list.</param>
1321
public VirtualizingListCollectionView(VirtualizingList<T> inner)
1422
: base(inner)
1523
{
1624
}
1725

26+
/// <summary>
27+
/// Gets the count of the filtered items.
28+
/// </summary>
1829
public override int Count => filtered?.Count ?? Inner.Count;
30+
31+
/// <inheritdoc/>
1932
public override bool IsEmpty => Count == 0;
2033

2134
bool IList.IsReadOnly => true;
2235
bool IList.IsFixedSize => false;
2336
object ICollection.SyncRoot => null;
2437
bool ICollection.IsSynchronized => false;
2538

39+
/// <summary>
40+
/// Gets the inner virtualizing list.
41+
/// </summary>
2642
protected VirtualizingList<T> Inner => (VirtualizingList<T>)SourceCollection;
2743

2844
object IList.this[int index]
@@ -31,6 +47,7 @@ object IList.this[int index]
3147
set { throw new NotImplementedException(); }
3248
}
3349

50+
/// <inheritdoc/>
3451
public override object GetItemAt(int index)
3552
{
3653
if (filtered == null)
@@ -43,14 +60,14 @@ public override object GetItemAt(int index)
4360
}
4461
}
4562

46-
int IList.Add(object value) { throw new NotSupportedException(); }
47-
bool IList.Contains(object value) { throw new NotImplementedException(); }
48-
void IList.Clear() { throw new NotSupportedException(); }
49-
int IList.IndexOf(object value) { throw new NotImplementedException(); }
50-
void IList.Insert(int index, object value) { throw new NotSupportedException(); }
51-
void IList.Remove(object value) { throw new NotSupportedException(); }
52-
void IList.RemoveAt(int index) { throw new NotSupportedException(); }
53-
void ICollection.CopyTo(Array array, int index) { throw new NotImplementedException(); }
63+
int IList.Add(object value) => throw new NotSupportedException();
64+
bool IList.Contains(object value) => throw new NotImplementedException();
65+
void IList.Clear() => throw new NotSupportedException();
66+
int IList.IndexOf(object value) => throw new NotImplementedException();
67+
void IList.Insert(int index, object value) => throw new NotSupportedException();
68+
void IList.Remove(object value) => throw new NotSupportedException();
69+
void IList.RemoveAt(int index) => throw new NotSupportedException();
70+
void ICollection.CopyTo(Array array, int index) => throw new NotImplementedException();
5471

5572
protected override void RefreshOverride()
5673
{

src/GitHub.App/GitHub.App.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@
194194
<HintPath>..\..\packages\Rx-XAML.2.2.5-custom\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
195195
<Private>True</Private>
196196
</Reference>
197+
<Reference Include="System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
198+
<HintPath>..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll</HintPath>
199+
</Reference>
197200
<Reference Include="System.Web" />
198201
<Reference Include="System.Xaml" />
199202
<Reference Include="System.Xml.Linq" />
@@ -204,6 +207,7 @@
204207
<Reference Include="WindowsBase" />
205208
</ItemGroup>
206209
<ItemGroup>
210+
<Compile Include="Collections\IVirtualizingListSource.cs" />
207211
<Compile Include="Collections\SequentialListSource.cs" />
208212
<Compile Include="Collections\VirtualizingList.cs" />
209213
<Compile Include="Collections\VirtualizingListCollectionView.cs" />

src/GitHub.App/SampleData/PullRequestListItemViewModelDesigner.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ public class PullRequestListItemViewModelDesigner : ViewModelBase, IPullRequestL
1313
public IActorViewModel Author { get; set; }
1414
public int CommentCount { get; set; }
1515
public bool IsCurrent { get; set; }
16-
public IReadOnlyList<ILabelViewModel> Labels { get; set; }
1716
public int Number { get; set; }
1817
public string Title { get; set; }
1918
public DateTimeOffset UpdatedAt { get; set; }

0 commit comments

Comments
 (0)