Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/DynamicData/List/ObservableListEx.Adapt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
// Roland Pheasant licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData.Binding;
using DynamicData.Cache.Internal;
using DynamicData.List.Internal;
using DynamicData.List.Linq;

// ReSharper disable once CheckNamespace
namespace DynamicData;

/// <summary>
/// ObservableList extensions for Adapt.
/// </summary>
public static partial class ObservableListEx
{
/// <summary>
/// Injects a side effect into a changeset stream via an <see cref="IChangeSetAdaptor{T}"/>.
/// The adaptor's <c>Adapt</c> method is invoked for each changeset before it is forwarded downstream unchanged.
/// </summary>
/// <typeparam name="T">The type of items in the list.</typeparam>
/// <param name="source">The source <see cref="IObservable{IChangeSet{T}}"/> to observe and adapt.</param>

Check warning on line 31 in src/DynamicData/List/ObservableListEx.Adapt.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has syntactically incorrect cref attribute 'IObservable{IChangeSet{T}}'
/// <param name="adaptor">The <see cref="IChangeSetAdaptor{T}"/> adaptor whose <c>Adapt</c> method is invoked for each changeset.</param>
/// <returns>A list changeset stream identical to the source, with the adaptor side effect applied.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="adaptor"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This is the primary extension point for custom UI binding adaptors (e.g., <see cref="Bind{T}(IObservable{IChangeSet{T}}, IObservableCollection{T}, BindingOptions)"/>
/// delegates to this operator). If the adaptor throws, the exception propagates downstream as <c>OnError</c>.
/// </para>
/// </remarks>
/// <seealso cref="Bind{T}(IObservable{IChangeSet{T}}, IObservableCollection{T}, BindingOptions)"/>
public static IObservable<IChangeSet<T>> Adapt<T>(this IObservable<IChangeSet<T>> source, IChangeSetAdaptor<T> adaptor)
where T : notnull
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
adaptor.ThrowArgumentNullExceptionIfNull(nameof(adaptor));

return Observable.Create<IChangeSet<T>>(
observer =>
{
var locker = InternalEx.NewLock();
return source.Synchronize(locker).Select(
changes =>
{
adaptor.Adapt(changes);
return changes;
}).SubscribeSafe(observer);
});
}
}
139 changes: 139 additions & 0 deletions src/DynamicData/List/ObservableListEx.AutoRefresh.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
// Roland Pheasant licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData.Binding;
using DynamicData.Cache.Internal;
using DynamicData.List.Internal;
using DynamicData.List.Linq;

// ReSharper disable once CheckNamespace
namespace DynamicData;

/// <summary>
/// ObservableList extensions for AutoRefresh.
/// </summary>
public static partial class ObservableListEx
{
/// <summary>
/// Monitors all properties on each item (via <see cref="INotifyPropertyChanged"/>) and emits <b>Refresh</b>
/// changes when any property changes, causing downstream operators to re-evaluate.
/// </summary>
/// <typeparam name="TObject">The type of items, which must implement <see cref="INotifyPropertyChanged"/>.</typeparam>
/// <param name="source">The source <see cref="IObservable{IChangeSet{TObject}}"/> to monitor for property-driven refresh signals.</param>
/// <param name="changeSetBuffer">An optional <see cref="TimeSpan"/> buffer duration to batch multiple refresh signals into a single changeset.</param>
/// <param name="propertyChangeThrottle">An optional <see cref="TimeSpan"/> throttle applied to each item's property change notifications.</param>
/// <param name="scheduler">The scheduler for throttle and buffer timing. Defaults to <see cref="GlobalConfig.DefaultScheduler"/>.</param>
/// <returns>A list changeset stream with additional <b>Refresh</b> changes injected when properties change.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// Wraps <see cref="AutoRefreshOnObservable{TObject, TAny}"/> using <c>WhenAnyPropertyChanged()</c> as the re-evaluator.
/// Pair with <see cref="Filter{T}(IObservable{IChangeSet{T}}, Func{T, bool})"/> or <see cref="Sort{T}(IObservable{IChangeSet{T}}, IComparer{T}, SortOptions, IObservable{Unit}?, IObservable{IComparer{T}}?, int)"/>
/// to get reactive re-evaluation on property changes.
/// </para>
/// <list type="table">
/// <listheader><term>Event</term><description>Behavior</description></listheader>
/// <item><term>Add/AddRange</term><description>Subscribes to <c>PropertyChanged</c> on each new item. The original change is forwarded.</description></item>
/// <item><term>Replace</term><description>Unsubscribes from the old item, subscribes to the new. The original change is forwarded.</description></item>
/// <item><term>Remove/RemoveRange/Clear</term><description>Unsubscribes from removed items. The original change is forwarded.</description></item>
/// <item><term>Moved/Refresh</term><description>Forwarded unchanged.</description></item>
/// <item><term>Property changes</term><description>A <b>Refresh</b> change is emitted for the item whose property changed.</description></item>
/// </list>
/// <para><b>Worth noting:</b> Each item generates a subscription. For large lists with frequent property changes, use <paramref name="changeSetBuffer"/> and <paramref name="propertyChangeThrottle"/> to reduce churn.</para>
/// </remarks>
/// <seealso cref="AutoRefresh{TObject, TProperty}(IObservable{IChangeSet{TObject}}, Expression{Func{TObject, TProperty}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
/// <seealso cref="AutoRefreshOnObservable{TObject, TAny}(IObservable{IChangeSet{TObject}}, Func{TObject, IObservable{TAny}}, TimeSpan?, IScheduler?)"/>
/// <seealso cref="SuppressRefresh{T}(IObservable{IChangeSet{T}})"/>
/// <seealso cref="ObservableCacheEx.AutoRefresh{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
public static IObservable<IChangeSet<TObject>> AutoRefresh<TObject>(this IObservable<IChangeSet<TObject>> source, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null)
where TObject : INotifyPropertyChanged
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));

