1

I want to set a dirty flag for any of the required properties in my view model. I initialize IsDirty to false in the constructor. Unfortunately all of the setters in my properties are called after the constructor. Is there a way I can set IsDirty to false after all of the setters? The setters all have a line IsDirty=true;

I'm using the Prism framework with Xamarin 4.0, but Prism documentation doesn't have anything on the ViewModel life cycle.

My redacted constructor looks like this:

public SomeDetailsViewModel(INavigationService navigationService) : base(navigationService)
{
    Sample = new SampleDTO();

    InitializeLookupValues();

    _samplesService = new SampleService(BaseUrl);

    TextChangedCommand = new Command(() => OnTextChanged());
    AddSampleCommand = new Command(() => AddCurrentSample());
    CancelCommand = new Command(() => Cancel());

    IsDirty = false;

}

Edit 3:

The constructor calls InitializeLookupValues(). These appear to be the culprit.

private async Task InitializeLookupValues()
        {
            App app = Prism.PrismApplicationBase.Current as App;
            string baseUrl = app.Properties["ApiBaseAddress"] as string;

            _lookupService = new LookupDataService(baseUrl);

            int TbId = app.CurrentProtocol.TbId;
            int accessionId = CollectionModel.Instance.Accession.AccessionId;
            Parts = await _lookupService.GetParts(accessionId);//HACK

            Containers = await _lookupService.GetSampleContainers(TbId);
            Additives = await _lookupService.GetAdditives(TbId);
            UnitsOfMeasure = await _lookupService.GetUnitsOfMeasure();
            
            // with a few more awaits not included.
        }

After exiting the constructor each of the properties are set. They look like this one.

public ObservableCollection<PartDTO> Parts
{
    get
    {
        return parts;
    }
    set
    {
        SetProperty(ref parts, value);
    }
}
private PartDTO part;
public PartDTO SelectedPart
{
    get
    {
        return part;
    }
    set
    {
        SetProperty(ref part, value);
        
        IsDirty = true;
    }
}

Where IsDirty is defined thus:

private bool isDirty;
public bool IsDirty
{
    get
    {
        return isDirty;
    }
    set
    {
        SetProperty(ref isDirty, value);
        Sample.DirtyFlag = value;
    }
}

I haven't explicitly set any of the properties. I would like to avoid their being initialized automatically, or call something after them.

Edit
Just a note to everyone I have been debugging to find out what I could. I found that in each data-bound property the getter is called twice, then the setter is called. I looked at what generated code I could find, and there is no obvious place where data binding is explicitly calling the setter.

Edit 2
What I hadn't shown before, and now looks likes it's a critical piece of information, was that I populate the ObservableCollection with an async call to a service. As far as I can tell, because of XAML data binding, the SelectedPart property setter is called. If I debug slowly this start to show in some places. I've added the async call above.

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
Blanthor
  • 2,568
  • 8
  • 48
  • 66
  • Who sets the properties? Have you put a breakpoint in the setter? – Haukinger Aug 11 '19 at 13:53
  • I did set breakpoints in the properties. It appears that they are automatically set right after the constructor is exited. At this point, I haven't written code to set the properties. It could be that the default values are called in the XAML. I'll check the generated file, when I'm at work. – Blanthor Aug 11 '19 at 15:00
  • You can check the value when the breakpoints is hit, then you can know who set the property through the value. – nevermore Aug 12 '19 at 06:36
  • 1
    Can you edit the code so that it would actually compile? `await` in the constructor will not compile. – Haukinger Aug 15 '19 at 13:32
  • @Haukinger Apologies, I was trying to simplify the code for readability. – Blanthor Aug 15 '19 at 14:16
  • @Haukinger Hopefully my corrections are sufficient now. – Blanthor Aug 15 '19 at 14:51
  • Edit in my answer, too, seeing your problem clearer now. – Haukinger Aug 15 '19 at 17:33

2 Answers2

1

Since the SetProperty methods are overridable you can inject some custom logic. This could be very useful for when you have objects that you need to validate if they have been altered.

public class StatefulObject : Prism.Mvvm.BindableBase
{
    private bool _isDirty;
    public bool IsDirty
    {
        get => _isDirty;
        private set => SetProperty(ref _isDirty, value);
    }

