Skip to content

Commit

Permalink
Rename ValidatingViewModel to ValidatingBindable
Browse files Browse the repository at this point in the history
- ViewModel now inherits ValidatingBindable instead of Bindable and thus conforms to INotifyDataErrorInfo
- SetErrors is now protected
- IsValid was moved to ViewModel
  • Loading branch information
nkristek committed Aug 21, 2019
1 parent 48714d9 commit af9f743
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 339 deletions.
2 changes: 1 addition & 1 deletion src/Smaragd/ViewModels/DialogModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
/// <inheritdoc cref="IDialogModel" />
public abstract class DialogModel
: ValidatingViewModel, IDialogModel
: ViewModel, IDialogModel
{
private string _title;

Expand Down
1 change: 1 addition & 0 deletions src/Smaragd/ViewModels/IBindable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace NKristek.Smaragd.ViewModels
{
/// <inheritdoc cref="INotifyPropertyChanged" />
/// <summary>
/// Notifies clients that a property value is changing or has changed.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Smaragd/ViewModels/IValidatingBindable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel;

namespace NKristek.Smaragd.ViewModels
{
/// <inheritdoc cref="IBindable" />
/// <summary>
/// Notifies clients that errors of a property have changed.
/// </summary>
public interface IValidatingBindable
: IBindable, INotifyDataErrorInfo
{
}
}
25 changes: 0 additions & 25 deletions src/Smaragd/ViewModels/IValidatingViewModel.cs

This file was deleted.

7 changes: 6 additions & 1 deletion src/Smaragd/ViewModels/IViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// Defines properties which are useful for a viewmodel implementation.
/// </summary>
public interface IViewModel
: IBindable
: IValidatingBindable
{
/// <summary>
/// Indicates if a property changed and the change is not persisted.
Expand All @@ -26,5 +26,10 @@ public interface IViewModel
/// Indicates if this <see cref="IViewModel"/> instance is currently being updated.
/// </summary>
bool IsUpdating { get; set; }

/// <summary>
/// If data is valid.
/// </summary>
bool IsValid { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using NKristek.Smaragd.Attributes;

namespace NKristek.Smaragd.ViewModels
{
/// <inheritdoc cref="IValidatingViewModel" />
public abstract class ValidatingViewModel
: ViewModel, IValidatingViewModel
/// <inheritdoc cref="IValidatingBindable" />
public abstract class ValidatingBindable
: Bindable, IValidatingBindable
{
private readonly Dictionary<string, IReadOnlyCollection<object>> _errors = new Dictionary<string, IReadOnlyCollection<object>>();

#region IValidatingViewModel

/// <inheritdoc />
[IsDirtyIgnored]
[PropertySource(nameof(HasErrors))]
public virtual bool IsValid => !HasErrors;
public virtual bool HasErrors => _errors.Count > 0;

/// <inheritdoc />
public virtual IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName))
return _errors.SelectMany(kvp => kvp.Value);
return _errors.TryGetValue(propertyName, out var errors) ? errors : Enumerable.Empty<object>();
}

/// <summary>
/// Set validation errors of a property.
/// </summary>
/// <param name="errors">The errors of the property.</param>
/// <param name="propertyName">The name of the property.</param>
/// <exception cref="ArgumentNullException"><paramref name="propertyName"/> is <see langword="null"/> or empty.</exception>
public virtual void SetErrors(IEnumerable errors, [CallerMemberName] string propertyName = null)
protected virtual void SetErrors(IEnumerable errors, [CallerMemberName] string propertyName = null)
{
if (String.IsNullOrEmpty(propertyName))
throw new ArgumentNullException(nameof(propertyName));
Expand All @@ -44,25 +51,9 @@ public virtual void SetErrors(IEnumerable errors, [CallerMemberName] string prop
}
}

#endregion

#region INotifyDataErrorInfo

/// <inheritdoc />
[IsDirtyIgnored]
public virtual bool HasErrors => _errors.Count > 0;

/// <inheritdoc />
public virtual IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName))
return _errors.SelectMany(kvp => kvp.Value);
return _errors.TryGetValue(propertyName, out var errors) ? errors : Enumerable.Empty<object>();
}

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

