How to make IsDirty working with private backing fields

How to make IsDirty working with private backing fields

Old forum URL: forums.lhotka.net/forums/t/11259.aspx


Miroslav Galajda posted on Monday, March 26, 2012

 

Hi all,

I need to solve the problem with IsDirty when using private fields for object references.

When you change the business property implemented as a private backing field, the IsDirty doesn't change.

I know the reason but I'm not satisfied with this knowledge :-)

The value of the property implemented as private backing field is stored in that backing field and so it bypasses the FieldManager, which is responsible for holding values and the state for IsDirty checking.

Has anyone ever had the need for IsDirty working with private backing fields?

 

Yours sincerely

Miroslav Galajda

 

JonnyBee replied on Monday, March 26, 2012

Hi,

Use SetProperty/GetProperty overloads that accepts the private field as ref parameter in your business object.

Miroslav Galajda replied on Monday, March 26, 2012

Hi, this is exactly how I'm using it.

As I described the situation in the question, this API doesn't help and doesn't work that way.

I've debug version of CSLA 4.2 and it doesn't go through FieldManager so it can't store possible detected change.

Any other suggestions?

Thank you

 

Yours sincerely

Miroslav Galajda

JonnyBee replied on Monday, March 26, 2012

This code runs as expected and makes the object dirty:

namespace MyTestProject
{
    public class PropertyGetSet : Csla.BusinessBase<PropertyGetSet>
    {
        public static readonly PropertyInfo<string> NameProperty = RegisterProperty<string>(c => c.Name,
                                                                                            RelationshipTypes.
                                                                                                PrivateField);

        private string _name = NameProperty.DefaultValue;

        public string Name
        {
            get { return GetProperty(NameProperty, _name); }
            set { SetProperty(NameProperty, ref _name, value); }

        }

        public static PropertyGetSet Fetch()
        {
            return DataPortal.Fetch<PropertyGetSet>();
        }

        public void DataPortal_Fetch()
        {
            MarkClean();
        }
    }

    [TestClass]
    public class TestPropertyGetSet
    {
        [TestMethod]
        public void IsDirtyAfterPropertySet()
        {
            var obj = PropertyGetSet.Fetch();
            Assert.IsFalse(obj.IsDirty);
            obj.Name = "Miroslav";
            Assert.IsTrue(obj.IsDirty);
        }
    }
}

JonnyBee replied on Monday, March 26, 2012

If your child is a BusinessObject then you need to make sure to call OnAddEventHooks when object is set and in OnDeserialized:

        public void DataPortal_Fetch()
        {
            _child = PropertyGetSetChild.Fetch();
            OnAddEventHooks(_child);
            MarkClean();
        }

        protected override void OnDeserialized(System.Runtime.Serialization.StreamingContext context)
        {
            base.OnDeserialized(context);
            OnAddEventHooks(_child);
        }

and override IsValid and IsDirty in the parent object:

        public override bool IsDirty
        {
            get { return base.IsDirty || _child.IsDirty; }
        }

        public override bool IsValid
        {
            get { return base.IsValid || _child.IsDirty; }
        }

JonnyBee replied on Monday, March 26, 2012

These tests run OK:


namespace MyTestProject
{
    public class PropertyGetSet : Csla.BusinessBase<PropertyGetSet>
    {
        public static readonly PropertyInfo<string> NameProperty = RegisterProperty<string>(c => c.Name,
                                                                                            RelationshipTypes.
                                                                                                PrivateField);

        private string _name = NameProperty.DefaultValue;

        public string Name
        {
            get { return GetProperty(NameProperty, _name); }
            set { SetProperty(NameProperty, ref _name, value); }
        }

        public static readonly PropertyInfo<PropertyGetSetChild> ChildProperty = RegisterProperty<PropertyGetSetChild>(c => c.Child, RelationshipTypes.PrivateField);
        private PropertyGetSetChild _child = ChildProperty.DefaultValue;
        public PropertyGetSetChild Child
        {
            get { return GetProperty(ChildProperty, _child); }
            set { SetProperty(ChildProperty, ref _child, value); }
        }

        public static PropertyGetSet Fetch()
        {
            return DataPortal.Fetch<PropertyGetSet>();
        }

        public override bool IsDirty
        {
            get { return base.IsDirty || _child.IsDirty; }
        }