    protected override bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
    {
        var isDirty = base.SetProperty(ref storage, value, onChanged, propertyName);
        if(isDirty && propertyName != nameof(isDirty))
        {
            IsDirty = true;
        }

        return isDirty;
    }

    public void Reset() => IsDirty = false;
}

Keep in mind that when you initialize fields in this IsDirty would be true, so before binding you would want to call the Reset method to set IsDirty back to false that way you can reliably know when a field has been changed.

Note that how you handle this is somewhat up to you. For instance you might do this with Linq...

var fooDTOs = someService.GetDTOs().Select(x => { x.Reset(); return x; });

You might also enforce a pattern like:

public class FooDTO : StatefulObject
{
    public FooDTO(string prop1, string prop2)
    {
        // Set the properties...
        Prop1 = prop1;

        // Ensure IsDirty is false;
        Reset(); 
    }
}
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
Dan Siegel
  • 5,724
  • 2
  • 14
  • 28
  • How do I manage to time the Reset() call just before binding? Do you mean that I should call it from the view, before setting the BindingContext? – Blanthor Aug 12 '19 at 17:16
  • @Blanthor how you choose to use it is really up to you. It really doesn't matter exactly where you call the reset, you just want to make sure that IsDirty is false when the object is first presented for Binding. Or in otherwords before INotifyPropertyChanged will be called forcing a UI update. Keep in mind you could also expand on this if you say wanted to track which properties were updated or if it's the first time you've set the property vs changing the value of an existing property. – Dan Siegel Aug 12 '19 at 18:28
1

Is there a way I can set IsDirty to false after all of the setters?

The setters aren't called by themselves, there has to be someone calling them. You should identify who's doing that and either stop him from setting stuff without good reason (preferred) or make him reset the dirty flag after he's done.

As suggested in the comments, adding a breakpoint in the setter and having a look at the stacktrace is a good starting point for finding the source of the setting... if I had to guess, I'd suspect some navigation related callback.

But you should try to make sure that the view model is initialized after the constructor and that IsDirty actually means "has been changed through the view" and not "maybe changed by the user, might also be just part of a delayed initialization".

After your multiple edits, an edit from me:

You should modify your architecture to account for the asynchronous initialization of your view model. Just running everything in parallel and hoping for the best rarely works.

You could make the properties read-only until initialization is complete, for instance, and set IsDirty to false at the end of InitializeLookupValues.

Pseudo-Code:

Constructor()
{
    Task.Run( async () => await InitializeAsync() );
}

string Property
{
    get => _backingField;
    set
    {
        if (_isInitialized && SetProperty( ref _backingField, value ))
            _isDirty = true;
    }
}

private async Task InitializeAsync()
{
    await SomeAsynchronousStuff();
    _isInitialized = true;
}

private bool _isInitialized;
private bool _isDirty;

Probably, you want to expose _isInitialized as a property to the view to show some hourglass, and use a ManualResetEvent instead of a simple bool... but you get the idea.

Haukinger
  • 10,420
  • 2
  • 15
  • 28
  • Sure I've been debugging, but apparently not the right way. Note my edit 2 above along with an async call. There are a 7 calls to awaitable services. I'm now trying to determine whether WhenAll or WaitAll should be followed by IsDirty = false; – Blanthor Aug 15 '19 at 12:45
  • 1
    There's no such thing as an `async` constructor, so obviously `await` cannot be used in the constructor. You want to look up asynchronous initialization patterns... (e.g. https://blog.stephencleary.com/2013/01/async-oop-2-constructors.html) but whatever you do, you do _not_ want to block on asynchronous calls! – Haukinger Aug 15 '19 at 13:30
  • Corrected in Edit 3. My original post brought in calls from the InitializeLookupValues, rather than showing just enough :( Hopefully I didn't fail at that this time. – Blanthor Aug 15 '19 at 14:47
  • If you call an async method without awaiting it, it will run, well, asynchronously. It's expected behaviour for it to run in parallel with the rest of the constructor, and it's pure luck whether initialization terminates first or `IsDirty` is set to `false` first. – Haukinger Aug 15 '19 at 17:23