/// <summary>
/// Raises an event on <see cref="INotifyDataErrorInfo.ErrorsChanged"/> to indicate that the validation errors have changed.
/// </summary>
Expand All @@ -71,7 +62,5 @@ protected virtual void NotifyErrorsChanged([CallerMemberName] string propertyNam
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}

#endregion
}
}
}
11 changes: 10 additions & 1 deletion src/Smaragd/ViewModels/ViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace NKristek.Smaragd.ViewModels
{
/// <inheritdoc cref="IViewModel" />
public abstract class ViewModel
: Bindable, IViewModel
: ValidatingBindable, IViewModel
{
private readonly INotificationCache _notificationCache = new NotificationCache();

Expand Down Expand Up @@ -127,6 +127,15 @@ private void OnChildCollectionChanged(object sender, NotifyCollectionChangedEven
IsDirty = true;
}

/// <inheritdoc />
[IsDirtyIgnored]
public override bool HasErrors => base.HasErrors;

/// <inheritdoc />
[IsDirtyIgnored]
[PropertySource(nameof(HasErrors))]
public virtual bool IsValid => !HasErrors;

private bool _isDirty;

/// <inheritdoc />
Expand Down
207 changes: 207 additions & 0 deletions test/Smaragd.Tests/ViewModels/ValidatingBindableTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using NKristek.Smaragd.ViewModels;
using Xunit;

namespace NKristek.Smaragd.Tests.ViewModels
{
public class ValidatingBindableTests
{
private class TestBindable
: ValidatingBindable
{
private int _property;

public int Property
{
get => _property;
set => SetProperty(ref _property, value);
}

private int _anotherProperty;

public int AnotherProperty
{
get => _anotherProperty;
set => SetProperty(ref _anotherProperty, value);
}

public void NotifyPropertyChangingExternal(string propertyName)
{
NotifyPropertyChanging(propertyName);
}

public void NotifyPropertyChangedExternal(string propertyName)
{
NotifyPropertyChanged(propertyName);
}

public bool HasNotifiedErrorsChanged;

protected override void NotifyErrorsChanged([CallerMemberName] string propertyName = null)
{
base.NotifyErrorsChanged(propertyName);
HasNotifiedErrorsChanged = true;
}

public void NotifyErrorsChangedExternal(string propertyName)
{
NotifyErrorsChanged(propertyName);
}

public void SetErrorsExternal(IEnumerable errors, [CallerMemberName] string propertyName = null)
{
SetErrors(errors, propertyName);
}
}

#region SetErrors

[Fact]
public void SetErrors_throws_ArgumentNullException_when_propertyName_is_null()
{
var viewModel = new TestBindable();
Assert.Throws<ArgumentNullException>(() => viewModel.SetErrorsExternal(Enumerable.Empty<string>(), null));
}

[Fact]
public void SetErrors_propertyName_is_set_by_CallerMemberName()
{
var invokedErrorChangedEvents = new List<string>();
var viewModel = new TestBindable();
viewModel.ErrorsChanged += (sender, args) => invokedErrorChangedEvents.Add(args.PropertyName);
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1));
Assert.Contains(nameof(SetErrors_propertyName_is_set_by_CallerMemberName), invokedErrorChangedEvents);
}

[Fact]
public void SetErrors_sets_errors_of_property()
{
var errors = Enumerable.Repeat("error", 1);
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(errors, nameof(viewModel.Property));
Assert.Equal(errors, viewModel.GetErrors(nameof(viewModel.Property)));
}

[Fact]
public void SetErrors_null_removes_errors_of_property()
{
var errors = Enumerable.Repeat("error", 1);
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(errors, nameof(viewModel.Property));
viewModel.SetErrorsExternal(null, nameof(viewModel.Property));
Assert.Empty(viewModel.GetErrors(nameof(viewModel.Property)));
}

[Fact]
public void SetErrors_empty_collection_removes_errors_of_property()
{
var errors = Enumerable.Repeat("error", 1);
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(errors, nameof(viewModel.Property));
viewModel.SetErrorsExternal(Enumerable.Empty<string>(), nameof(viewModel.Property));
Assert.Empty(viewModel.GetErrors(nameof(viewModel.Property)));
}

[Fact]
public void SetErrors_notifies_when_errors_changed()
{
var viewModel = new TestBindable();
Assert.False(viewModel.HasNotifiedErrorsChanged);
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1), nameof(viewModel.Property));
Assert.True(viewModel.HasNotifiedErrorsChanged);
}

