Modified / IsDirty flag in ObservableObject ?
stephenhauck opened this issue · comments
I like that you have an observable object but did I miss how to track changes when it's modified or is that not in the object model ?
Hello Stephen,
this is the approach I followed ...take it with a grain of salt.
It's a "let's see if it works" try.:)
I also needed to be able to localize the error messages
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections;
using System.ComponentModel;
namespace OneClick.ViewModels;
internal partial class EditableViewModel : ObservableObject, INotifyDataErrorInfo
{
private readonly List _canBeDirtyProperties = new();
private readonly List _dirtyProperties = new();
private readonly Dictionary<string, List<string>> _propertyErrors = new();
[ObservableProperty]
[AlsoNotifyCanExecuteFor(nameof(SaveCommand))]
private bool _isDirty;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public bool CanSave => !HasErrors && IsDirty;
public bool HasErrors => _propertyErrors.Count > 0;
private Dictionary<string, object> SavedState { get; } = new();
public IEnumerable GetErrors(string? propertyName)
{
return _propertyErrors!.GetValueOrDefault(propertyName, new List<string>());
}
protected void AddError(string propertyName, string errorMessage)
{
if (!_propertyErrors.ContainsKey(propertyName))
{
_propertyErrors.Add(propertyName, new List<string>());
_propertyErrors[propertyName].Add(errorMessage);
}
OnErrorsChanged(propertyName);
}
protected void ClearErrors(string propertyName)
{
if (_propertyErrors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
var propertyName = e.PropertyName;
if (propertyName is not null && IsCanBeDirtyProperty(propertyName))
{
if (HasPropertyChanged(propertyName))
{
if (!_dirtyProperties.Contains(propertyName))
{
_dirtyProperties.Add(propertyName);
}
}
else
{
_dirtyProperties.Remove(propertyName);
}
}
IsDirty = _dirtyProperties.Count > 0;
}
[ICommand(CanExecute = nameof(CanSave))]
protected virtual void Save()
{
}
protected void SaveState()
{
foreach (var canBeDirtyProperty in _canBeDirtyProperties)
{
var value = GetPropValue(canBeDirtyProperty);
if (value is not null)
{
SavedState.Add(canBeDirtyProperty, value);
}
}
}
protected void SetCanBeDirtyProperties(IEnumerable<string> canBeDirtyProperties)
{
_canBeDirtyProperties.Clear();
_canBeDirtyProperties.AddRange(canBeDirtyProperties);
}
private object? GetPropValue(string propName)
{
return GetType().GetProperty(propName)?.GetValue(this, null);
}
private bool HasPropertyChanged(string propertyName)
{
return SavedState.ContainsKey(propertyName) && !SavedState[propertyName].Equals(GetPropValue(propertyName));
}
private bool IsCanBeDirtyProperty(string propertyName)
{
return _canBeDirtyProperties.Contains(propertyName);
}
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
}
internal partial class UserViewModel : EditableViewModel
{
[ObservableProperty]
[AlsoNotifyChangeFor(nameof(FullName))]
[AlsoNotifyCanExecuteFor(nameof(SaveCommand))]
private string _firstName = string.Empty;
[ObservableProperty]
[AlsoNotifyChangeFor(nameof(FullName))]
[AlsoNotifyCanExecuteFor(nameof(SaveCommand))]
private string _lastName = string.Empty;
public UserViewModel()
{
SetCanBeDirtyProperties(
new List<string>
{
nameof(FirstName),
nameof(LastName)
});
SaveState();
}
public UserViewModel(UserModel model)
{
SetCanBeDirtyProperties(
new List<string>
{
nameof(FirstName),
nameof(LastName)
});
FirstName = model.FirstName;
LastName = model.LastName;
SaveState();
}
public string FullName => $"{LastName} {FirstName}";
protected override void Save()
{
base.Save();
///Save your stuff
}
partial void OnFirstNameChanged(string value)
{
const string propertyName = nameof(FirstName);
if (!value.NameIsLegal())
{
AddError(propertyName, Localization.Resources.invalidFirstName);
}
else
{
ClearErrors(propertyName);
}
}
partial void OnLastNameChanged(string value)
{
const string propertyName = nameof(LastName);
if (!value.NameIsLegal())
{
AddError(propertyName, Localization.Resources.invalidLastName);
}
else
{
ClearErrors(propertyName);
}
}
}
internal record UserModel(string FirstName, string LastName);
Interesting ...... will review this .....
I was expecting it since Fody has done it for quite a while now .....
The localized errors is interesting ...
THANKS!