Skip to content

Commit 3f674f2

Browse files
committed
Split ObservableListEx.cs partials into one file per operator (overload set)
Mirrors the same convention applied to ObservableCacheEx (PR #1095): 1. ONE FILE PER OPERATOR NAME (one overload set per file). The previous 17 family files are replaced with 63 per-operator partial files. 2. BARE ObservableListEx.cs FILE restored to carry the canonical class-level XML documentation. All partials carry the same canonical class summary ('Extensions for ObservableList.') so SA1601 is satisfied and there are no divergent per-file class docs. 3. PRIVATE HELPERS placed AFTER all public members within their containing file. The 5 private 'Combine' overloads (used by And, Except, Or, Xor) are placed at the bottom of And.cs (alphabetically first caller). The byte content of every method body is preserved (verified programmatically).
1 parent d6c0391 commit 3f674f2

72 files changed

Lines changed: 3558 additions & 2384 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/DynamicData/List/ObservableListEx.Adapt.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
22
// Roland Pheasant licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for full license information.
44

@@ -19,7 +19,7 @@
1919
namespace DynamicData;
2020

2121
/// <summary>
22-
/// ObservableList extensions for Adapt.
22+
/// Extensions for ObservableList.
2323
/// </summary>
2424
public static partial class ObservableListEx
2525
{
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
2+
// Roland Pheasant licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Collections.ObjectModel;
6+
using System.ComponentModel;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq.Expressions;
9+
using System.Reactive;
10+
using System.Reactive.Concurrency;
11+
using System.Reactive.Disposables;
12+
using System.Reactive.Linq;
13+
using DynamicData.Binding;
14+
using DynamicData.Cache.Internal;
15+
using DynamicData.List.Internal;
16+
using DynamicData.List.Linq;
17+
18+
// ReSharper disable once CheckNamespace
19+
namespace DynamicData;
20+
21+
/// <summary>
22+
/// Extensions for ObservableList.
23+
/// </summary>
24+
public static partial class ObservableListEx
25+
{
26+
/// <summary>
27+
/// Adds a key to each item in a list changeset, converting it to a cache changeset that supports all keyed DynamicData operators.
28+
/// </summary>
29+
/// <typeparam name="TObject">The type of items in the list.</typeparam>
30+
/// <typeparam name="TKey">The type of the key.</typeparam>
31+
/// <param name="source">The source <see cref="IObservable{IChangeSet{TObject}}"/> to add keys to, converting to a cache changeset.</param>
32+
/// <param name="keySelector">A <see cref="Func{T, TResult}"/> function to extract a unique key from each item.</param>
33+
/// <returns>A cache <see cref="IObservable{IChangeSet{TObject, TKey}}"/> changeset stream with keyed items.</returns>
34+
/// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="keySelector"/> is <see langword="null"/>.</exception>
35+
/// <remarks>
36+
/// <para>
37+
/// All index information is dropped during conversion because cache changesets are unordered by default.
38+
/// Use this when you need to transition from list-based pipelines to cache-based operators (Filter by key, Join, Group, etc.).
39+
/// </para>
40+
/// </remarks>
41+
/// <seealso cref="ObservableCacheEx.RemoveKey{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}})"/>
42+
public static IObservable<IChangeSet<TObject, TKey>> AddKey<TObject, TKey>(this IObservable<IChangeSet<TObject>> source, Func<TObject, TKey> keySelector)
43+
where TObject : notnull
44+
where TKey : notnull
45+
{
46+
source.ThrowArgumentNullExceptionIfNull(nameof(source));
47+
keySelector.ThrowArgumentNullExceptionIfNull(nameof(keySelector));
48+
49+
return source.Select(changes => new ChangeSet<TObject, TKey>(new AddKeyEnumerator<TObject, TKey>(changes, keySelector)));
50+
}
51+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
2+
// Roland Pheasant licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Collections.ObjectModel;
6+
using System.ComponentModel;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq.Expressions;
9+
using System.Reactive;
10+
using System.Reactive.Concurrency;
11+
using System.Reactive.Disposables;
12+
using System.Reactive.Linq;
13+
using DynamicData.Binding;
14+
using DynamicData.Cache.Internal;
15+
using DynamicData.List.Internal;
16+
using DynamicData.List.Linq;
17+
18+
// ReSharper disable once CheckNamespace
19+
namespace DynamicData;
20+
21+
/// <summary>
22+
/// Extensions for ObservableList.
23+
/// </summary>
24+
public static partial class ObservableListEx
25+
{
26+
/// <summary>
27+
/// Applies a logical AND (intersection) between multiple list changeset streams.
28+
/// Only items present in ALL sources appear in the result.
29+
/// </summary>
30+
/// <typeparam name="T">The type of items in the lists.</typeparam>
31+
/// <param name="source">The first source <see cref="IObservable{IChangeSet{T}}"/> to intersect.</param>
32+
/// <param name="others">The additional <see cref="IObservable{IChangeSet{T}}"/> changeset streams to intersect with.</param>
33+
/// <returns>A list changeset stream containing items that exist in every source.</returns>
34+
/// <exception cref="ArgumentNullException"><paramref name="others"/> is <see langword="null"/>.</exception>
35+
/// <remarks>
36+
/// <para>
37+
/// Uses reference counting per item across all sources. An item appears downstream only when
38+
/// its reference count is non-zero in ALL sources. Item identity is determined by the default equality comparer.
39+
/// </para>
40+
/// <list type="table">
41+
/// <listheader><term>Event</term><description>Behavior</description></listheader>
42+
/// <item><term>Add/AddRange</term><description>The item's reference count is incremented in its source tracker. If the item is now present in all sources, an <b>Add</b> is emitted.</description></item>
43+
/// <item><term>Replace</term><description>The old item's reference count is decremented and the new item's is incremented. Depending on whether each is present in ALL sources, this emits an <b>Add</b>, <b>Remove</b>, <b>Replace</b>, or nothing.</description></item>
44+
/// <item><term>Remove/RemoveRange/Clear</term><description>The item's reference count is decremented. If it was in the result and is no longer in all sources, a <b>Remove</b> is emitted.</description></item>
45+
/// <item><term>Refresh</term><description>Forwarded as <b>Refresh</b> if the item is currently in the result.</description></item>
46+
/// <item><term>Moved</term><description>Ignored (set operations are position-independent).</description></item>
47+
/// </list>
48+
/// <para><b>Worth noting:</b> Item identity uses object equality, not position. Duplicate items in a single source are reference-counted independently.</para>
49+
/// </remarks>
50+
/// <seealso cref="Or{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
51+
/// <seealso cref="Except{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
52+
/// <seealso cref="Xor{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
53+
/// <seealso cref="ObservableCacheEx.And{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, IObservable{IChangeSet{TObject, TKey}}[])"/>
54+
public static IObservable<IChangeSet<T>> And<T>(this IObservable<IChangeSet<T>> source, params IObservable<IChangeSet<T>>[] others)
55+
where T : notnull
56+
{
57+
others.ThrowArgumentNullExceptionIfNull(nameof(others));
58+
59+
return source.Combine(CombineOperator.And, others);
60+
}
61+
62+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
63+
/// <param name="sources">A <see cref="ICollection{T}"/> of changeset streams to intersect.</param>
64+
/// <remarks>
65+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
66+
/// <para>This overload accepts a pre-built collection of sources instead of a params array.</para>
67+
/// </remarks>
68+
public static IObservable<IChangeSet<T>> And<T>(this ICollection<IObservable<IChangeSet<T>>> sources)
69+
where T : notnull => sources.Combine(CombineOperator.And);
70+
71+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
72+
/// <param name="sources">An <see cref="IObservableList{T}"/> of changeset streams. Sources can be added or removed dynamically.</param>
73+
/// <remarks>
74+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
75+
/// <para>This overload supports dynamic source management: adding or removing changeset streams from the observable list triggers re-evaluation.</para>
76+
/// </remarks>
77+
public static IObservable<IChangeSet<T>> And<T>(this IObservableList<IObservable<IChangeSet<T>>> sources)
78+
where T : notnull => sources.Combine(CombineOperator.And);
79+
80+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
81+
/// <param name="sources">An <see cref="IObservableList{IObservableList{T}}"/> of <see cref="IObservableList{IObservableList{T}}"/>. Each inner list's changes are connected automatically.</param>
82+
/// <remarks>
83+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
84+
/// <para>This overload accepts <see cref="IObservableList{T}"/> instances directly, calling <c>Connect()</c> internally.</para>
85+
/// </remarks>
86+
public static IObservable<IChangeSet<T>> And<T>(this IObservableList<IObservableList<T>> sources)
87+
where T : notnull => sources.Combine(CombineOperator.And);
88+
89+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
90+
/// <param name="sources">An <see cref="IObservableList{ISourceList{T}}"/> of <see cref="ISourceList{T}"/>. Each inner list's changes are connected automatically.</param>
91+
/// <remarks>
92+
/// <inheritdoc cref="And{T}(IObservable{IChangeSet{T}}, IObservable{IChangeSet{T}}[])"/>
93+
/// <para>This overload accepts <see cref="ISourceList{T}"/> instances directly, calling <c>Connect()</c> internally.</para>
94+
/// </remarks>
95+
public static IObservable<IChangeSet<T>> And<T>(this IObservableList<ISourceList<T>> sources)
96+
where T : notnull => sources.Combine(CombineOperator.And);
97+
98+
private static IObservable<IChangeSet<T>> Combine<T>(this ICollection<IObservable<IChangeSet<T>>> sources, CombineOperator type)
99+
where T : notnull
100+
{
101+
sources.ThrowArgumentNullExceptionIfNull(nameof(sources));
102+
103+
return new Combiner<T>(sources, type).Run();
104+
}
105+
106+
private static IObservable<IChangeSet<T>> Combine<T>(this IObservable<IChangeSet<T>> source, CombineOperator type, params IObservable<IChangeSet<T>>[] others)
107+
where T : notnull
108+
{
109+
source.ThrowArgumentNullExceptionIfNull(nameof(source));
110+
others.ThrowArgumentNullExceptionIfNull(nameof(others));
111+
112+
if (others.Length == 0)
113+
{
114+
throw new ArgumentException("Must be at least one item to combine with", nameof(others));
115+
}
116+
117+
var items = source.EnumerateOne().Union(others).ToList();
118+
return new Combiner<T>(items, type).Run();
119+
}
120+
121+
private static IObservable<IChangeSet<T>> Combine<T>(this IObservableList<ISourceList<T>> sources, CombineOperator type)
122+
where T : notnull
123+
{
124+
sources.ThrowArgumentNullExceptionIfNull(nameof(sources));
125+
126+
return Observable.Create<IChangeSet<T>>(
127+
observer =>
128+
{
129+
var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList();
130+
var subscriber = changesSetList.Combine(type).SubscribeSafe(observer);
131+
return new CompositeDisposable(changesSetList, subscriber);
132+
});
133+
}
134+
135+
private static IObservable<IChangeSet<T>> Combine<T>(this IObservableList<IObservableList<T>> sources, CombineOperator type)
136+
where T : notnull
137+
{
138+
sources.ThrowArgumentNullExceptionIfNull(nameof(sources));
139+
140+
return Observable.Create<IChangeSet<T>>(
141+
observer =>
142+
{
143+
var changesSetList = sources.Connect().Transform(s => s.Connect()).AsObservableList();
144+
var subscriber = changesSetList.Combine(type).SubscribeSafe(observer);
145+
return new CompositeDisposable(changesSetList, subscriber);
146+
});
147+
}
148+
149+
private static IObservable<IChangeSet<T>> Combine<T>(this IObservableList<IObservable<IChangeSet<T>>> sources, CombineOperator type)
150+
where T : notnull
151+
{
152+
sources.ThrowArgumentNullExceptionIfNull(nameof(sources));
153+
154+
return new DynamicCombiner<T>(sources, type).Run();
155+
}
156+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
2+
// Roland Pheasant licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System.Collections.ObjectModel;
6+
using System.ComponentModel;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq.Expressions;
9+
using System.Reactive;
10+
using System.Reactive.Concurrency;
11+
using System.Reactive.Disposables;
12+
using System.Reactive.Linq;
13+
using DynamicData.Binding;
14+
using DynamicData.Cache.Internal;
15+
using DynamicData.List.Internal;
16+
using DynamicData.List.Linq;
17+
18+
// ReSharper disable once CheckNamespace
19+
namespace DynamicData;
20+
21+
/// <summary>
22+
/// Extensions for ObservableList.
23+
/// </summary>
24+
public static partial class ObservableListEx
25+
{
26+
/// <summary>
27+
/// Wraps a <see cref="ISourceList{T}"/> as a read-only <see cref="IObservableList{T}"/>, hiding mutation methods.
28+
/// </summary>
29+
/// <typeparam name="T">The type of items in the list.</typeparam>
30+
/// <param name="source">The <see cref="ISourceList{T}"/> mutable source list to wrap.</param>
31+
/// <returns>A read-only observable list that mirrors the source.</returns>
32+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
33+
public static IObservableList<T> AsObservableList<T>(this ISourceList<T> source)
34+
where T : notnull
35+
{
36+
source.ThrowArgumentNullExceptionIfNull(nameof(source));
37+
38+
return new AnonymousObservableList<T>(source);
39+
}
40+
41+
/// <summary>
42+
/// Materializes a changeset stream into a read-only <see cref="IObservableList{T}"/>.
43+
/// The list is kept in sync with the source stream for the lifetime of the subscription.
44+
/// </summary>
45+
/// <typeparam name="T">The type of items in the list.</typeparam>
46+
/// <param name="source">The source <see cref="IObservable{IChangeSet{T}}"/> to materialize into a read-only list.</param>
47+
/// <returns>A read-only observable list reflecting the current state of the stream.</returns>
48+
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
49+
/// <remarks>
50+
/// <para>
51+
/// This is the primary way to <b>multicast</b> a changeset pipeline. Materializing once into an <see cref="IObservableList{T}"/>,
52+
/// then calling <c>Connect()</c> on the result for each downstream consumer, ensures the upstream operators are evaluated only once
53+
/// regardless of how many subscribers consume the result.
54+
/// </para>
55+
/// </remarks>
56+
/// <seealso cref="AsObservableList{T}(ISourceList{T})"/>
57+
public static IObservableList<T> AsObservableList<T>(this IObservable<IChangeSet<T>> source)
58+
where T : notnull
59+
{
60+
source.ThrowArgumentNullExceptionIfNull(nameof(source));
61+
62+
return new AnonymousObservableList<T>(source);
63+
}
64+
}

src/DynamicData/List/ObservableListEx.AutoRefresh.cs

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
22
// Roland Pheasant licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for full license information.
44

@@ -19,7 +19,7 @@
1919
namespace DynamicData;
2020

2121
/// <summary>
22-
/// ObservableList extensions for AutoRefresh.
22+
/// Extensions for ObservableList.
2323
/// </summary>
2424
public static partial class ObservableListEx
2525
{
@@ -98,42 +98,4 @@ public static IObservable<IChangeSet<TObject>> AutoRefresh<TObject, TProperty>(t
9898
changeSetBuffer,
9999
scheduler);
100100
}
101-
102-
/// <summary>
103-
/// Monitors each item with a custom observable and emits <b>Refresh</b> changes whenever that observable fires,
104-
/// causing downstream operators (Filter, Sort, Group) to re-evaluate.
105-
/// </summary>
106-
/// <typeparam name="TObject">The type of items in the list.</typeparam>
107-
/// <typeparam name="TAny">The type emitted by the re-evaluator observable (value is ignored).</typeparam>
108-
/// <param name="source">The source <see cref="IObservable{IChangeSet{TObject}}"/> to monitor for observable-driven refresh signals.</param>
109-
/// <param name="reevaluator">A <see cref="Func{T, TResult}"/> factory that, given an item, returns an observable whose emissions trigger a <b>Refresh</b> for that item.</param>
110-
/// <param name="changeSetBuffer">An optional <see cref="TimeSpan"/> buffer duration to batch refresh signals into a single changeset.</param>
111-
/// <param name="scheduler">The <see cref="IScheduler"/> for buffering.</param>
112-
/// <returns>A list changeset stream with additional <b>Refresh</b> changes injected when per-item observables fire.</returns>
113-
/// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="reevaluator"/> is <see langword="null"/>.</exception>
114-
/// <remarks>
115-
/// <para>
116-
/// This is the general-purpose refresh mechanism. <see cref="AutoRefresh{TObject}(IObservable{IChangeSet{TObject}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
117-
/// is a convenience wrapper that uses <c>WhenAnyPropertyChanged()</c> as the re-evaluator.
118-
/// </para>
119-
/// <list type="table">
120-
/// <listheader><term>Event</term><description>Behavior</description></listheader>
121-
/// <item><term>Add/AddRange</term><description>Subscribes to the re-evaluator observable for each new item. The original change is forwarded.</description></item>
122-
/// <item><term>Replace</term><description>Unsubscribes from the old item's observable, subscribes to the new. The original change is forwarded.</description></item>
123-
/// <item><term>Remove/RemoveRange/Clear</term><description>Unsubscribes from removed items. The original change is forwarded.</description></item>
124-
/// <item><term>Moved/Refresh</term><description>Forwarded unchanged.</description></item>
125-
/// <item><term>Re-evaluator fires</term><description>The item's current index is looked up and a <b>Refresh</b> change is emitted.</description></item>
126-
/// </list>
127-
/// </remarks>
128-
/// <seealso cref="AutoRefresh{TObject}(IObservable{IChangeSet{TObject}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
129-
/// <seealso cref="SuppressRefresh{T}(IObservable{IChangeSet{T}})"/>
130-
/// <seealso cref="ObservableCacheEx.AutoRefreshOnObservable{TObject, TKey, TAny}(IObservable{IChangeSet{TObject, TKey}}, Func{TObject, IObservable{TAny}}, TimeSpan?, IScheduler?)"/>
131-
public static IObservable<IChangeSet<TObject>> AutoRefreshOnObservable<TObject, TAny>(this IObservable<IChangeSet<TObject>> source, Func<TObject, IObservable<TAny>> reevaluator, TimeSpan? changeSetBuffer = null, IScheduler? scheduler = null)
132-
where TObject : notnull
133-
{
134-
source.ThrowArgumentNullExceptionIfNull(nameof(source));
135-
reevaluator.ThrowArgumentNullExceptionIfNull(nameof(reevaluator));
136-
137-
return new AutoRefresh<TObject, TAny>(source, reevaluator, changeSetBuffer, scheduler).Run();
138-
}
139101
}

0 commit comments

Comments
 (0)