Skip to content

Commit

Permalink
perf: Make ViewModelBase's internal errors dictionary lazy initialize…
Browse files Browse the repository at this point in the history
…d to reduce default memory allocations.
  • Loading branch information
jeanplevesque committed Dec 15, 2023
1 parent 3a43c23 commit c945e32
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 5 deletions.
183 changes: 183 additions & 0 deletions src/DynamicMvvm.Tests/ViewModel/ViewModelBaseErrorsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;

namespace Chinook.DynamicMvvm.Tests.ViewModel
{
public class ViewModelBaseErrorsTests
{
[Fact]
public void It_Has_No_Errors_By_Default()
{
// Arrange
var sut = new ViewModelBase();

// Assert
sut.HasErrors.Should().BeFalse();
sut.GetErrors(null).Should().BeEmpty();
sut.GetErrors("FakeProperty").Should().BeEmpty();
}

[Fact]
public void It_Can_Set_Errors_For_A_Property()
{
// Arrange
var sut = new ViewModelBase();

// Act
sut.SetErrors("Foo", new[] { "Bar" });

// Assert
sut.HasErrors.Should().BeTrue();
sut.GetErrors("Foo").Should().ContainEquivalentOf("Bar");
}

[Fact]
public void It_Can_Set_Errors_Using_Dictionary()
{
// Arrange
var sut = new ViewModelBase();

// Act
sut.SetErrors(new Dictionary<string, IEnumerable<object>>()
{
{ "Foo", new[] { "Bar" } }
});

// Assert
sut.HasErrors.Should().BeTrue();
sut.GetErrors(null).Should().ContainEquivalentOf("Bar");
}

[Fact]
public void It_Can_Clear_Errors_For_A_Property()
{
// Arrange
var sut = new ViewModelBase();

sut.SetErrors("Prop1", new[] { "Error1" });
sut.SetErrors("Prop2", new[] { "Error2" });

// Act
sut.ClearErrors("Prop1");

// Assert
sut.HasErrors.Should().BeTrue();
sut.GetErrors(null).Should().ContainEquivalentOf("Error2");
}

[Fact]
public void It_Can_Clear_Errors_For_All_Properties()
{
// Arrange
var sut = new ViewModelBase();

sut.SetErrors("Prop1", new[] { "Error1" });
sut.SetErrors("Prop2", new[] { "Error2" });

// Act
sut.ClearErrors();

// Assert
sut.HasErrors.Should().BeFalse();
sut.GetErrors(null).Should().BeEmpty();
}

[Fact]
public void It_Raises_ErrorsChanged_When_Errors_Are_Set()
{
// Arrange
var sut = new ViewModelBase();
var receivedValues = new List<(object, DataErrorsChangedEventArgs)>();

sut.ErrorsChanged += OnErrorsChanged;

// Act
sut.SetErrors("Foo", new[] { "Bar" });

// Assert
receivedValues.Count().Should().Be(1);
receivedValues[0].Item1.Should().Be(sut);
receivedValues[0].Item2.PropertyName.Should().Be("Foo");

void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
receivedValues.Add((sender, e));
}
}

[Fact]
public void It_Raises_ErrorsChanged_When_Errors_Are_Cleared()
{
// Arrange
var sut = new ViewModelBase();
var receivedValues = new List<(object, DataErrorsChangedEventArgs)>();

sut.SetErrors("Foo", new[] { "Bar" });
sut.ErrorsChanged += OnErrorsChanged;

// Act
sut.ClearErrors("Foo");

// Assert
receivedValues.Count().Should().Be(1);
receivedValues[0].Item1.Should().Be(sut);
receivedValues[0].Item2.PropertyName.Should().Be("Foo");

void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
receivedValues.Add((sender, e));
}
}

[Fact]
public void It_Raises_ErrorsChanged_When_Errors_Are_Cleared_For_All_Properties()
{
// Arrange
var sut = new ViewModelBase();
var receivedValues = new List<(object, DataErrorsChangedEventArgs)>();

sut.SetErrors("Foo", new[] { "Bar" });
sut.ErrorsChanged += OnErrorsChanged;

// Act
sut.ClearErrors();

// Assert
receivedValues.Count().Should().Be(1);
receivedValues[0].Item1.Should().Be(sut);
receivedValues[0].Item2.PropertyName.Should().BeNull();

void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
receivedValues.Add((sender, e));
}
}

[Fact]
public void Clear_Doesnt_Raise_ErrorsChanged_There_Are_No_Errors()
{
// Arrange
var sut = new ViewModelBase();
var receivedValues = new List<(object, DataErrorsChangedEventArgs)>();

sut.ErrorsChanged += OnErrorsChanged;

// Act
sut.ClearErrors();

// Assert
receivedValues.Should().BeEmpty();

void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
{
receivedValues.Add((sender, e));
}
}
}
}
25 changes: 20 additions & 5 deletions src/DynamicMvvm/ViewModel/ViewModelBase.Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ namespace Chinook.DynamicMvvm
{
public partial class ViewModelBase
{
private ConcurrentDictionary<string, IEnumerable<object>> _errors = new ConcurrentDictionary<string, IEnumerable<object>>();
private Dictionary<string, IEnumerable<object>> _errors;

/// <inheritdoc />
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

/// <inheritdoc />
public bool HasErrors => _errors.Values.SelectMany(x => x).Any();
public bool HasErrors => _errors is null ? false : _errors.Values.SelectMany(x => x).Any();

/// <inheritdoc />
public IEnumerable GetErrors(string propertyName)
Expand All @@ -27,12 +27,12 @@ public IEnumerable GetErrors(string propertyName)
if (string.IsNullOrEmpty(propertyName))
{
// Entity level errors.
errors = _errors.Values.SelectMany(s => s);
errors = _errors?.Values.SelectMany(s => s);
}
else
{
// Property level errors.
_errors.TryGetValue(propertyName, out errors);
_errors?.TryGetValue(propertyName, out errors);
}

return errors ?? Enumerable.Empty<object>();
Expand All @@ -49,6 +49,7 @@ public void SetErrors(string propertyName, IEnumerable<object> errors)
return;
}

EnsureErrorsAreInitialized();
_errors[propertyName] = errors;

ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
Expand All @@ -65,7 +66,7 @@ public void SetErrors(IDictionary<string, IEnumerable<object>> errors)
return;
}

_errors = new ConcurrentDictionary<string, IEnumerable<object>>(errors);
_errors = new Dictionary<string, IEnumerable<object>>(errors);

ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName: null));
}
Expand All @@ -81,6 +82,12 @@ public void ClearErrors(string propertyName = null)
return;
}

if (_errors is null)
{
// No errors to clear.
return;
}

if (string.IsNullOrEmpty(propertyName))
{
_errors.Clear();
Expand All @@ -92,5 +99,13 @@ public void ClearErrors(string propertyName = null)

ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}

private void EnsureErrorsAreInitialized()
{
if (_errors is null)
{
_errors = new Dictionary<string, IEnumerable<object>>();
}
}
}
}

0 comments on commit c945e32

Please sign in to comment.