Skip to content

Instantly share code, notes, and snippets.

@skarllot
Created April 7, 2024 21:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skarllot/82413dfe14832a16f773722734f59377 to your computer and use it in GitHub Desktop.
Save skarllot/82413dfe14832a16f773722734f59377 to your computer and use it in GitHub Desktop.
Subject class for using INotifyDataErrorInfo with Avalonia UI
using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Reactive.Subjects;
using CSharpFunctionalExtensions;
namespace Example;
public sealed class ChangeErrorsSubject : ISubject<ImmutableList<PropertyDataError>>, INotifyDataErrorInfo, IDisposable
{
private static readonly List<string> s_noErrors = [];
private readonly Subject<ImmutableList<PropertyDataError>> _subject = new();
private readonly Dictionary<string, List<string>> _propertyErrors = new(StringComparer.Ordinal);
private readonly Dictionary<string, DataErrorsChangedEventArgs> _errorsEventArgsCache = new(StringComparer.Ordinal);
/// <inheritdoc />
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
/// <inheritdoc />
public bool HasErrors => _propertyErrors.Count > 0;
public IEnumerable GetErrors(string? propertyName) => propertyName is not null
? _propertyErrors.TryFind(propertyName).GetValueOrDefault(s_noErrors)
: s_noErrors;
public void AddError(string? propertyName, string error)
{
_propertyErrors.AddOrUpdate(
propertyName ?? string.Empty,
static (_, arg) => [arg],
static (_, v, arg) => v.Add(arg),
error);
OnErrorsChanged(propertyName);
_subject.OnNext(GetCurrentState());
}
public bool RemoveErrors(string? propertyName)
{
bool isRemoved = _propertyErrors.Remove(propertyName ?? string.Empty);
if (!isRemoved)
{
return false;
}
OnErrorsChanged(propertyName);
_subject.OnNext(GetCurrentState());
return true;
}
public void OnNext(ImmutableList<PropertyDataError> value)
{
var allNames = value.Select(x => x.PropertyName ?? string.Empty)
.Concat(_propertyErrors.Keys)
.Distinct(StringComparer.Ordinal);
var lookup = value.ToLookup(x => x.PropertyName ?? string.Empty, x => x.Error, StringComparer.Ordinal);
List<string> changedProperties = [];
foreach (string propertyName in allNames)
{
var newErrors = lookup[propertyName].ToList();
if (newErrors.Count == 0)
{
_propertyErrors.Remove(propertyName);
changedProperties.Add(propertyName);
}
else
{
var currErrors = _propertyErrors.AddOrUpdate(
propertyName,
static (_, arg) => arg,
static (_, v, arg) => v.SequenceEqual(arg) ? v : arg,
newErrors);
if (currErrors == newErrors)
{
changedProperties.Add(propertyName);
}
}
}
changedProperties.ForEach(OnErrorsChanged);
if (changedProperties.Count > 0)
{
_subject.OnNext(value);
}
}
public void OnError(Exception error) => _subject.OnError(error);
public void OnCompleted() => _subject.OnCompleted();
public IDisposable Subscribe(IObserver<ImmutableList<PropertyDataError>> observer) => _subject.Subscribe(observer);
public void Dispose() => _subject.Dispose();
private ImmutableList<PropertyDataError> GetCurrentState() => _propertyErrors
.SelectMany(
static kv =>
string.IsNullOrEmpty(kv.Key)
? kv.Value.Select(static v => new PropertyDataError(null, v))
: kv.Value.Select(v => new PropertyDataError(kv.Key, v)))
.ToImmutableList();
private void OnErrorsChanged(string? propertyName) => ErrorsChanged?.Invoke(
this,
_errorsEventArgsCache.GetOrAdd(
propertyName ?? string.Empty,
static k => new DataErrorsChangedEventArgs(string.IsNullOrEmpty(k) ? null : k)));
}
namespace Example;
public sealed record PropertyDataError(string? PropertyName, string Error);
using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel;
using ReactiveUI;
namespace Example;
public class RoutableViewModelBase : ViewModelBase, IRoutableViewModel, INotifyDataErrorInfo, IDisposable
{
private readonly ChangeErrorsSubject _changeErrorsSubject = new();
protected RoutableViewModelBase(IScreen hostScreen)
{
HostScreen = hostScreen;
}
event EventHandler<DataErrorsChangedEventArgs>? INotifyDataErrorInfo.ErrorsChanged
{
add => _changeErrorsSubject.ErrorsChanged += value;
remove => _changeErrorsSubject.ErrorsChanged -= value;
}
public IScreen HostScreen { get; }
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString()[..5];
public bool HasErrors => _changeErrorsSubject.HasErrors;
protected IObservable<ImmutableList<PropertyDataError>> WhenErrorsChanged => _changeErrorsSubject;
protected IObserver<ImmutableList<PropertyDataError>> ChangeErrors => _changeErrorsSubject;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) => _changeErrorsSubject.GetErrors(propertyName);
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_changeErrorsSubject.Dispose();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment