< Summary

Information
Class: MyNet.Observable.Behaviors.ModificationTrackingBehavior
Assembly: MyNet.Observable
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Behaviors/ModificationTrackingBehavior.cs
Tag: 323_28699572109
Line coverage
80%
Covered lines: 79
Uncovered lines: 19
Coverable lines: 98
Total lines: 283
Line coverage: 80.6%
Branch coverage
62%
Covered branches: 49
Total branches: 78
Branch coverage: 62.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
.ctor(...)100%11100%
.ctor(...)100%210%
get_IsModified()100%11100%
ShouldTrack(...)100%22100%
MarkAsModified()66.66%6685.71%
ResetModified(...)83.33%6688.88%
MyNet.Observable.IModificationAware.ResetModified()100%210%
OnPropertyChanged(...)70%101084.61%
OnCollectionChanged(...)0%2040%
EnsureInitialStateAttached()75%4483.33%
MayYieldModificationAwareValue(...)50%66100%
TryAttachProperty(...)100%1010100%
Attach(...)83.33%66100%
Detach(...)16.66%17633.33%
OnChildChanged(...)50%9880%
RaiseIsModifiedChanged()100%11100%
DisposeManagedResources()50%8890%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Behaviors/ModificationTrackingBehavior.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ModificationTrackingBehavior.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Collections.Specialized;
 10using System.ComponentModel;
 11using System.Reflection;
 12using System.Threading;
 13using MyNet.Metadata;
 14using MyNet.Observable.Behaviors.Metadata.Features;
 15using MyNet.Reflection;
 16
 17namespace MyNet.Observable.Behaviors;
 18
 19/// <summary>
 20/// Provides a behavior that tracks modifications to an object and its nested properties, including collections. It impl
 21/// </summary>
 22public sealed class ModificationTrackingBehavior : SuspendableBehavior<ObservableObject>, IModificationTrackingBehavior
 23{
 24    private readonly CollectionTrackingBehavior? _collections;
 25    private readonly bool _ownsCollections;
 6326    private readonly HashSet<string> _attachedProperties = new(StringComparer.Ordinal);
 6327    private readonly HashSet<IModificationAware> _trackedChildren = [];
 6328    private readonly Dictionary<IModificationAware, INotifyPropertyChanged> _childNotifiers = [];
 6329    private readonly Lock _attachSync = new();
 30
 31    /// <summary>
 32    /// Initializes a new instance of the <see cref="ModificationTrackingBehavior"/> class with the specified owner. Thi
 33    /// </summary>
 34    /// <param name="owner">The owner object.</param>
 35    public ModificationTrackingBehavior(ObservableObject owner)
 6336        : this(owner, new(owner), ownsCollections: true)
 37    {
 6338    }
 39
 40    /// <summary>
 41    /// Initializes a new instance of the <see cref="ModificationTrackingBehavior"/> class.
 42    /// </summary>
 43    /// <param name="owner">The owner object.</param>
 44    /// <param name="collections">The collection tracking behavior.</param>
 45    public ModificationTrackingBehavior(ObservableObject owner, CollectionTrackingBehavior collections)
 046        : this(owner, collections, ownsCollections: false)
 47    {
 048    }
 49
 50    private ModificationTrackingBehavior(ObservableObject owner, CollectionTrackingBehavior? collections, bool ownsColle
 6351        : base(owner)
 52    {
 6353        _collections = collections;
 6354        _ownsCollections = ownsCollections;
 55
 6356        _collections?.CollectionChanged += OnCollectionChanged;
 57
 6358        EnsureInitialStateAttached();
 6359    }
 60
 61    /// <inheritdoc />
 62    public bool IsModified
 63    {
 64        get
 65        {
 14466            EnsureInitialStateAttached();
 14467            return field;
 68        }
 69
 70        private set;
 71    }
 72
 73    /// <summary>
 74    /// Determines whether the specified property should be tracked for modifications, using metadata from <see cref="Me
 75    /// </summary>
 76    /// <param name="propertyName">The name of the property to check.</param>
 77    /// <returns><c>true</c> if the property should be tracked; otherwise, <c>false</c>.</returns>
 573978    private bool ShouldTrack(string propertyName) => !MetadataRegistry.Get(Owner.GetType()).GetProperty(propertyName).Tr
 79
 80    /// <summary>
 81    /// Marks the object as modified.
 82    /// </summary>
 83    public void MarkAsModified()
 84    {
 11485        if (IsDisposed || IsSuspended)
 086            return;
 87
 11488        if (IsModified)
 6989            return;
 90
 4591        IsModified = true;
 92
 4593        RaiseIsModifiedChanged();
 4594    }
 95
 96    /// <summary>
 97    /// Resets the modification state.
 98    /// </summary>
 99    /// <param name="recursive">
 100    /// Indicates whether nested objects should also be reset.
 101    /// </param>
 102    public void ResetModified(bool recursive = true)
 103    {
 6104        EnsureInitialStateAttached();
 105
 6106        if (recursive)
 107        {
 18108            foreach (var child in _trackedChildren)
 3109                child.ResetModified();
 110        }
 111
 6112        if (!IsModified)
 0113            return;
 114
 6115        IsModified = false;
 116
 6117        RaiseIsModifiedChanged();
 6118    }
 119
 120    /// <inheritdoc/>
 0121    void IModificationAware.ResetModified() => ResetModified();
 122
 123    /// <inheritdoc />
 124    public void OnPropertyChanged(PropertyMutationContext context)
 125    {
 189126        EnsureInitialStateAttached();
 127
 189128        if (IsDisposed || IsSuspended)
 0129            return;
 130
 189131        if (string.IsNullOrWhiteSpace(context.PropertyName))
 0132            return;
 133
 189134        if (!ShouldTrack(context.PropertyName))
 3135            return;
 136
 186137        if (Equals(context.OldValue, context.NewValue))
 81138            return;
 139
 105140        Detach(context.OldValue);
 105141        Attach(context.NewValue);
 142
 105143        MarkAsModified();
 105144    }
 145
 146    #region Collections (via CollectionTrackingBehavior)
 147
 148    private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
 149    {
 0150        EnsureInitialStateAttached();
 151
 0152        if (IsDisposed || IsSuspended)
 0153            return;
 154
 0155        MarkAsModified();
 0156    }
 157
 158    #endregion
 159
 160    #region Attach / Detach
 161
 162    /// <summary>
 163    /// Attaches to readable owner properties that are already initialized. Properties whose getters fail (for example w
 164    /// </summary>
 165    internal void EnsureInitialStateAttached()
 166    {
 402167        if (IsDisposed)
 0168            return;
 169
 170        lock (_attachSync)
 171        {
 11904172            foreach (var property in Owner.GetType().GetPublicProperties())
 5550173                TryAttachProperty(property);
 402174        }
 402175    }
 176
 177    private static bool MayYieldModificationAwareValue(Type propertyType)
 178    {
 72179        propertyType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
 180
 72181        return typeof(IModificationAware).IsAssignableFrom(propertyType) || (typeof(System.Collections.IEnumerable).IsAs
 72182                                                                             && propertyType != typeof(string));
 183    }
 184
 185    private void TryAttachProperty(PropertyInfo property)
 186    {
 5550187        if (!ShouldTrack(property.Name) || _attachedProperties.Contains(property.Name))
 4587188            return;
 189
 963190        if (!PropertyValueAccess.TryGetValue(Owner, property, out var value))
 96191            return;
 192
 867193        if (value is null && MayYieldModificationAwareValue(property.PropertyType))
 39194            return;
 195
 828196        _attachedProperties.Add(property.Name);
 828197        Attach(value);
 828198    }
 199
 200    /// <summary>
 201    /// Attaches to a value if it is modification-aware or a collection. This method is called when a property changes t
 202    /// </summary>
 203    private void Attach(object? value)
 204    {
 933205        if (value is IModificationAware child)
 206        {
 12207            if (_trackedChildren.Add(child))
 208            {
 9209                if (value is INotifyPropertyChanged npc)
 210                {
 9211                    _childNotifiers[child] = npc;
 9212                    npc.PropertyChanged += OnChildChanged;
 213                }
 214            }
 215        }
 933216    }
 217
 218    /// <summary>
 219    /// Detaches from a value if it is modification-aware or a collection. This method is called when a property changes
 220    /// </summary>
 221    private void Detach(object? value)
 222    {
 105223        if (value is IModificationAware child)
 224        {
 0225            if (_trackedChildren.Remove(child))
 226            {
 0227                if (_childNotifiers.TryGetValue(child, out var npc))
 228                {
 0229                    npc.PropertyChanged -= OnChildChanged;
 0230                    _childNotifiers.Remove(child);
 231                }
 232            }
 233        }
 105234    }
 235
 236    /// <summary>
 237    /// Handles the PropertyChanged event of a child object that is modification-aware. This method is called when a chi
 238    /// </summary>
 239    /// <param name="sender">The child object that raised the event.</param>
 240    /// <param name="e">The event data.</param>
 241    private void OnChildChanged(object? sender, PropertyChangedEventArgs e)
 242    {
 9243        if (IsDisposed || IsSuspended)
 0244            return;
 245
 9246        if (sender is IModificationAware { IsModified: true })
 9247            MarkAsModified();
 9248    }
 249
 250    #endregion
 251
 252    #region Notifications
 253
 254    /// <summary>
 255    /// Raises a notification that the IsModified property has changed. This method is called whenever the modification 
 256    /// </summary>
 51257    private void RaiseIsModifiedChanged() => Owner.NotifyPropertyChanged(nameof(IModificationAware.IsModified));
 258
 259    #endregion
 260
 261    #region Dispose
 262
 263    /// <inheritdoc />
 264    protected override void DisposeManagedResources()
 265    {
 3266        _collections?.CollectionChanged -= OnCollectionChanged;
 267
 3268        if (_ownsCollections)
 3269            _collections?.Dispose();
 270
 6271        foreach (var notifier in _childNotifiers.Values)
 0272            notifier.PropertyChanged -= OnChildChanged;
 273
 3274        _childNotifiers.Clear();
 3275        _trackedChildren.Clear();
 3276        _attachedProperties.Clear();
 277
 3278        base.DisposeManagedResources();
 3279    }
 280
 281    #endregion
 282}
 283