using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
namespace XFEEDToolBox.Core.Presentation;
/// <summary>A stable UI identity over immutable snapshots; only changed exposed properties notify.</summary>
public abstract class LiveRow<T>(T value) : INotifyPropertyChanged where T : class
{
public T Record { get; private set; } = value;
public event PropertyChangedEventHandler? PropertyChanged;
private PropertyInfo[]? displayProperties;
public bool Update(T next)
{
if (EqualityComparer<T>.Default.Equals(Record, next)) return false;
displayProperties ??= GetType().GetProperties().Where(p => p.Name != nameof(Record) && p.CanRead && p.GetIndexParameters().Length == 0).ToArray();
var before = displayProperties.Select(p => p.GetValue(this)).ToArray();
Record = next;
var changed=false;
for (var i = 0; i < displayProperties.Length; i++)
if (!Equals(before[i], displayProperties[i].GetValue(this)))
{
changed=true;
PropertyChanged?.Invoke(this, new(displayProperties[i].Name));
}
return changed;
}
}
public sealed record ListDelta(int Added, int Removed, int Updated, int Moved)
{
public bool Changed => Added + Removed + Updated + Moved > 0;
}
/// <summary>Never resets the collection or replaces surviving rows. Reorders with Move only when requested.</summary>
public sealed class LiveCollection<TRow, TValue, TKey>(Func<TValue,TKey> key, Func<TValue,TRow> create)
: ObservableCollection<TRow> where TRow : LiveRow<TValue> where TValue : class where TKey : notnull
{
public ListDelta Apply(IEnumerable<TValue> values, bool reorder = true)
{
var next = values.DistinctBy(key).ToArray();
var wanted = next.Select(key).ToHashSet();
var byKey = this.ToDictionary(row => key(row.Record));
var removed = 0; var added = 0; var updated = 0; var moved = 0;
for (var i = Count - 1; i >= 0; i--)
if (!wanted.Contains(key(this[i].Record))) { RemoveAt(i); removed++; }
var position = 0;
foreach (var value in next)
{
if (byKey.TryGetValue(key(value), out var row))
{
if (row.Update(value)) updated++;
if (reorder && (position >= Count || !ReferenceEquals(this[position], row)))
{
var current = IndexOf(row);
if (current != position) { Move(current, position); moved++; }
}
}
else
{
row = create(value);
if (reorder) Insert(position, row); else Add(row);
added++;
}
position++;
}
return new(added, removed, updated, moved);
}
}
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
namespace XFEEDToolBox.Core.Presentation;
/// <summary>A stable UI identity over immutable snapshots; only changed exposed properties notify.</summary>
public abstract class LiveRow<T>(T value) : INotifyPropertyChanged where T : class
{
public T Record { get; private set; } = value;
public event PropertyChangedEventHandler? PropertyChanged;
private PropertyInfo[]? displayProperties;
public bool Update(T next)
{
if (EqualityComparer<T>.Default.Equals(Record, next)) return false;
displayProperties ??= GetType().GetProperties().Where(p => p.Name != nameof(Record) && p.CanRead && p.GetIndexParameters().Length == 0).ToArray();
var before = displayProperties.Select(p => p.GetValue(this)).ToArray();
Record = next;
var changed=false;
for (var i = 0; i < displayProperties.Length; i++)
if (!Equals(before[i], displayProperties[i].GetValue(this)))
{
changed=true;
PropertyChanged?.Invoke(this, new(displayProperties[i].Name));
}
return changed;
}
}
public sealed record ListDelta(int Added, int Removed, int Updated, int Moved)
{
public bool Changed => Added + Removed + Updated + Moved > 0;
}
/// <summary>Never resets the collection or replaces surviving rows. Reorders with Move only when requested.</summary>
public sealed class LiveCollection<TRow, TValue, TKey>(Func<TValue,TKey> key, Func<TValue,TRow> create)
: ObservableCollection<TRow> where TRow : LiveRow<TValue> where TValue : class where TKey : notnull
{
public ListDelta Apply(IEnumerable<TValue> values, bool reorder = true)
{
var next = values.DistinctBy(key).ToArray();
var wanted = next.Select(key).ToHashSet();
var byKey = this.ToDictionary(row => key(row.Record));
var removed = 0; var added = 0; var updated = 0; var moved = 0;
for (var i = Count - 1; i >= 0; i--)
if (!wanted.Contains(key(this[i].Record))) { RemoveAt(i); removed++; }
var position = 0;
foreach (var value in next)
{
if (byKey.TryGetValue(key(value), out var row))
{
if (row.Update(value)) updated++;
if (reorder && (position >= Count || !ReferenceEquals(this[position], row)))
{
var current = IndexOf(row);
if (current != position) { Move(current, position); moved++; }
}
}
else
{
row = create(value);
if (reorder) Insert(position, row); else Add(row);
added++;
}
position++;
}
return new(added, removed, updated, moved);
}
}