-
Notifications
You must be signed in to change notification settings - Fork 162
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
Add initial draft for "Enabling Batch Events for ObservableCollection" #320
base: main
Are you sure you want to change the base?
Conversation
@terrajobst Thanks so much for putting this document together. It's fantastic and very well thought out. I would point out in the note around contiguous blocks of items, I could be wrong but it appears that's only true if you set the Index properties in One good thing that could come out of this would be simple guidance around the DOs and DON'Ts around using Might even be a good opportunity to think about marking Also want to throw out there that a variant of the "Handle it in CollectionView" option could be to add an optional
Appreciate everyone's effort to try to make this happen in .NET 10! |
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.
Thanks, this looks like a good write-up of the problem space. I wonder if we should try to commit to a specific solution before we merge this to the design repo. For the record I am supportive of the global switch approach since it does the right thing™️ and offers an off-ramp for impacted applications.
I'd say yes. It's the primary reason why I marked it as draft. I would leave the other solutions in the document, with an explanation why we went with what we believe the best solution will be, for prosperity, but also has an alternative should the route we take become unworkable. |
My understanding is that the party creating the One way to deal with this in I'm personally leaning towards the eventing adapter. We could have an extension method on public ObservableCollection<Customer> Customers { get; }
public INotifyCollectionChanged CustomersSingleItemEvents => Customers.ToSingleItemNotifications(); It's not entirely clear what the wrapper would need to implement in order to be useful and if we need a new public type to handle it. Maybe we could make it so that What I like about wrapping is that people don't need to change consuming code to subscribe to different events and that we don't need a brand-new collection type that view models would need to support. |
How would |
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.
Thanks for putting this together and providing a robust and definitive description of the issue! 🦙❤️
Co-authored-by: Michael Hawker MSFT (XAML Llama) <24302614+michael-hawker@users.noreply.github.com>
Excellent point. I believe you're correct -- this doesn't seem feasible. I've pushed 3450d45 to remove this option. |
I would recommend against a global switch, and here's why: Let's say I have an application that I use to collect near-real-time data, and I choose to use WPF for the UI.
In this situation, I would want the first A global switch here would be detrimental because WPF's presence in my app means that NO The developer would also be expecting certain behaviors that wouldn't happen and would have no indication at runtime why they wouldn't work (unless a Console warning was added in whenever the switch is on and the event is thrown). I believe the only workable implementation alternative that doesn't force WPF to fix their problems would be to create a new property on The downside would be that something else could potentially witch the mode back, at which point the WPF controls would throw the expected exception, but maybe the exception could be modified to provide additional information about the need for setting the correct mode. HTH! |
Understood, however I think the chicken-and-egg situation that both the libraries and WPF teams find themselves in is that we can't ship batched events OOTB because it breaks WPF and WPF can't fix their handlers because batched events haven't shipped yet. The point of a global flag is that it gives downstream consumers the ability to meaningfully fix any issues (as opposed to making speculative changes based on what one expects might break), at their own convenience, and without breaking the universe. I can see this happening in the following timeline:
I realize that the situation between steps 1. and 2. is going to be less than ideal, but from my perspective it's the best way to safely progress the ecosystem towards batched events without introducing additional concepts in our APIs. |
I think if we treat a global flag not as a shipping feature but as an "in between previews staging mechanism", the end result at GA is basically equivalent to "it just works". Now, this doesn't help other parties (say, Avalonia), which might force us to either add a global switch for GA or include an API. However, I believe we can wait till the first previews and see how feasible it is to get most of the outside parties involved early on so have support by GA as well. |
I think shipping a global flag in GA is going to be inevitable for those apps using bespoke handlers that also don't account for batched events, and that is a good thing in my mind. The main open question is what the default value of said global flag is going to be in GA. |
- > [!NOTE] | ||
> For the same reason event adapters aren't viable just having a new | ||
> interface is probably not sufficient. Rather, we'd need a different | ||
> implementation like `BulkObservableCollection<T>`. However, for WinUI |
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.
We looked into this before and we concluded that actually we would not need a new interface at all for both WinUI 3 and UWP. INotifyCollectionChanged
is projected to either Microsoft.UI.Xaml.Interop.INotifyCollectionChanged
(for WinUI 3) or Windows.UI.Xaml.Interop.INotifyCollectionChanged
(for UWP), and these interfaces already have all the support they need for multiple items (they kinda mirror the .NET event args). Which also means the underlying infrastructure (ie. WinUI 3 and UWP XAML controls) already work in term of that, we'd just need to do additional testing to be extra sure. But in general, all of this should "just work" for both WinUI 3 and modern UWP.
cc. @manodasanW
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, I think if any work would be within like ListView
vs. the projections of the data, similar to the work needed in WPF.
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.
In theory, ListView
and all other controls should also just work, because they've been built against that WinRT interface which also supports batch updates. As in, someone could've been sending batch updates already, eg. from C++. So I'd be surprised if those controls didn't support that scenario. Of course though, we should definitely test. But I'm saying at least the good news here is that on the .NET front, there should be no extra work/complexity 🙂
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.
That's cool!
Presumably, the assumption still is that the ranges are contiguous, correct?
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.
Looking at the properties for NotifyCollectionChangedEventArgs
I assume so, yes. @jkoritzinsky do you perhaps know? Otherwise if we need we can reach out to XAML folks to confirm (anyone from the WinUI team), can't hurt.
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.
Yes, the two event args types have a 1:1 projection; that is each can represent exactly the same set of states as the other. So if the .NET version only supports contiguous ranges, the WinRT versions also support only contiguous ranges.
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.
Adding @ranjeshj who can maybe comment more on the UI work beyond the projection capabilities for the containerization aspects.
Beyond our own UX stacks where we know there's a problem, how many apps is this going to be though? If they're implementing something custom, isn't it usually to support batches for this interface? How much do we want to coddle invalid assumptions/shortcuts vs. just a cost of upgrading to the latest .NET and the cost of us having/maintaining separate paths? Can we just better prepare folks in .NET 9 for this change? e.g. Is there an analyzer that could be shipped in .NET 9 to highlight when something implementing |
If the global flag was transitory for the release cycle that would be a lot easier pill to swallow, certainly. Would it be possible to add telemetry if the flag is set that warns the developer and points them to an aka.ms location that would explain what is happening? |
I'm curious why you think that shipping a feature switch is problematic, regardless of its default value. We ship feature switches all the time for the most minor potential breaking change that we introduce, I don't see why we couldn't do the same for something this substantial. |
Can you explain what you mean by "adding telemetry"? To me, this would mean that we'd just track how many people turns this on; this doesn't result in any kind of notification to the person enabling it. |
I mean like a |
I had hoped my detailed example showed that not being able to control it on a case-by-case basis would yield unpredictable results at runtime for developer customers. But I can put it a different way:
If I experienced this, I'd be wicked pissed. If that's a transitory situation through the .NET 10 release process that is thoroughly documented, that's one thing. If it ships in RTM and WPF decides to punt again, you'd be hard-pressed to counter the notion from customers that Microsoft is just enabling WPF to continue being lazy in not fixing their architectural flaws (as AFAIK every other UI framework has done work already for a feature like this). In these types of situations, I would personally favor a solution that isolates the problem and creates the widest benefit (internal possibly-permanent But I also understand the intellectual consistency of "we use global flags for stuff like this so we'd do the same thing here too" and certainly wouldn't fault you for using it. I just wanted to provide a counter-viewpoint so you could see the situation from an "outside Microsoft" perspective. I hope that is helpful. Thank you all so much for your hard work on this issue. :) |
I think there is a misunderstanding of what we mean by global switches. We usually think of them as compatibility switches that the app developer turns on, as opposed to a library author or a part of the .NET runtime, such as WPF. In many cases the switches are not resulting in new behavior but rather restore previous behavior (i.e. they act as opting out from behavior that we identified as potentially disruptive). In some cases we ship global switches for experimental features that consumers set in order to opt-into behavior that we would like to get feedback on. We have largely replaced this with So in that sense I don't think the global switch would work as you describe, where console apps/unit tests experience one set of behaviors while WPF apps have a different behavior -- unless, of course, the app developer says so. Here is what I could imagine we'd do:
Does this make sense? |
Thanks for that explanation :) Ultimately I understand the purpose and how they are implemented. I'm just pointing out that: a) A library developer could in fact set these switches. I run the BlazorEssentials library and I will likely turn that flag on in my b) A developer could in fact set the switch to different values in different parts of their application framework that could cause problems they may or may not understand. The global flag solution may solve the problem for you internally, but it is not "idiot-proof" externally. That's all I'm saying. The only solution to that for the .NET 10 release cycle may just be good solid documentation about how the feature is supposed to work, what UI frameworks are planned for support, and how to deal with various situations. And I'm just trying to point that out up front so it gets planned for and handled. Sorry for beleaguering the point, I'll shut up now :) |
Totally. My take is that if the final design requires setting a global flag to use bulk notifications, then we failed. If that's all we can do to ship the feature, I'd start by questioning if shipping it is even worth it, due to the very interop problems you cite. I see global flags only as a temporary band-aid to allow customers to work around breaking changes while the ecosystem responds to it. The design of the feature must mean that eventually it just works.
No no no, I created this very place so we talk about the implications of the design in depth! :-) |
You mean calling |
All modern UI frameworks (Avalonia, WinUI 3, UWP, uno and etc.) support batch event out-of-box. You can simply use the following code to verify this: public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel = new();
public MainWindow()
{
InitializeComponent();
DataContext = _viewModel;
}
private void Button_Click(object? sender, RoutedEventArgs e)
{
_viewModel.List.AddRange(["Item 1", "Item 2", "Item 3"]);
}
}
public class MyViewModel
{
public MyList<string> List { get; set; } = new();
}
public class MyList<T> : List<T>, INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public new void AddRange(IEnumerable<T> collection)
{
var index = this.Count;
var list = collection.ToList();
foreach (var item in collection)
{
Add(item);
}
CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, list, index));
}
} The issue here is only WPF doesn't support batch event, making all other frameworks pay the bill for WPF is not a great decision. The right way should really be adding batch APIs to |
I see WPF mentionned a lot but is it really a WPF specific problem ? I know WPF needs to be fixed for batching to be enabled but I think that WPF having this problem shows that other code bases probably also have this problem. Things like control vendors (Like DevExpress, Telerik, etc), open-source control libraries or any other code that hooks onto CollectionChanged could potentially silently break. Also, code bases could look like it supports batching but since Even if WPF was fixed tomorrow I don't think enabling batching by default would be a wise decision. My comment seems a bit negative but I'm supportive of allowing apps to enable batching one way or another. |
Why not let it throw if it's not supported? Existing code still uses non-batch apis so that it will still work. New code developers will know whether batch apis are supported or not immediately after the exception being thrown. |
That's why during the development of .NET 10 it would be going through 3 stages:
It might sound "weird" to hope for external library developers pro-actively testing and fixing bulk events during stage 1. But that's actually a pretty normal way to introduce new features or changes, and updating your code to stay compatible with newer .NET releases is an essential part of what a library developer/maintainer does. |
> implementation like `BulkObservableCollection<T>`. However, for WinUI | ||
> we likely also still need an interface 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.
This would not only impact WinUI (3), but UWP as well. In general, it would impact anyone trying to ever marshal this new interface across the WinRT boundary. The fact it impacts WinUI 3 and UWP is just a consequence of this.
that turns off bulk notifications. | ||
- Pro: Allows some part of the view model to use bulk notifications when the | ||
UI supports it while turning it off for the parts that don't. | ||
- Con: Still limiting when the same collection is bound to multiple UI |
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'd say the same collection being bound to multiple UI elements would be an uncommon pattern.
There are also only a few built-in surfaces in the UX frameworks that need to worry about this as they're all generally built upon a common base class.
It seems the most straight-forward approach is the global switch. I would hope if it were added early enough in the .NET 10 development cycle, it'd give frameworks a long runway to support leading up to release as part of the updating generally required release-to-release anyway as we've broken/changed other parts of .NET prior already (e.g. RID graph).
@terrajobst Since we are already in the development cycle for .NET 10, do you have any information whether this is going to be pulled into the roadmap? |
It's pretty clear that
ObservableCollection<T>
isn't a simple change to drive so I decided to write a design document that we can use at the beginning of .NET 10` to come up with a plan.@eiriktsarpalis, if you got some time I'd appreciate your review on this.