Define if a property is loaded or set by the view

247 views Asked by At

I'm trying to make a "Dirty" implementation with Catel.
I have a viewmodel, with a [Model] property and a few [ViewModelToModel] mapped to it.
And I added a boolean member _canGetDirty, that when set to true allows viewmodel properties to prompt a service for saving.

So my logic is that if the model property changes, _canGetDirty is set to false, so the viewmodel properties change without getting dirty, and when the model is done changing we set _canGetDirty to true anew.

The problem is that the PropertyChanged event for the model property is called before the viewmodel properties are changed, hence _canGetDirty is always true, and my service is called for saving whenever I load a new model.

How to work around this issue?

public class MyViewModel : ViewModelBase
{
    private IMyService _myService;
    private bool _canGetDirty;

    public MyViewModel(MyModel myModel, IMyService myService)
    {
        MyModel = myModel;
        _myService = myService;
    }

    [Model]
    public MyModel MyModel
    {
        get { return GetValue<MyModel>(MyModelProperty); }
        set
        {
            _canGetDirty = false;
            SetValue(MyModelProperty, value);
        }
    }

    [ViewModelToModel("MyModel")]
    public string Prop1
    {
        get { return GetValue<string>(Prop1Property); }
        set { SetValue(Prop1Property, value); }
    }

    [ViewModelToModel("MyModel")]
    public string Prop2
    {
        get { return GetValue<string>(Prop2Property); }
        set { SetValue(Prop2Property, value); }
    }

    [ViewModelToModel("MyModel")]
    public string Prop3Contains
    {
        get { return GetValue<string>(Prop3Property); }
        set { SetValue(Prop3Property, value); }
    }

    #region Registering
    public static readonly PropertyData Prop1Property = RegisterProperty("Prop1", typeof(string), null, PropertyToSaveChanged);
    public static readonly PropertyData Prop2Property = RegisterProperty("Prop2", typeof(string), null, PropertyToSaveChanged);
    public static readonly PropertyData Prop3Property = RegisterProperty("Prop3", typeof(string), null, PropertyToSaveChanged);
    public static readonly PropertyData MyModelProperty = RegisterProperty("MyModel", typeof(MyModel), null, MyModelChanged);
    #endregion

    #region Property Changed Handlers
    private static void MyModelChanged(object sender, PropertyChangedEventArgs e)
    {
        (sender as MyViewModel)._canGetDirty = true;
    }

    private static void PropertyToSaveChanged(object sender, PropertyChangedEventArgs e)
    {
        var vm = sender as MyViewModel;

        if (vm._canGetDirty)
            vm._myService.AskForSaving();
    }
    #endregion
}

Edit: some explanation on how Catel works in this context.

Registered properties changes:

We register properties that will notify updates with RegisterProperty:

public static readonly PropertyData Prop1Property = RegisterProperty("Prop1",
    typeof(string), null, PropertyToSaveChanged);

The last parameter is a callback function called when the registered property changes.

Automatic update of model's properties:

We set a property as a Model:

[Model]
public MyModel MyModel
{
    get { return GetValue<MyModel>(MyModelProperty); }
    set
    {
        _canGetDirty = false;
        SetValue(MyModelProperty, value);
    }
}

This class contains a few properties (Prop1, Prop2, Prop3). Catel allows us to automatically update them from the viewmodel by mapping them with ViewModelToModel:

[ViewModelToModel("MyModel")]
public string Prop1
{
    get { return GetValue<string>(Prop1Property); }
    set { SetValue(Prop1Property, value); }
}
2

There are 2 answers

1
ΩmegaMan On

Assuming that ViewModelBase adheres to INotifyPropertyChanged subscribe to the classes' INotifyPropertyChanged event and set the dirty flag there instead of subscribing to individual change events.

By definition that should happen after any value is set.

Example as

public MyViewModel(MyModel myModel, IMyService myService)
 {
    ...
    this.PropertyChanged += (sender, args) => 
                            {
                                if (_canGetDirty)
                                    _myService?.AskForSaving();
                            };
 }

You could weed out any race condition logic on the mode with the args.PropertyName check.

1
Geert van Horrik On

First of all, I recommend using Catel.Fody, that simplifies your property registration a lot (and yes, it also supports change callbacks ;-) ).

Why would the model externally change? When the model changes (which gets injected into your ctor), it should recreate a new VM and thus you can start with a new slate.

Back to this issue: did you test whether your setter is actually being called? It could be possible that Catel internally directly calls SetValue (equal to the behavior of dependency properties) instead of calling the wrapper in your vm. Catel works this way:

  1. You update the model
  2. The change callback calls (so you set _canBeDirty => true)
  3. The vm notices that the model has been changed and updates the exposes / linked properties.

My suspicion is basically that you are setting _canBeDirty => true too early.