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

Adds Range Manipulation APIs for Collection<T> and ObservableCollection<T> #65101

Closed
wants to merge 2 commits into from

Conversation

SkyeHoefling
Copy link
Contributor

@SkyeHoefling SkyeHoefling commented Feb 9, 2022

Re-Adds range manipulation APIs for Collection<T> and ObservableCollection<T>

Resolves: #18087

Added the following new API that have been approved in #18087:

// Adds a range to the end of the collection.
// Raises CollectionChanged (NotifyCollectionChangedAction.Add)
public void AddRange(IEnumerable<T> collection) => InsertItemsRange(0, collection);

// Inserts a range
// Raises CollectionChanged (NotifyCollectionChangedAction.Add)
public void InsertRange(int index, IEnumerable<T> collection) => InsertItemsRange(index, collection);

// Removes a range.
// Raises CollectionChanged (NotifyCollectionChangedAction.Remove)
public void RemoveRange(int index, int count) => RemoveItemsRange(index, count);

// Will allow to replace a range with fewer, equal, or more items.
// Raises CollectionChanged (NotifyCollectionChangedAction.Replace)
public void ReplaceRange(int index, int count, IEnumerable<T> collection)
{
     RemoveItemsRange(index, count);
     InsertItemsRange(index, collection);
}

// virtual methods
protected virtual void InsertItemsRange(int index, IEnumerable<T> collection);
protected virtual void RemoveItemsRange(int index, int count);

The code contained in this PR has already gone through a full PR review that was originally submitted in 2019 and was pulled due to downstream breaking changes in WPF. I have been talking with the .NET Team and the WPF Team and they both have approved the necessary changes to WPF to get this to work.

Original PRs

Related WPF Issues/PRs

Benchmarks - 2/11/2022

BenchmarkDotNet=v0.13.1.1694-nightly, OS=Windows 10 (10.0.19043.1466/21H1/May2021Update)
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=7.0.100-dev
  [Host]     : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
  Job-WEEQYH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog  Toolchain=CoreRun
IterationTime=250.0000 ms  MaxIterationCount=20  MinIterationCount=15
WarmupCount=1

|                            Method | Size |       Mean |     Error |    StdDev |     Median |        Min |        Max |  Gen 0 |  Gen 1 | Allocated |
|---------------------------------- |----- |-----------:|----------:|----------:|-----------:|-----------:|-----------:|-------:|-------:|----------:|
|          ObservableCollection_Add |  512 |  13.312 us | 0.2078 us | 0.1943 us |  13.216 us |  13.139 us |  13.834 us | 5.3797 | 0.1582 |  44.24 KB |
|     ObservableCollection_AddRange |  512 |   7.043 us | 0.0797 us | 0.0706 us |   7.031 us |   6.941 us |   7.199 us | 1.0054 | 0.0279 |   8.34 KB |
|       ObservableCollection_Insert |  512 |  13.729 us | 0.2636 us | 0.2707 us |  13.604 us |  13.488 us |  14.310 us | 5.3926 | 0.1618 |  44.24 KB |
|  ObservableCollection_InsertRange |  512 |   6.986 us | 0.0343 us | 0.0268 us |   6.980 us |   6.958 us |   7.053 us | 1.0141 | 0.0290 |   8.34 KB |
|       ObservableCollection_Remove |  512 | 117.300 us | 0.6794 us | 0.6355 us | 117.100 us | 116.313 us | 118.329 us | 2.3321 |      - |  22.11 KB |
|  ObservableCollection_RemoveRange |  512 |  33.449 us | 0.2618 us | 0.2449 us |  33.412 us |  33.094 us |  33.839 us | 0.9328 |      - |    8.2 KB |
|      ObservableCollection_Replace |  512 |  93.224 us | 0.8643 us | 0.8085 us |  92.844 us |  92.183 us |  94.654 us | 9.2456 |      - |  76.11 KB |
| ObservableCollection_ReplaceRange |  512 |  39.413 us | 0.1830 us | 0.1712 us |  39.404 us |  39.126 us |  39.677 us | 0.9422 |      - |   8.26 KB |

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Feb 9, 2022
@ghost
Copy link

ghost commented Feb 9, 2022

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

Issue Details

Re-Adds range manipulation APIs for Collection<T> and ObservableCollection<T>

Resolves: #18087

Added the following new API that have been approved in #18087:

// Adds a range to the end of the collection.
// Raises CollectionChanged (NotifyCollectionChangedAction.Add)
public void AddRange(IEnumerable<T> collection) => InsertItemsRange(0, collection);

// Inserts a range
// Raises CollectionChanged (NotifyCollectionChangedAction.Add)
public void InsertRange(int index, IEnumerable<T> collection) => InsertItemsRange(index, collection);

// Removes a range.
// Raises CollectionChanged (NotifyCollectionChangedAction.Remove)
public void RemoveRange(int index, int count) => RemoveItemsRange(index, count);