        public override bool IsValid
        {
            get { return base.IsValid || _child.IsDirty; }
        }

        public void DataPortal_Fetch()
        {
            _child = PropertyGetSetChild.Fetch();
            OnAddEventHooks(_child);
            MarkClean();
        }

        protected override void OnDeserialized(System.Runtime.Serialization.StreamingContext context)
        {
            base.OnDeserialized(context);
            OnAddEventHooks(_child);
        }
    }

    public class PropertyGetSetChild : Csla.BusinessBase<PropertyGetSetChild>
    {
        public static readonly PropertyInfo<string> Name2Property = RegisterProperty<string>(c => c.Name2,
                                                                                            RelationshipTypes.
                                                                                                PrivateField);

        private string _name2 = Name2Property.DefaultValue;

        public string Name2
        {
            get { return GetProperty(Name2Property, _name2); }
            set { SetProperty(Name2Property, ref _name2, value); }
        }

        public static PropertyGetSetChild Fetch()
        {
            return DataPortal.FetchChild<PropertyGetSetChild>(null);
        }

        public void Child_Fetch(object criteria)
        {
            MarkClean();
        }
    }

    [TestClass]
    public class TestPropertyGetSet
    {
        [TestMethod]
        public void IsDirtyAfterPropertySet()
        {
            var obj = PropertyGetSet.Fetch();
            Assert.IsFalse(obj.IsDirty);
            obj.Name = "Miroslav";
            Assert.IsTrue(obj.IsDirty);
        }

        [TestMethod]
        public void IsDirtyAfterChildPropertySet()
        {
            var obj = PropertyGetSet.Fetch();
            Assert.IsFalse(obj.IsDirty);
            obj.Child.Name2 = "Miroslav";
            Assert.IsTrue(obj.IsDirty);
        }
    }
}

Miroslav Galajda replied on Monday, March 26, 2012

Hi, 

 

Yes I have the problem with child object. Thanks I will try.

 

Yours sincerely 

Miroslav Galajda

tiago replied on Monday, March 26, 2012

JonnyBee

If your child is a BusinessObject then you need to make sure to call OnAddEventHooks when object is set and in OnDeserialized:

 

 

Hi Jonny,

Must have missed the OnAddEventHooks thing . When was it introduced? Reading code documentation it's not clear what its purpose. Can you shed some light on the subject?

 

JonnyBee replied on Tuesday, March 27, 2012

Actually, there is 2 important procedures:

The purpose is to make the "parent" observe events in the child object and raise own events to notify UI / listeners of chenges.
This is necessary for the

events to work as expected.

It's necessary to make UI helpers like PropertyStatus / ObjectStatus / ReadWriteAuthorization to work as expected as they hook into one or more of these events.

Not exactly sure when these made it into the framework but they do exist in Csla 3.6 and forward.

Miroslav Galajda replied on Tuesday, March 27, 2012

Well, finally I found the reason why your code is working well and my not.

I've incorporated the "Custom FieldData Classes" into my business layer from the article by Jason Bock: http://jasonbock.net/jb/Default.aspx?blog=entry.9cc70d85bef34e2b9a683ba82615f8a3.

The reason I've used this is to have the feature of determining whether the value was changed against the original value, e.g. you change the property several times to different values but finally you change it to the original value so the object should not indicate that IsDirty is true.

The main problem with the solution provided by Jason Bock, is  that it uses the FieldData to check the dirty flag, and unfortunatelly properties implemented as private backing fields bypass the FieldManager.

There is overriden IsSelfDirty property as following:

    public override bool IsSelfDirty
    {
        get
        {
            var isSelfDirty = false;

            foreach(var registeredProperty in this.FieldManager.GetRegisteredProperties())
            {
                if(registeredProperty.RelationshipType.HasFlag(RelationshipTypes.Child) &&
                    this.FieldManager.IsFieldDirty(registeredProperty))
                {
                    isSelfDirty = true;
                    break;
                }
            }

            return isSelfDirty;
        }
    }
Original implementation, in CSLA, simply returns _isDirty field, which is correctly set to true.

Do you have any suggestions?

Thank you

Yours sincerely
Miroslav Galajda

JonnyBee replied on Tuesday, March 27, 2012

Hi.