return source.AutoRefreshOnObservable(
t =>
{
if (propertyChangeThrottle is null)
{
return t.WhenAnyPropertyChanged();
}

return t.WhenAnyPropertyChanged().Throttle(propertyChangeThrottle.Value, scheduler ?? GlobalConfig.DefaultScheduler);
},
changeSetBuffer,
scheduler);
}

/// <summary>
/// Monitors a single property (selected by <paramref name="propertyAccessor"/>) on each item via <see cref="INotifyPropertyChanged"/>
/// and emits <b>Refresh</b> changes when that property changes, causing downstream operators to re-evaluate. More efficient than
/// the all-properties overload when only one property (of type <typeparamref name="TProperty"/>) affects downstream behavior.
/// </summary>
/// <inheritdoc cref="AutoRefresh{TObject}(IObservable{IChangeSet{TObject}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
public static IObservable<IChangeSet<TObject>> AutoRefresh<TObject, TProperty>(this IObservable<IChangeSet<TObject>> source, Expression<Func<TObject, TProperty>> propertyAccessor, TimeSpan? changeSetBuffer = null, TimeSpan? propertyChangeThrottle = null, IScheduler? scheduler = null)
where TObject : INotifyPropertyChanged
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
propertyAccessor.ThrowArgumentNullExceptionIfNull(nameof(propertyAccessor));

return source.AutoRefreshOnObservable(
t =>
{
if (propertyChangeThrottle is null)
{
return t.WhenPropertyChanged(propertyAccessor, false);
}

return t.WhenPropertyChanged(propertyAccessor, false).Throttle(propertyChangeThrottle.Value, scheduler ?? GlobalConfig.DefaultScheduler);
},
changeSetBuffer,
scheduler);
}

/// <summary>
/// Monitors each item with a custom observable and emits <b>Refresh</b> changes whenever that observable fires,
/// causing downstream operators (Filter, Sort, Group) to re-evaluate.
/// </summary>
/// <typeparam name="TObject">The type of items in the list.</typeparam>
/// <typeparam name="TAny">The type emitted by the re-evaluator observable (value is ignored).</typeparam>
/// <param name="source">The source <see cref="IObservable{IChangeSet{TObject}}"/> to monitor for observable-driven refresh signals.</param>
/// <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>
/// <param name="changeSetBuffer">An optional <see cref="TimeSpan"/> buffer duration to batch refresh signals into a single changeset.</param>
/// <param name="scheduler">The <see cref="IScheduler"/> for buffering.</param>
/// <returns>A list changeset stream with additional <b>Refresh</b> changes injected when per-item observables fire.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> or <paramref name="reevaluator"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This is the general-purpose refresh mechanism. <see cref="AutoRefresh{TObject}(IObservable{IChangeSet{TObject}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
/// is a convenience wrapper that uses <c>WhenAnyPropertyChanged()</c> as the re-evaluator.
/// </para>
/// <list type="table">
/// <listheader><term>Event</term><description>Behavior</description></listheader>
/// <item><term>Add/AddRange</term><description>Subscribes to the re-evaluator observable for each new item. The original change is forwarded.</description></item>
/// <item><term>Replace</term><description>Unsubscribes from the old item's observable, subscribes to the new. The original change is forwarded.</description></item>
/// <item><term>Remove/RemoveRange/Clear</term><description>Unsubscribes from removed items. The original change is forwarded.</description></item>
/// <item><term>Moved/Refresh</term><description>Forwarded unchanged.</description></item>
/// <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>
/// </list>
/// </remarks>
/// <seealso cref="AutoRefresh{TObject}(IObservable{IChangeSet{TObject}}, TimeSpan?, TimeSpan?, IScheduler?)"/>
/// <seealso cref="SuppressRefresh{T}(IObservable{IChangeSet{T}})"/>
/// <seealso cref="ObservableCacheEx.AutoRefreshOnObservable{TObject, TKey, TAny}(IObservable{IChangeSet{TObject, TKey}}, Func{TObject, IObservable{TAny}}, TimeSpan?, IScheduler?)"/>
public static IObservable<IChangeSet<TObject>> AutoRefreshOnObservable<TObject, TAny>(this IObservable<IChangeSet<TObject>> source, Func<TObject, IObservable<TAny>> reevaluator, TimeSpan? changeSetBuffer = null, IScheduler? scheduler = null)
where TObject : notnull
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
reevaluator.ThrowArgumentNullExceptionIfNull(nameof(reevaluator));

return new AutoRefresh<TObject, TAny>(source, reevaluator, changeSetBuffer, scheduler).Run();
}
}
Loading
Loading