// Will allow to replace a range with fewer, equal, or more items.
// Raises CollectionChanged (NotifyCollectionChangedAction.Replace)
public void ReplaceRange(int index, int count, IEnumerable<T> collection)
{
     RemoveItemsRange(index, count);
     InsertItemsRange(index, collection);
}

// virtual methods
protected virtual void InsertItemsRange(int index, IEnumerable<T> collection);
protected virtual void RemoveItemsRange(int index, int count);

The code contained in this PR has already gone through a full PR review that was originally submitted in 2019 and was pulled due to downstream breaking changes in WPF. I have been talking with the .NET Team and the WPF Team and they both have approved the necessary changes to WPF to get this to work.

Original PRs

Related WPF Issue

Pending Changes

I am still working on the changes to dotnet/wpf, once I submit the PR to dotnet/wpf I will link it here.

Author: ahoefling
Assignees: -
Labels:

area-System.Collections, new-api-needs-documentation

Milestone: -

@eiriktsarpalis
Copy link
Member

Change blocked on dotnet/wpf#6097.

@Sergio0694
Copy link
Contributor

First thing, this is awesome, so glad to see this being picked up again! 🎉

@ahoefling have you also talked with the WinUI 3 team to confirm whether they're planning to add support for this? UWP is not a concern given it would never have access to these new APIs, but WinUI 3 very much is, just like WPF is/was (hence the delay).

@SkyeHoefling
Copy link
Contributor Author

Thank you @Sergio0694 I am glad we are getting everyone involved early enough to get this in for .NET 7.

I have not spoken with the WinUI 3 team yet, do you know who we should loop into this PR so we can make sure there are no issues. Are there any other downstream projects that we should be worried about for breaking changes? Last time I looked at the MAUI/Xamarin.Forms code they do something different with CollectionChanged and it shouldn't be a problem like it is in WPF.

@eiriktsarpalis eiriktsarpalis marked this pull request as draft February 10, 2022 16:58
@eiriktsarpalis
Copy link
Member

Thanks @ahoefling. I'm converting this PR to a draft until dotnet/wpf#6097 has been resolved.

@SkyeHoefling
Copy link
Contributor Author

Do any of the build artifacts provide a ready to use sdk I can use to compile a wpf sample application? I am having some trouble getting my changes in this PR to be used in my WPF application as it is a new API. If not is there a doc link on the dotnet/sdk project? The pages I have read thus far don't mention how to use your own dotnet/runtime

@Sergio0694
Copy link
Contributor

"I have not spoken with the WinUI 3 team yet, do you know who we should loop into this PR so we can make sure there are no issues."

Pinging @ryandemopoulos as we had talked about this internally a while back, he'd know who to relay this to I'm sure 😊

"Are there any other downstream projects that we should be worried about for breaking changes? Last time I looked at the MAUI/Xamarin.Forms code they do something different with CollectionChanged and it shouldn't be a problem like it is in WPF."

As for actual frameworks, can't think of other Microsoft-maintained ones other than WPF, WinUI 3 and MAUI (MAUI is owned by the .NET team anyway so someone from there will be able to comment on whether that needs any changes or not). As for other projects, we'll likely need to support this as well in the Windows Community Toolkit (cc. @michael-hawker) and in the MAUI Community Toolkit (cc. @brminnick), as well as likely obsoleting additional APIs from there once these are built-in. This is not meant to be an exhaustive list so please do chime in if I missed anything here 😄

@SkyeHoefling
Copy link
Contributor Author

I believe this was discussed in the MAUI Community Toolkit when it was the Xamarin Community Toolkit. They have their own implementation of AddRange for ObservableCollection as it wasn't included in runtime. Once we get this merged that code can be removed from the toolkit. @brminnick do you know if we had an issue documented for that?

@SkyeHoefling
Copy link
Contributor Author

SkyeHoefling commented Feb 10, 2022

Do any of the build artifacts provide a ready to use sdk

I spent some time today working with @snickler today and we were able to get the sdk to compile WPF with the code in this PR. This allows me to create a console app as well, so I am going to create benchmarks and attach them to the main description to provide a more in-depth analysis of the performance impact this PR has.

@Sergio0694
Copy link
Contributor

"They have their own implementation of AddRange for ObservableCollection as it wasn't included in runtime. Once we get this merged that code can be removed from the toolkit."

Yup, this was my understanding as well. It's also why so far I didn't want to port them over or implement equivalent APIs in the MVVM Toolkit either, as I wanted to instead do this right and just wait for proper BCL support, so that users would've had a single (built-in) API to use anywhere, instead of needing external helpers just to work around specific framework limitations. That's also why I'm really happy to see this being picked up again, and really hope it'll make it this time around 😄

@SkyeHoefling
Copy link
Contributor Author