#endregion

#region HasErrors

[Fact]
public void HasErrors_is_true_when_validation_errors_exist()
{
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1), nameof(viewModel.Property));
Assert.True(viewModel.HasErrors);
}

[Fact]
public void HasErrors_is_false_when_no_validation_errors_exist()
{
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1), nameof(viewModel.Property));
viewModel.SetErrorsExternal(Enumerable.Empty<string>(), nameof(viewModel.Property));
Assert.False(viewModel.HasErrors);
}

[Fact]
public void HasErrors_gets_notified_before_errors_change()
{
var invokedPropertyChangingEvents = new List<string>();
var viewModel = new TestBindable();
viewModel.PropertyChanging += (sender, args) => invokedPropertyChangingEvents.Add(args.PropertyName);
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1), nameof(viewModel.Property));
Assert.Contains(nameof(viewModel.HasErrors), invokedPropertyChangingEvents);
}

[Fact]
public void HasErrors_gets_notified_after_errors_change()
{
var invokedPropertyChangedEvents = new List<string>();
var viewModel = new TestBindable();
viewModel.PropertyChanged += (sender, args) => invokedPropertyChangedEvents.Add(args.PropertyName);
viewModel.SetErrorsExternal(Enumerable.Repeat("error", 1), nameof(viewModel.Property));
Assert.Contains(nameof(viewModel.HasErrors), invokedPropertyChangedEvents);
}

#endregion

#region GetErrors

[Theory]
[InlineData(null, 2)]
[InlineData("", 2)]
[InlineData(nameof(TestBindable.Property), 1)]
[InlineData("NotExistingProperty", 0)]
public void GetErrors_with_error(string propertyName, int expectedErrorCount)
{
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(Enumerable.Repeat("Value has to be at least 5.", 1), nameof(TestBindable.Property));
viewModel.SetErrorsExternal(Enumerable.Repeat("Value has to be at least 5.", 1), nameof(TestBindable.AnotherProperty));
Assert.Equal(Enumerable.Repeat("Value has to be at least 5.", expectedErrorCount), viewModel.GetErrors(propertyName));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(nameof(TestBindable.Property))]
[InlineData("NotExistingProperty")]
public void GetErrors_without_error(string propertyName)
{
var viewModel = new TestBindable();
viewModel.SetErrorsExternal(Enumerable.Repeat("Value has to be at least 5.", 1), nameof(TestBindable.Property));
viewModel.SetErrorsExternal(Enumerable.Empty<string>(), nameof(viewModel.Property));
Assert.Empty(viewModel.GetErrors(propertyName));
}

#endregion

#region NotifyErrorsChanged

[Fact]
public void NotifyErrorsChanged_raises_event_on_ErrorsChanged()
{
var invokedErrorChangedEvents = new List<string>();
var viewModel = new TestBindable();
viewModel.ErrorsChanged += (sender, args) => invokedErrorChangedEvents.Add(args.PropertyName);

viewModel.NotifyErrorsChangedExternal(nameof(viewModel.Property));

Assert.Equal(Enumerable.Repeat(nameof(viewModel.Property), 1), invokedErrorChangedEvents);
}

#endregion
}
}
Loading

0 comments on commit af9f743

Please sign in to comment.