-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Changes-tracking collection for list views #111
Conversation
8fd274c
to
ae0cf35
Compare
TrackingCollection gets items from an observable sequence and updates its contents in such a way that two updates to the same object (as defined by an Equals call) will result in one object on the list being updated (as opposed to having two different instances of the object added to the list). This allows us to grab a bunch of objects from cache, load a collection with them, and then request updated versions of the objects from the server, which when returned will update the collection in place (and the UI with it), seamlessly. The collection supports ordering and filtering, and filtering includes enough information to allow limiting the number of items to return (and display). It can be resorted and refiltered in place.
ae0cf35
to
7d40d74
Compare
Is |
|
||
namespace GitHub.Collections | ||
{ | ||
public class TrackingCollection<T> : ObservableCollection<T>, ITrackingCollection<T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's document this class like you did in the PR.
{ | ||
OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); | ||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, this[start], start, idxSmallest)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, this method does a completely new sort, right? Could we just call list.Sort
and then raise the NotifyCollectionChangedAction.Reset
event rather than raise an event for each item?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, list.Sort
makes sense here.
Ok, I'm done with my first pass. 🍨 |
It's GREEN! All the tests except for two timing tests are running on ci. The timings tests are iffy and likely to fail on a server, so better not have that stuff there. Also, looks like we can't use your string interpolation thing on the server. Not sure why, but it just won't build and I'm not in the mood to fight it any more. 📦 |
Excellent! I'll do another round of reviews tonight! |
data = CheckFilter(data); | ||
data = CalculateIndexes(data); | ||
data = SortedRemove(data); | ||
data = FilteredRemove(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data
isn't returned. So why the assignments? Could these be void methods? Or should data
be returned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are part of a pipeline of methods that get called sequentially and that process an item (see Listen method). They might or might not return a different ActionData object as part of the processing. SortedRemove and FilteredRemove aren't technically a part of the pipeline of the Listen method (because they'd always be noop there), but for all intents and purposes they follow the same pattern.
Just pretend these 4 calls are each a Select call on an observable 😄
I debated whether to return the removed item (hence the final assignment doing nothing). Might stil do that. In the meantime, the final assignment is just keeping the pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's a pipeline, shouldn't each method return a new instance of ActionData
instead of mutating the passed in instance? I think this calling pattern is really confusing.
Also, SortedRemove
and FilteredRemove
actually mutate the current TrackingCollection
and do not mutate data
. They only read from it. They are very different from the methods called before them, which do some calculations and return an ActionData
with the calculations. I think that difference and intent would be better communicated by making those two methods return void
.
So this method would look like.
var data = new ActionData(TheAction.Remove, item, null, position - 1, position, original);
data = CheckFilter(data);
data = CalculateIndexes(data);
SortedRemove(data);
FilteredRemove(data);
Where data
is never mutated. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's a pipeline, shouldn't each method return a new instance of ActionData instead of mutating the passed in instance? I think this calling pattern is really confusing.
Yes, if this was a purely functional thing, it would return a new ActionData
for every call. I don't do that because I don't want to be creating thousands of objects and putting pressure on the GC when I don't have to.
ActionData
has non-mutable data, which is the data regarding the item that's being processed and where it is in the sorted, unfiltered list (the item, it's position in the list, where it was before, and a reference to the list itself). This data is built on ProcessItem
. After that, other information is added to ActionData
so I don't have to keep looking it up, like whether it's visible according to the current filter, it's index in the filtered list, etc. All this (mutable) information is related to the filtered list (the one that is visible via the Items
property of the base class).
I tried to strike a balance between immutability and the fact that every item processed would have at least 3 "updates" to it's ActionData
object (or more), and go through 5 methods, and I don't like needlessly throwing pressure at the GC. Whenever data that is essential to the object is calculated, a new ActionData
is created. Other data merely updates the existing ActionData
(for instance, if there's no filter, only the data for the unfiltered list is needed)
Also, SortedRemove and FilteredRemove actually mutate the current TrackingCollection and do not mutate data.
Neither do the other SortedXXX
and FilteredXXX
methods that are called via the Select
calls in the Listen
method. By the time they're called, the data in ActionData
is ready for processing by these methods, so they merely use it and pass it through.
In a future where I can figure out how to include Remove actions into the pipeline that's set up in the Listen
methods, SortedRemove
and FilteredRemove
will have to conform to the same pattern as the other SortedXX
and FilteredXX
methods. The only reason they're not included in the Listen
method list of Select
calls right now is because they would be noops and I don't see the point in having method calls that currently are known to be noops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I change this so that RemoveItem actually returns the item that was removed. It's a common pattern and useful thing to have (because the item you pass in might not be the exact same instance that is removed). It returns null when it couldn't find anything to remove, so that's a nice thing to have too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and I don't like needlessly throwing pressure at the GC
But you wouldn't be if ActionData
was a struct, right? It really feels like a struct to me. It's just a value type that defines what action should happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you wouldn't be if ActionData was a struct, right? It really feels like a struct to me. It's just a value type that defines what action should happen.
You're absolutely correct, it should be a struct, and that makes things make more sense. I'll fix that up right away.
Ugh, |
Just a little bit more to go! Back to you @shana. 😄 |
I really don't know what to do about this. I can't understand why we have a limit on PR size without a way to see the full diff on the website, it makes it really pointless for feature work. 😢 I'm hacking on a diff commenter in VS so you can comment there on the PR, but can't make that work fast enough to get this thing out the door. If only we had PR reviewing in VS... 😜 |
IsIncluded = other.IsIncluded; | ||
} | ||
|
||
public ActionData(ActionData other, int position) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ctor
is never used. Should we remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like some of these ctors
could be defined in terms of the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaned up the constructors. The ctor
above is being used now (before it was passing default args and hitting another ctor
, that's fixed now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you remember to push those changes? The only change I see is a merge to master. The constructors haven't changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did, but it went to the wrong branch because the deskop app is just not liking me today. All fixed, everything pushed correctly now :P
Back to you! 🍍 |
Changes-tracking collection for list views
TrackingCollection gets items from an observable sequence and updates its contents in such a way that two updates to the same object (as defined by an Equals call) will result in one object on the list being updated (as opposed to having two different instances of the object added to the list).
This allows us to grab a bunch of objects from cache, load a collection with them, and then request updated versions of the objects from the server, which when returned will update the collection in place (and the UI with it), seamlessly.
The collection supports ordering and filtering, and filtering includes enough information to allow limiting the number of items to return (and display). It can be resorted and refiltered in place.
This PR also adds the IPullRequestModel model interface, because that's where this list will be used and might as well test things on it.
Also now that I've tagged 1.0.15, we can put these into master. Any further bug fixes on the 1.0 series can be done on a branch if needed.