Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SortAndBind fixes and improvements #939

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

RolandPheasant
Copy link
Collaborator

@RolandPheasant RolandPheasant commented Sep 4, 2024

  • Enable opt-in to ResetOnFirstTimeLoad to ensure backwards compatibility with the old Sort(...).Bind(...) behaviour.

To opt-in system-wide after upgrading from v8 to v9 (and migrating to SortAndBind) like this:

DynamicDataOptions.SortAndBind = DynamicDataOptions.SortAndBind with { ResetOnFirstTimeLoad = true };
  • Specialised handing for BindingList when used with SortAndBind, to ensure reset is respected.

  • Specify SortAndBind scheduler at a system with level.

DynamicDataOptions.SortAndBind = DynamicDataOptions.SortAndBind with { Scheduler= myMainThreadScheduler };

With this ObserveOn(myMainThreadScheduler) will be a thing of the past.

Fixes #929

Copy link
Member

@dwcullop dwcullop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable to me.

@@ -33,12 +34,17 @@ public SortAndBind(IObservable<IChangeSet<TObject, TKey>> source,
// static one time comparer
var applicator = new SortApplicator(_cache, target, comparer, options);

_sorted = source.Do(changes =>
if (options.Scheduler is not null)
source = source.ObserveOn(options.Scheduler);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is insufficient. Refer to #932, the logical query this needs to serve as a replacement for is...

.Sort(...)
.ObserveOn(...)
.Bind(...)

I.E. the scheduling needs to be applied to both input streams, source and comparer. Otherwise, notifications coming in on comparer can trigger the ObservableCollection be manipulated off of the main thread.

Another .ObserveOn() call applied to comparer should be sufficient here, since there is no change stream "between" the sorting and binding steps anymore.

Copy link
Collaborator Author

@RolandPheasant RolandPheasant Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair call, I'll fix that.

Also I was wondering whether to add scheduler overloads to SortAndBind, or instead to instruct the use of

DynamicDataOptions.SortAndBind = DynamicDataOptions.SortAndBind with
{
    Scheduler = myMainThreadScheduler
};

which is neat but not obvious.

Or should I do both? Thoughts?

EDIT:

Or better yet set the scheduler like RxUI:

DynamicDataOptions.MainThreadScheduler= myMainThreadScheduler;

Copy link
Collaborator Author

@RolandPheasant RolandPheasant Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've applied the scheduler to the comparer changed observable, and moved the UI thread scheduler option to DynamicDataOptions.MainThreadScheduler.

So the one remaining question is whether it's worth adding 12 new overloads to pass an optional comparer to SortAndBind or to rely on the global main thread scheduler only?

Copy link
Collaborator

@JakenVeina JakenVeina Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say both. There's value in being able to specify on a per-call basis, and in being able to set it globally and not have to worry about it.

Not sure about whether a global-level scheduler should be universal, or per-operator. Again, I think there's value in both, but in this case, I think it's a little cumbersome to HAVE both.

Rather than MainThreadScheduler, I would suggest BindScheduler or BindingScheduler because that clarifies how it's actually being USED if you set it. It would only be pulled for binding operations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to BindingScheduler + added scheduler overloads for new generation of binding operators.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I was wondering whether to add scheduler overloads to SortAndBind, or instead to instruct the use of

I suggest both: Giving the ability to provide all the options for their special cases AND providing a helper method that facilitates the most common use case(s).

@@ -20,4 +21,10 @@ public static class DynamicDataOptions
/// Gets or sets the system-wide default values for all SortAndBind operations.
/// </summary>
public static SortAndBindOptions SortAndBind { get; set; } = new();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that BindingScheduler should go in SortAndBindOptions instead of the top level DynamicDataOptions. It seems confusing to have ADDITIONAL "Binding" options at the top level when there is already a section for Binding specific options.

If it applies to both SortAndBindOptions and BindingOptions, then it can be in both, allowing the ultimate in flexibility for consumers.

You could also do both: Add a Scheduler property to SortAndBindOptions and a DefaultScheduler property on DynamicDataOptions and then coalesce the values.

scheduler ??= localSortAndBindOptions.Scheduler
    ?? DynamicDataOptions.SortAndBind.Scheduler
    ?? DynamicDataOptions.DefaultScheduler
    ?? Scheduler.Default;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do that way, then you don't need a separate IScheduler parameter in addition to the SortAndBindOptions parameter.

You can have overloads that take EITHER a SortAndBindOptions OR an IScheduler parameter, and then the latter version can just call the former version with something like DynamicDataOptions.SortAndBind with { Scheduler: scheduler }.

Copy link
Collaborator Author

@RolandPheasant RolandPheasant Sep 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang. My original PR had the scheduler on SortAndBindOption and I'm now going in circles.

In many respects, it makes sense to have the scheduler on SortAndBindOption as all the new binding operators use these whereas I would rather not add it to the original binding options at that would mean I'd have to retrospectively change those having marked them obsolete.

If you do that way, then you don't need a separate IScheduler parameter in addition to the SortAndBindOptions parameter.
You can have overloads that take EITHER a SortAndBindOptions OR an IScheduler parameter, and then the latter version can just call the former version with something like DynamicDataOptions.SortAndBind with { Scheduler: scheduler }.

Wise words.

I'll sit on it for a day or two, but keeping the overloads to a minimum seems like a good idea.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems confusing to have ADDITIONAL "Binding" options at the top level when there is already a section for Binding specific options.

It doesn't seem confusing to me. DynamicDataOptions.BindingScheduler applies to all "Binding" operators, while .SortAndBindOptions.Scheduler applies to just the .SortAndBind() operator. I think there's value to be argued for both, but I definitely wouldn't add DynamicDataOptions.BindingScheduler without retrofitting it into all the existing operators. That would definitely be counter-intuitive. And I'd say that scope of change should be its own PR.

If you do that way, then you don't need a separate IScheduler parameter in addition to the SortAndBindOptions parameter.

Agreed. I see no point in having overloads for both IScheduler and SortAndBindOptions. If there was already an IScheduler one, it would make sense to add one for SortAndBindOptions, and leave the existing one there to avoid breakage, but that's not the case here.

In many respects, it makes sense to have the scheduler on SortAndBindOption as all the new binding operators use these

I think this is probably the way to go. That is, having each operator define its own XXXOptions struct, instead of just adding optional parameters. It makes adding new options much simpler. And, honestly, I think the point about being forced to retrofit some older operators when adding new options is a BENEFIT. It's a constraint we're placing on ourselves to keep the API a little more sane for consumers. I quite literally think there is only one downside to the XXXOptions pattern, and that's the increase in call site size (I.E. every call requires a full allocation of the options struct, regardless of which options are needed). I'm inclined to think this is a cost worth accepting, particularly since operator calls are not hot paths, only the operator innards are.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: ResetOnFirstTimeLoad not working with sorting
3 participants