I tried creating a benchmark project and wasn't able to get it working. Looks like BenchmarkDotNet doesn't like the custom FrameworkReference in the .csproj file. Does anyone have advice on how to run benchmarks on a custom build of the runtime?

csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>win10-x86</RuntimeIdentifier>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Update="Microsoft.NETCore.App" RuntimeFrameworkVersion="7.0.0-dev" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
    <PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.1" />
  </ItemGroup>
	
</Project>

Error message when running the benchmark application

// Build Error: Standard output:

 Standard error:
 D:\RangeNet7Benchmarks\bin\release\net7.0\win-x64\0ff95f23-1884-4ee9-8dfb-a4a8b8e03c1a\BenchmarkDotNet.Autogenerated.csproj(21,25): error MSB4066: The attribute "Update" in element <FrameworkReference> is unrecognized.

Some Things I've Tried

Standard publish

dotnet publish -c release -r win-x64 --self-contained

dotnet build/run

dotnet build -c release -r win-x64 --no-self-contained
dotnet run -c release -r win-x64 --no-self-contained

Removing <FrameworkReference> from the csproj. The generated file added it back in, which was odd

@eiriktsarpalis
Copy link
Member

@ahoefling you should try reusing the infrastructure in dotnet/performance, see https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/README.md#private-runtime-builds for instructions.

@SkyeHoefling
Copy link
Contributor Author

Thanks @eiriktsarpalis for pointing me in the right direction. I was able to run benchmarks on all the new APIs and compared them to the old APIs. The performance and allocation improvements are significant.

Below are 2 sets of benchmarks to demonstrate performance improvement and allocation improvements on a small dataset of 512 records and a large dataset of 262144 records. It appears that all the new range APIs are providing significant performance improvements and allocation improvements.

Size: 512

BenchmarkDotNet=v0.13.1.1694-nightly, OS=Windows 10 (10.0.19043.1466/21H1/May2021Update)
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=7.0.100-dev
  [Host]     : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
  Job-WEEQYH : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog  Toolchain=CoreRun
IterationTime=250.0000 ms  MaxIterationCount=20  MinIterationCount=15
WarmupCount=1

|                            Method | Size |       Mean |     Error |    StdDev |     Median |        Min |        Max |  Gen 0 |  Gen 1 | Allocated |
|---------------------------------- |----- |-----------:|----------:|----------:|-----------:|-----------:|-----------:|-------:|-------:|----------:|
|          ObservableCollection_Add |  512 |  13.312 us | 0.2078 us | 0.1943 us |  13.216 us |  13.139 us |  13.834 us | 5.3797 | 0.1582 |  44.24 KB |
|     ObservableCollection_AddRange |  512 |   7.043 us | 0.0797 us | 0.0706 us |   7.031 us |   6.941 us |   7.199 us | 1.0054 | 0.0279 |   8.34 KB |
|       ObservableCollection_Insert |  512 |  13.729 us | 0.2636 us | 0.2707 us |  13.604 us |  13.488 us |  14.310 us | 5.3926 | 0.1618 |  44.24 KB |
|  ObservableCollection_InsertRange |  512 |   6.986 us | 0.0343 us | 0.0268 us |   6.980 us |   6.958 us |   7.053 us | 1.0141 | 0.0290 |   8.34 KB |
|       ObservableCollection_Remove |  512 | 117.300 us | 0.6794 us | 0.6355 us | 117.100 us | 116.313 us | 118.329 us | 2.3321 |      - |  22.11 KB |
|  ObservableCollection_RemoveRange |  512 |  33.449 us | 0.2618 us | 0.2449 us |  33.412 us |  33.094 us |  33.839 us | 0.9328 |      - |    8.2 KB |
|      ObservableCollection_Replace |  512 |  93.224 us | 0.8643 us | 0.8085 us |  92.844 us |  92.183 us |  94.654 us | 9.2456 |      - |  76.11 KB |
| ObservableCollection_ReplaceRange |  512 |  39.413 us | 0.1830 us | 0.1712 us |  39.404 us |  39.126 us |  39.677 us | 0.9422 |      - |   8.26 KB |

Size: 262144

BenchmarkDotNet=v0.13.1.1694-nightly, OS=Windows 10 (10.0.19043.1466/21H1/May2021Update)
AMD Ryzen 9 3950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=7.0.100-dev
  [Host]     : .NET 7.0.0 (7.0.22.10302), X64 RyuJIT
  Job-RCJSOK : .NET 7.0.0 (42.42.42.42424), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  Arguments=/p:DebugType=portable,-bl:benchmarkdotnet.binlog  Toolchain=CoreRun
IterationTime=250.0000 ms  MaxIterationCount=20  MinIterationCount=15
WarmupCount=1