Change to use this code

        public override bool IsSelfDirty
        {
            get
            {
                if (IsDeleted) return true;
 
                foreach (var registeredProperty in this.FieldManager.GetRegisteredProperties())
                {
                    // skip private backing fields 
                    if ((registeredProperty.RelationshipType & RelationshipTypes.PrivateField) == RelationshipTypes.PrivateField) 
                        continue;
 
                    // skip fields that have not been set yet
                    if (!FieldManager.FieldExists(registeredProperty) continue; 

                    // skip child objects that implement ITrackStatus                     var fieldData = FieldManager.GetFieldData(registeredProperty);                     if (fieldData.Value is ITrackStatuscontinue;                      if (FieldManager.IsFieldDirty(registeredProperty))                     {                         return true;                     }                 }                 return false;             }         }

and override IsSelfDirty in your business object to return

        public override bool IsSelfDirty
        {
            get { return base.IsSelfDirty || _child.IsDirty; }
        }

And you do not have to override IsDirty in your BO.:

 

 

 

Miroslav Galajda replied on Tuesday, March 27, 2012

Thank you, but I don't know how would it help me.

Maybe we don't understand each other.

When I return to your example where PropertyGetSet class has a PropertyGetSetChild as Child.

What I want to do is that I change the child reference in the root object and I want to use it as general principle so I don't want to override IsDirty in each case to include each child.

var obj = PropertyGetSet.Fetch();

Assert.IsFalse(obj.IsDirty);
obj.Child = Child.NewChild(); // set the new child instance
Assert.IsTrue(obj.IsDirty);

So expected result is that obj detects the change without modifying IsDirty in the PropertyGetSet class (obj).

JonnyBee replied on Tuesday, March 27, 2012

That is not how IsDirty should be handled!

You should NOT change/set new child object into parent object and expect that to change "IsSelfDirty".  When a child object implements "ITrackStatus" then IsDirty is propagated down to the child object itself. 

If the child has IsNew then by default - Csla will consider the object as Dirty.

You are using the CustomFieldData that is NOT a supported addin in CSLA. The purpose of CustomFieldData is to change the IsDirty check to test foractual changes in fields. Ie Edit field with new value = dirty, edit field to old value = not Dirty.

Take f.ex: LazyLoading a child from a datasource (maybe even another data source) should not make the object tree dirty. Only if any of the property values is changed should the object tree become dirty.

Miroslav Galajda replied on Tuesday, March 27, 2012

I will describe the problem more closely.

The last example I used was just the modified version of yours.

The child property in my context is actually only a reference to a child in the object tree in another subtree.

So I have businessbindinglistbase in one subtree and I have reference to one of the items in that list in another part of the object tree.

And the behavior is that I can select any of the item from that child list and assign it to another child as a reference.

This is refered in the csla book as an Inter-Graph Reference.

All I want to do is use this style of reference while using the custom field data with previously described change detection.

Is it possible?

 

Thank you

 

Yours sincerely

Miroslav Galajda

JonnyBee replied on Tuesday, March 27, 2012

Hi.

I prefer to use a Key value for the "data" reference (usually a managed property).

The intergraph reference should not be part of the "undo" operation (and thus should not be viewed as a "data" property).
So my preference is to regard  intergraph references as navigation property only to get additional values on that referenced object.

Also, this allows the Key value to be part of an Undo operation and after the undo operation you should reestablish/verify the intergraph reference based on the (possible reverted) Key value.

I believe the same goes if you have a remote data portal - intergraph references needs to be reestablished after the object graph is deserialized.
My projects are typically edge applications so I may be wrong about the remote data portal.

See also Using Csla 4 - Creating Business Objects section "Maintaining a reference" on page 36.

Miroslav Galajda replied on Wednesday, March 28, 2012

What I've previously described is business requirement that I can't do without. I need to intergraph reference as data and be able to detect if the object with a such reference has changed if there is set another reference.

Then it remains on me nothing more than to modify CSLA.

RockfordLhotka replied on Wednesday, March 28, 2012

You may not have to modify CSLA as much as override OnPropertyChanging or OnPropertyChanged or perhaps best is PropertyHasChanged. That way you'll know that the property has changed and you can write code to compare with the original value or whatever other value you are storing.

Copyright (c) Marimer LLC