MVVM-Samples icon indicating copy to clipboard operation
MVVM-Samples copied to clipboard

Modified / IsDirty flag in ObservableObject ?

Open stephenhauck opened this issue 2 years ago • 2 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 ?

stephenhauck avatar May 16 '22 23:05 stephenhauck

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);

robertodalmonte avatar Jun 08 '22 05:06 robertodalmonte

Interesting ...... will review this ..... I was expecting it since Fody has done it for quite a while now ..... The localized errors is interesting ...

THANKS!

stephenhauck avatar Jun 08 '22 12:06 stephenhauck