|                            Method |   Size |          Mean |       Error |      StdDev |        Median |           Min |           Max |     Gen 0 |    Gen 1 |    Gen 2 | Allocated |
|---------------------------------- |------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|----------:|---------:|---------:|----------:|
|          ObservableCollection_Add | 262144 |     11.401 ms |   0.1276 ms |   0.1194 ms |     11.424 ms |     11.091 ms |     11.530 ms | 2478.2609 | 217.3913 | 217.3913 |     22 MB |
|     ObservableCollection_AddRange | 262144 |      4.008 ms |   0.0324 ms |   0.0270 ms |      4.008 ms |      3.967 ms |      4.062 ms |  203.1250 | 187.5000 | 187.5000 |      4 MB |
|       ObservableCollection_Insert | 262144 |     11.696 ms |   0.1085 ms |   0.1015 ms |     11.695 ms |     11.489 ms |     11.902 ms | 2476.1905 | 238.0952 | 238.0952 |     22 MB |
|  ObservableCollection_InsertRange | 262144 |      4.068 ms |   0.0345 ms |   0.0322 ms |      4.069 ms |      4.010 ms |      4.124 ms |  203.1250 | 187.5000 | 187.5000 |      4 MB |
|       ObservableCollection_Remove | 262144 | 50,134.391 ms | 359.8109 ms | 318.9630 ms | 50,142.713 ms | 49,714.222 ms | 50,671.855 ms | 1000.0000 |        - |        - |     11 MB |
|  ObservableCollection_RemoveRange | 262144 |  6,213.556 ms |  30.0685 ms |  28.1261 ms |  6,215.856 ms |  6,180.528 ms |  6,269.518 ms |         - |        - |        - |      4 MB |
|      ObservableCollection_Replace | 262144 | 12,666.790 ms | 114.1951 ms | 106.8181 ms | 12,713.632 ms | 12,500.733 ms | 12,801.275 ms | 4000.0000 |        - |        - |     38 MB |
| ObservableCollection_ReplaceRange | 262144 |  6,217.794 ms |  20.2580 ms |  18.9494 ms |  6,214.457 ms |  6,183.521 ms |  6,250.623 ms |         - |        - |        - |      4 MB |
Benchmarking Methodology and Code

I created the benchmarks using the https://github.com/dotnet/performance project and ran it against my locally compiled x64 version of .NET 7 runtime. Most of the APIs had an equivelant old API with the exception of Replace/ReplaceRange. There is no `Replace` API so I had to add additional code to simulate this behavior. This may skew the results for that particular benchmark

[Benchmark]
public ObservableCollection<T> ObservableCollection_Add()
{
    var collection = new ObservableCollection<T>();
    foreach (T value in _uniqueValues)
    {
        collection.Add(value);
    }
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_AddRange()
{
    var collection = new ObservableCollection<T>();
    collection.AddRange(_uniqueValues);
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_Insert()
{
    var collection = new ObservableCollection<T>();
    for (int i = 0; i < _uniqueValues.Length; i++)
    {
        collection.Insert(i, _uniqueValues[i]);
    }
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_InsertRange()
{
    var collection = new ObservableCollection<T>();
    collection.InsertRange(0, _uniqueValues);
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_Remove()
{
    var collection = new ObservableCollection<T>(_uniqueValues);
    for (int i = 0; i < collection.Count; i++)
    {
        collection.Remove(collection[i]);
    }
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_RemoveRange()
{
    var collection = new ObservableCollection<T>(_uniqueValues);
    collection.RemoveRange(0, collection.Count);
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_Replace()
{
    var collection = new ObservableCollection<T>(_uniqueValues);
    for (int i = 0; i < collection.Count; i++)
    {
        collection.RemoveAt(i);
        collection.Insert(i, _uniqueValues[i]);
    }
    return collection;
}

[Benchmark]
public ObservableCollection<T> ObservableCollection_ReplaceRange()
{
    var collection = new ObservableCollection<T>(_uniqueValues);
    collection.ReplaceRange(0, collection.Count, _uniqueValues);
    return collection;
}

@ghost ghost closed this Mar 13, 2022
@ghost
Copy link

ghost commented Mar 13, 2022

Draft Pull Request was automatically closed for inactivity. Please let us know if you'd like to reopen it.

@SkyeHoefling
Copy link
Contributor Author

Is there a way we can keep this open as a draft as things are going to take a little bit of time while I balance work commitments and finishing up the WPF work? I hope the performance numbers I posted help convince everyone else how important this PR is to overall runtime performance for ObservableCollection<T>.

@Sergio0694
Copy link
Contributor

@eiriktsarpalis could you help reopen this? 🙂

@ahoefling is there any help you need from the teams with the other PRs you're doing for WPF etc. or is it just a matter of finding the time to finish them?

I'll also see if I can ask WinUI 3 folks internally about this (like WPF, they'll need to support the new events in the built-in controls or things will fall apart).

We would really like to see this change to make it to .NET 7 to support it in the MVVM Toolkit (which is also the reference MVVM implementation for MAUI at this point), as we're explicitly not wanting to add polyfills or workarounds for this and just do it properly once this is merged. Will also need to ask about MAUI support for this but I assume that shouldn't be a problem.

There's still plenty of time (not too much, but a fair amount), so I'm cautiously optimistic. This would be a great feature to finally have 😄

@eiriktsarpalis
Copy link
Member

Thanks for the feedback, we'll see if we can update the automation so fair warning is provided before drafts get auto-closed.

@ghost
Copy link

ghost commented Jun 18, 2022

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@Sergio0694
Copy link
Contributor

Starting to lose hope here as we're cutting it close at this point — @ahoefling what's the status on your PR, is there any remaining work on your end and do you need help? @PureWeen the MAUI team has added support for this to the .NET 7 milestone, is that correct? And is that still on track? And does anyone have an update on the WPF side, @ahoefling were you also going to contribute support for this there? Were there any blockers on that front?

Asking here and not internally given all of those projects are open source, so wanted Andrew and other folks to be able to see follow ups here if possible 🙂

@Symbai
Copy link

Symbai commented Jun 19, 2022

And does anyone have an update on the WPF side

If you would have taken just a quick look at the WPF repo, you would immediately see that there is no chance for anything else than bug fixes. Not even complete community PR's are getting reviewed. dotnet/wpf#6542 dotnet/wpf#6556 (comment) dotnet/wpf#6171 (comment) WPF community is more than frustrated about WPF team reject anything else than bug fixes since it has been open sourced. If this HERE is waiting for WPF then it has no chance at all as long as the WPF team remains the same.

@eiriktsarpalis
Copy link
Member

When I reached out to the WPF team they confirmed they will not have the bandwidth to work in this in .NET 7. Effectively, the situation remains the same since this PR was last reverted.

FWIW, I tend to agree with @legistek's analysis in this and other threads: fundamentally, this is changing the contract for observable collections and has the potential to break any observer depending on the current behavior, not just WPF.

@Sergio0694
Copy link
Contributor

I would argue this does not break WPF in of itself. The collection changed event args already support ranged operations, so WPF is already broken with any custom observable collection raising such an event (it's just people never bothered to implement them since WPF doesn't support them). Even with this API in, it's not like WPF apps upgrading to .NET 7 would break, they would only break if new code targeting .NET 7 then called the new ranged APIs. But I don't see that as being conceptually that different than, as I mentioned, someone just implementing a custom collection and raising a the collection changed event with multiple items (which has been possible since .NET 1, basically). That said, I can understand the WPF team not having bandwidth.

I will say though it's disappointing that we're still stuck with this after 6 years (since the issue, otherwise after way more).
I'm just really surprised at how little interest there seems to be for this I suppose. Let's try again in .NET 8 I guess. 😣

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jun 20, 2022

The collection changed event args already support ranged operations, so WPF is already broken with any custom observable collection raising such an event

I would guess it's yet another manifestation of Hyrum's Law. Wouldn't be surprised if other components in the ecosystem exhibited the same problem.

Even with this API in, it's not like WPF apps upgrading to .NET 7 would break, they would only break if new code targeting .NET 7 then called the new ranged APIs.

I'm not particularly knowledgeable about WPF, but my recollection is that it broke in a substantial way as soon as runtime merged this change originally.

someone just implementing a custom collection and raising a the collection changed event with multiple items

Would doing that be a less disruptive way for newer frameworks to get range support?

@Sergio0694
Copy link
Contributor

"Wouldn't be surprised if other components in the ecosystem exhibited the same problem."

I agree this is very likely not the only one, it's just one that keeps popping up quite often. It's been a pain point for years (first on WPF, then on UWP, now on both MAUI and WinUI 3, etc.), and something that has received countless feature requests for. The solution many have come up with is to just roll their own collection types with support for "range operations", with the big drawback being though that since no UI framework among these actually supports the API, they're just forced to either iterate over items one by one (which is just terrible for performance), or just reset the collection and reinitialize it (which is also bad for performance, maybe just a little bit less, and also completely breaks all animations). It's just a very unfortunate situation where customers get frustrated, and they often have to both do extra work to build these helpers, and then still end up with a suboptimal experience. I mean, we have the same exact custom collection type in the Microsoft Store too, and it has the same exact issues (and I hate it). It's just a really common issue, and I'm saying I'd love for us to find the time to coordinate with the right folks and maybe try to finally get this resolved at least for .NET 8 🙂

I've also received multiple requests to add such a custom collection type in the .NET Community Toolkit, and I've had to reject them for the reasons mentioned above (it still results in a very bad experience, and I'd much rather just fix this properly in the BCL). The net result though is that at least for now, the API is not there and plenty of folks are frustrated.

"but my recollection is that it broke in a substantial way as soon as runtime merged this change originally."

I find that surprising and I'd love to learn more about that. Even with the new APIs, as long as nobody actually calls them, there should be no functional differences in the way the currently existing APIs work at all. So I'm not sure I understand what broke. Are you sure it wasn't just folks trying out the new APIs, and then reporting WPF crashing/breaking in that scenario? 🤔

"Would doing that be a less disruptive way for newer frameworks to get range support?"

I personally don't think that'd be the correct solution for this issue, for a number of reasons:

  • The core issue is that right now, the various UI frameworks don't support multiple collection updates. Where they come from is not what's causing the issue. That is, even with a new collection type, you'd get the same exact crashes until the frameworks were updated to handle the args. And if they were updated, then there would be no reason to have a separate collection type in the first place, as it'd just be the same exact one but with two new APIs. Those should just be added on what we have.
  • It'd make updating codebases much more tedious, as now you'd have to swap the whole collection type in all locations rather than just being able to opt-in into a range update whenever necessary.
  • I'd argue it'd be an abstraction leak. You want an observable collection, the fact that some callsites would need to perform range operations on that is just an implementation detail of your viewmodels. You shouldn't need to use a completely different collection type just to support this.

I would really love for us to just manage to coordinate across the various teams and officially add support for this in a future release, not sooner than .NET 8 at this point. We could both update all underlying frameworks to support this (which I realize is non trivial work, especially since eg. WinUI 3 would likely need some new WinRT APIs and projections for this), and then provide the necessary migration docs, if needed. I believe this would be the best long term solution for this issue 🙂

@legistek
Copy link

legistek commented Jun 20, 2022

@Sergio0694:

Even with this API in, it's not like WPF apps upgrading to .NET 7 would break, they would only break if new code targeting .NET 7 then called the new ranged APIs.

I initially assumed this as well. But after digging very deep into the issue I learned the issue was extension methods. Many people (including myself) took matters into their own hands over the years and defined their own extension methods such as:

public static void AddRange<T>(this ICollection<T> dest, IEnumerable<T> items) 
{
   foreach (var item in items)
       dest.Add(item);
}

These were WPF friendly because they didn't use the INCC modes that break WPF.

However, when the new AddRange was added to the .NET Core preview, and specifically for ObservableCollection, the compiler silently preferred the new instance methods over peoples' static extension methods. This then indeed caused their WPF apps to break because the new range-supporting methods were inadvertently being called. Worse, the breakage could not be detected at compile time, only at run time.

So you see, there truly is no way to make these changes to ObservableCollection without a strong possibility of breaking many, many applications choosing to target .NET7 if they're not aware of this issue and what they have to do to avoid it. (Namely, rename their extension methods before retargeting).

But another solution exists that would not break any existing code 100% guaranteed. Just make a new class entirely - ObservableList. It would be very easy to refactor everything in this PR to make a new class, and get all the benefits of it, but in a purely opt-IN capacity. Even if people have their own ObservableLists already, at least those conflicts will be detected at compile time and people can choose whether to use the new one or their own. And focus on updating the new frameworks - WinUI and MAUI - to support this, and let the old ones continue to use OC.

You could still do this in .NET7 I reckon.

@Sergio0694
Copy link
Contributor

I see, so the issue is just when recompiling from source. What I meant is that existing binaries just running on .NET 7 would not be affected, which would still be the case (eg. if you were referencing some NuGet package, that would be fine). If the issue is this specific case, that's something that could be called out in a migration guide for this. Or, we could very well include an analyzer (that is, issue a warning if the new API is called from a callsite where an extension would've been in scope as well), if we decided this was worth it. I absolutely disagree with the conclusion that this is a valid reason to not implement this.

"You could still do this in .NET7 I reckon."

There's absolutely no way to get a new API like that proposed, reviewed, approved and implemented in .NET 7.
And that's assuming doing something like that was the right way to go, which I don't agree with at all.

@legistek
Copy link

legistek commented Jun 20, 2022

Correct nothing would break at the binary level.

I absolutely disagree with the conclusion that this is a valid reason to not implement this.

You may be right, but TPTB back when this all went down were faced with the same facts and decided to revert it. Part of it may have been being pressed for time as it was discovered late in the .NET Core 3 preview cycle IIRC.

You're right, a migration guide or analyzer could help. People retargeting their apps just need to be very keenly aware of this issue. Renaming their extension methods before migration should do the trick, but that's still putting the burden on them to find them all. (You have to figure most WPF apps have been around for quite awhile). This would be a lot more effort than your typical breaking change IMO, with a lot of opportunities to miss offending code.

The most important thing from my perspective is making sure folks like yourself understand that: (1) nothing has changed since this was originally done and reverted; (2) WPF is not going to be "fixed" to truly support ranges; the PM already essentially called this a nonstarter; and (3) the folks saying they have fixed WPF already to support this are incorrect; their proposals are just bandaids. So whatever you all do just please do so with accurate information and make sure we all are VERY AWARE of what you did so we can plan for it. Thanks!

@Symbai
Copy link

Symbai commented Jun 20, 2022

Renaming their extension methods before migration should do the trick

If that is what people have to do, to make this not a breaking change. Well then we could use that argument for literally everything that break existing code and just say "Devs have to change their code then before they can use it". You know that many people are using automatic builds? Adding it to the analyzer just generating a warning will not be seen. It has to generate an error instead forcing builds to break. But then again, it's a breaking change. If anyone want this feature so badly, I'd recommend to go ahead and create a PR on WPF repo. Chances are low but that is the only chance you have (unless breaking change will be accepted).

@legistek
Copy link

On the topic of fixing the frameworks vs. changing the BCL, I think these need to be uncoupled because they're really not dependent on each other at all.

On changing the frameworks, everyone needs to accept that WPF isn't going to be changed. BUT the other frameworks could be and there's no reason they need to wait on anything in the BCL to add support for ranged INCC notifications, if for no other reason than to support individuals' own implementations.

On the BCL end, whether the frameworks are changed still has nothing to do with whether you're risking people accidentally invoking new methods when they didn't intend do. See my example above with the null range; it has nothing to do with WPF or any other framework.

A new class does the job with zero risk. No breaking code, no analyzers, or migration guides. As someone who runs a company that sells software to real people for actual money, my biggest concern in any update is not introducing new bugs. It tends to make customers upset. So while I care about things like abstraction paradigms and how tedious it might be to opt-into new functionality, I'd much rather sacrifice elegance and spend a little extra effort in favor of a guarantee that I won't be introducing unintended and difficult to detect side effects. Practical concerns that impact real commercial applications need to come first; otherwise, what's the point of any of this?

@legistek
Copy link

legistek commented Jun 21, 2022

Ok one more comment, sorry, and also about to contradict myself.

I could see scenarios where you would want to share viewmodels between WPF and WinUI or MAUI. If you do have a new class, you won't be able to use it with WPF, so sharing would then not be possible.

The same problem arises if you change OC though.

So here's a thought -

What if you kept this PR mostly as is but added public static bool UseRangedNotifications { get; set; } to ObservableCollection? Default to true. Then the AddRange and RemoveRange members of OC would check for this and choose between ranged INCC notifications or iterating to make individual ones.

Then in the WPF repo - and any other framework that doesn't (yet) support this - maybe in a static constructor for Application or Window or some other library startup code, you make sure to set this to false.

Then - I think, possibly, maybe - you could make this change and not break WPF, even if extension methods got overridden.

@eiriktsarpalis
Copy link
Member

Sounds like an interesting workaround. Could you share it #18087 just so that it gains more traction and feedback?

If I'm honest, it's unlikely we'd get to this for .NET 7, we're fairly late in development and there needs to be another round of API review and documenting the breaking change/workaround.

@legistek
Copy link

Good deal @eiriktsarpalis. I would love to post the idea on #18087 but unfortunately the individual who opened that issue (who doesn't appear to be affiliated with Microsoft) seems to have disliked my prior comments on that issue and blocked me personally which prevents me from commenting further on that or any issues he started.

@eiriktsarpalis
Copy link
Member

Well, that sucks. We'll see if there's a way to have your voice represented in that issue (worst case scenario we'll create a new one).

@legistek
Copy link

lol, well, there are far worse things in life. :) But thanks for throwing it out there!

@robertmclaws
Copy link

@legistek Apologies, I don't remember blocking you, but sorry if I did. I think your workaround has a lot of merit.

@legistek
Copy link

Cheers. Just happy to contribute what I can toward a solution to what has been a complex and vexing problem for a long time.

@SkyeHoefling
Copy link
Contributor Author

@Sergio0694 answering your question earlier about an update as you mentioned you spoke with @PureWeen from .NET MAUI. I've had very poor mental health this year and have had some life events that prevented me from getting to the WPF code. I had all intentions of doing it but at this point I really don't see myself finding the time or energy to bring this over the finish line for .NET 7. I do hope someone can volunteer their time to finish the WPF code. I'm sorry 😔

@mattleibow
Copy link
Member

I like the observable list here. Time to stop doing bad things in view models.

@robertmclaws
Copy link

I would ask for your apologies for the rant you’re about to read, but after 6 years and all the patience I can muster, I believe I’ve earned it.

The original issue and PR for this bug is about to celebrate it’s SEVENTH BIRTHDAY. Over that time Skye and I have expended so much energy trying to get this over the finish line. It is incredibly disappointing that there are so many at Microsoft continuing to get in the way of getting this done.

.NET Core was sold to us developers as a way to break from the past, and major releases were going to be places where breaking changes could be made. Yet for 6 years I’ve continued to hear excuse after excuse. To quote Yoda, “always with you it cannot be done.”

Had this been fixed with .NET Core 2 when I submitted the first PR, MAUI and the other platforms would have dealt with it when they were being built, and WPF would have been fixed when it was being migrated. But it was punted to .NET Core 3.

Then, after the PR was already accepted and integrated, I was told WPF was already late and had too many bugs to be able to get the work done in time for the .NET Core 3 release. Our work was held up because the WPF team over-promised and couldn’t get its act together. Victory was ripped away from us at the last possible second.

In the time since, instead of guiding people building new frameworks like MAUI towards making decisions that would allow this work to get done more easily, Microsoft finds time to invest in things like “Minimal APIs” that only marginally impact developers.

So you’ll have to forgive me if, after missing it in .NET Core 2, 3, 5, 6, AND 7, that maybe I don’t believe we’ll see it in .NET 8.

It’s cool though. Maybe we’ll see this get fixed by the time .NET 12 rolls around. In the meantime, I’ll just continue to wonder how anyone EVER let WPF get past a code review with exceptions being thrown in event handlers, and why it seems like the same people are being allowed to hold back amazing frameworks like Blazor from taking advantage of these features.

@SkyeHoefling thank you so much for your efforts. I can only imagine the emotional toll that having your status as “.NET contributor” ripped away year after year after effing year has had on you. It sucks having so many people stand in the way of something so obviously broken.

@dotMorten
Copy link

I don't get the worry here that multiple frameworks doesn't support multi item changes. This PR has nothing to do with this. INotifyCollectionChanged is the interface used for multi item changes and it already supports it and has since it was first designed. Any framework that listens to the event this interface declares could get multi item changes - including WPF - and WPF has never supported it but that doesn't mean the interface wasn't shipped. This PR doesn't create a new limitation in WPF.

Sure this PR makes it easier to raise multi item changes on a commonly used collection, but it doesn't in any way expose an issue that wasn’t already there. It merely implements the existing interface to its fullest. Holding up this PR because WPF has a problem that it always had and we’ve complained about for years, when this change can create huge performance gains for other UI frameworks is frankly incredibly disappointing. I guess we’ll just have to continue with using our own observable collection implementations.

@arknu
Copy link

arknu commented Jul 3, 2022

Absolutely agree with @dotMorten here. WPF already has a bug, that shouldn't hold back improvements to the BCL. And ObservableCollection badly needs this API.

The correct thing to do here is to fix WPF. And given that WPF is also a Microsoft project, this shouldn't be an insurmountable problem. WPF is already broken as things are.

I thought the whole point of the new .NET was to be able to more easily do these kinds of potentially breaking changes. If this breaks another part of .NET, just go fix that part as well.

@dotMorten
Copy link

dotMorten commented Jul 3, 2022

What part of WinUI 3 doesn't support this? Works for me? Judging from animation it correctly animated two adds at a time.
image

Update: insert does have issues and only detects the first element in WinUI3

@SkyeHoefling
Copy link
Contributor Author

I just rebased this branch on the latest changes in the main branch. Code is now up to date, there were no conflicts.

@eiriktsarpalis
Copy link
Member

Thanks @SkyeHoefling. Let's revisit an implementation once the updated proposal has been reviewed. We're fairly late in the development of .NET 7, so even if it does get approved, we'd likely push this to .NET 8 in order to give libraries adequate reaction time.

@robertmclaws
Copy link

robertmclaws commented Jul 4, 2022

Just an FYI to everyone on this thread, I believe I have discovered an issue with the updated proposal and have left comments on that thread.

The short version is that we should be using SemVer + conditional NuGet package references + breaking changes documentation to solve the problem, not allowing 3rd party libraries to set a flag that silently overrides how the entire .NET runtime works simply by installing an offending NuGet package.

If I go through all the effort to fix my libraries, some lazy developer should not be able to silently override my work or change the behavior or my customer's apps. THEIR code should break, not mine. The proposed fix just creates an equal-and-opposite problem.

To address this comment:

@Sergio0694:

However, when the new AddRange was added to the .NET Core preview, and specifically for ObservableCollection, the compiler silently preferred the new instance methods over peoples' static extension methods. This then indeed caused their WPF apps to break because the new range-supporting methods were inadvertently being called. Worse, the breakage could not be detected at compile time, only at run time.

The new range-supporting methods are not being "inadvertently" called, this is EXACTLY how .NET should behave. Notification here can be easily achieved with built-in Roslyn analyzers and/or Trace.Warn messages, in addition to "breaking changes" documentation for the .NET 8.0 release.

If we're going to punt this AGAIN, there is plenty of time to build the needed Roslyn analyzer. We should get THAT spec'd out and planned so that some non-Microsoft developer can contribute it while .NET 7 is being RTM'd.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 3, 2022
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Collections community-contribution Indicates that the PR has been added by a community member new-api-needs-documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Collection<T> and ObservableCollection<T> do not support ranges
10 participants