New Posts New Posts RSS Feed: Entities marked as HasChanged when a property hasn't really changed
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

Entities marked as HasChanged when a property hasn't really changed

 Post Reply Post Reply Page  <12
Author
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Topic: Entities marked as HasChanged when a property hasn't really changed
    Posted: 29-Sep-2009 at 8:01pm
[Rewritten 9/30/2009 to correct major misunderstanding on my part.]
 
You are correct. Let's look at why, first by seeing how the property set logic works and then by interpreting your experience.
 
How It Works
 
I peeked at the implementation for v.5.2.2 and here is what I see.
 
There are three potential groups of actions:
(1) pre-set interceptor actions,
(2) the actions DevForce performs related to setting the property value,
(3) post-set interceptor actions.
 
You can write interceptors that become actions in groups #1 and #3. DevForce owns group #2.
 
After each interceptor, DevForce checks to see if you canceled. If you did, all property set processing quits at that point. If you set cancel in a pre-set interceptor, none of the remaining pre-set interceptors nor the Group #2 and #3 actions will be performed; the property value will not change.
 
When you had a guard interceptor in the pre-set group that canceled because the incoming value matched the current value, the property set would bail out after completing the pre-set Group #1, before entering Group #2.
 
Group #2 is where DevForce potentially updates the property value. If there is no difference between the current and the incoming value, DevForce does not touch the current value, it does not change the EntityState, it won't raise PropertyChanged ... and it won't touch the Cancel flag either.
 
The way I read the code, no matter what DevForce does in Group #2, it will not touch the "Cancel" flag; that flag belongs to you and your interceptor chain.
 
My understanding is that you cannot affect what happens in Group #2. I'm a little fuzzy on this but it seems to be so even, for example, if the property fails validation. I'm prepared to be wrong about this; why would we bother checking the Cancel flag before entering Group #3 if it couldn't be changed in Group #2? I don't know as I write this.
 
For now you should assume that the Canceled flag will always be false coming out of Group #2 as it surely will be in your example.
 
DevForce, having processed Group #2, proceeds unhindered to Group #3 post-set actions.
 
For what it's worth, I agree with the way we do this today. I think it is correct to process post-set interceptors even if the property value is not changed. We don't know what you want to do post-set. You might want to take some action whether or not the property value changed. Maybe you want to log the attempt to change. We shouldn't guess.
 
What Happened To You
 
I'm betting you removed that guard interceptor - the one you called "BeforeSettingAnyPropertyCheckToNotDirtyForNoReason". Therefore, there was nothing to set the Canceled flag to true before it got to the post-set interceptor action group. 
 
The Canceled flag was false upon entering the Group #3 post-set interceptor actions and your post-set interceptors were invoked.
 
If you don't want to process post-set interceptors, you should put guard logic in them.


Edited by WardBell - 30-Sep-2009 at 1:02pm
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 7:31am
Three words: Cool. Crap. How?
Cool: Thank you. This explanation is very helpful. I was under the mistaken impression that the guard interceptor with a very low Order number: [BeforeSet(Order = -999999)] WOULD cancel the whole chain, but now I see it will only cancel the next group. Hmmm.
Crap: By removing the guard on BeforeSet, the code in group#2 is not setting the unchanged property, as expected, but the code in group#3 is running regardless.
How? So, how do I move the guard logic into each AfterSet interceptor in group#3 so that it aborts when it encounters an unchanged value.

My guard code currently fires in a BeforeSet and does this:

[BeforeSet(Order = -999999)]
public void BeforeSettingAnyPropertyCheckToNotDirtyForNoReason(IPropertyInterceptorArgs args)
{
    var dataArgs = args as IDataEntityPropertyInterceptorArgs;
    if (dataArgs != null)
    {
        var oldValue = dataArgs.DataEntityProperty.GetValue(dataArgs.Instance, EntityVersion.Current);
        args.Cancel = NewValueEqualsOldValue(args, oldValue);
    }
    var navArgs = args as INavigationEntityPropertyInterceptorArgs;
    if (navArgs != null)
    {
        var oldValue = navArgs.NavigationEntityProperty.GetValue(navArgs.Instance, EntityVersion.Current);
        args.Cancel = NewValueEqualsOldValue(args, oldValue);
    }
}

private bool NewValueEqualsOldValue(IPropertyInterceptorArgs args, object oldValue)
{
    bool valuesAreEqual = false;
    object newValue = args.Value;
    //changed first test to evaluate null and NullEntity to be equal 8/30/2009
    //re-evaluate if this presents a problem
    if ((Utility.Tools.IsNull(oldValue)) && (Utility.Tools.IsNull(newValue)))
        //both are null, nothings changed, cancel the update
        valuesAreEqual = true;
    else if (oldValue == null && newValue != null || oldValue != null && newValue == null)
        //one is null, don't cancel
        valuesAreEqual = false;
    else if (oldValue.Equals(newValue))
        //they're equal, nothings changed, cancel the update
        valuesAreEqual = true;
    return valuesAreEqual;
}


I tried moving that logic to an AfterSet interceptor, but the newValue and oldValue are always the same, and are the newValue. So the values are always equal and the guard test always fails. For example:

[AfterSet(EntityPropertyNames.BusAssociate)]
public void BusAssociateChanged(INavigationEntityPropertyInterceptorArgs navArgs)
{
    if (!NewValueEqualsOldValue(navArgs))
    {
        this.GmsBrokerAcctNbr = null;
        this.RatePlanId = null;
    }
}

protected bool NewValueEqualsOldValue(INavigationEntityPropertyInterceptorArgs navArgs)
{
    var oldValue = navArgs.NavigationEntityProperty.GetValue(navArgs.Instance, EntityVersion.Original);
    return NewValueEqualsOldValue(navArgs, oldValue);
}


Here, if the BusAssociate property is actually changing, I want to reset some other dependent properties, I don't want to reset those properties if the BusAssociate hasn't actually changed though.
In the NewValueEqualsOldValue, I've tried every one of the EntityVersion enumerations and still get back the new value.

Do you have any suggestions for how I can put such a guard method in the AfterSet interceptors?aaa

Edited by skingaby - 30-Sep-2009 at 7:35am
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 7:52am
Simple question: If a BeforeSet interceptor with a low Order number, sets the args.Cancel = true. Will subsequent BeforeSet interceptors with a higher Order number see that in their args.Cancel or is the args.Cancel different for each interceptor?

I.e. Will this work?
[BeforeSet(Order = 1)]
void ScrubTheMission(IPropertyInterceptorArgs args)
{ args.Cancel = true; }

[BeforeSet(Order = 2)]
void DoSomethingElse(IPropertyInterceptorArgs args)
{
   if(!args.Cancel) DoIt();
}
Back to Top
kimj View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 09-May-2007
Posts: 1391
Post Options Post Options   Quote kimj Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 9:56am
A cancel effectively terminates all further interceptor actions from being called.  So in your sample, DoSomethingElse will only be called if the entire setter action hasn't already been cancelled.  (This is true for either getters or setters.)
 
[Edit - oops, seems I've contradicted Ward.  Well, that's my reading of the current code base. :))


Edited by kimj - 30-Sep-2009 at 10:54am
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 11:11am
Hmmm... Yeh. So who's right?
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 12:27pm

When in doubt, trust Kim. I was reading the code; she knows the code.

And I should have written some test cases to verify my reading. I'm revising my post, not because I'm embarassed (which I am) but because I don't want someone to get it wrong.
 
Back with more in a second.
Back to Top
WardBell View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 31-Mar-2009
Location: Emeryville, CA,
Posts: 338
Post Options Post Options   Quote WardBell Quote  Post ReplyReply Direct Link To This Post Posted: 30-Sep-2009 at 1:34pm
I'm back after removing my mistake about when DevForce checks the Cancel flag (it checks after every interceptor, not after every interceptor group as I first thought).
 
Skingby, your issue remains. When you enter the post-set interceptors you don't know what the incoming value was and you don't know if DevForce pushed it into the entity (that is, set the current value to the incoming value) or decided that it should not do so.
 
You shouldn't assume, by the way, that if it did not do so, the reason is that the incoming value and the current value are the same. That's just one reason it might not update the current value. It is also possible that the incoming value failed validation and you changed your validation policy so that it does not throw an exception ... it simply refused to update the current value. So be careful about the assumptions you make (and that's great advice for me too).
 
I have a workaround. The IPropertyInterceptorArgs passed into every interceptor has a Tag property that takes an object. You get a fresh args object at the start of each property set and then this args object is passed from interceptor to interceptor until the property set concludes (by what ever means). That means you can stuff something into the tag in a pre-set and retrieve it in a post-set interceptor.
 
You may choose to stuff the old value for the property into the tag in a pre-set interceptor. In a post-set interceptor, if the "args.Tag == args.Value" you may choose to conclude that DevForce did not update the entity with the incoming value.
 
As your code makes all-too-clear, my simple "args.Tag == args.Value" is not up to the challenge. It is pseudo-code and you are working way too hard to do it for real.
 
I have put in a feature request to make it easier to know what DevForce did in Group #2. You will be able to inspect the args in your post-set to find "what you need" without having to write the code you showed here. I'm thinking you should be able to know:
 
- the incoming value
- the value of args.Value (which could have morphed in an interceptor) as it entered Group #2
- whether or not DevForce updated the current value
 
I can't say precisely what we will do nor when we will do it ... because I have to discuss it and plan for it with the team. But I've heard you and we will address the issue.
 
I hope the "Tag" trick tides you over until then.
Back to Top
mulpurir View Drop Down
Newbie
Newbie
Avatar

Joined: 10-Feb-2010
Location: Concord
Posts: 1
Post Options Post Options   Quote mulpurir Quote  Post ReplyReply Direct Link To This Post Posted: 10-Feb-2010 at 6:01pm
Is this feature is released.
Back to Top
skingaby View Drop Down
DevForce MVP
DevForce MVP
Avatar

Joined: 23-Apr-2008
Location: United States
Posts: 146
Post Options Post Options   Quote skingaby Quote  Post ReplyReply Direct Link To This Post Posted: 11-Feb-2010 at 10:57am
With regard to the last post from Ward, No.

But the rest of this thread works, including all the quirkiness described in this thread.

In summary, as I understand it, when an object's data bound property (i.e the Ideablade generated ones) is set:
1) First, the BeforeSet interceptors fire, in random order, unless you use the "Order" parameter in the BeforeSet signature. If any of the BeforeSets sets Cancel to true, then processing on that property stops right there.
2) Second, the Property Set behavior inside the Ideablade framework happens.
3) Third, the AfterSet interceptors fire, again, in random order unless you use the Order parameter, and again, if any one of the, sets Cancel to true, the processing stops right there.

Note, Ideablade changed the behavior a couple of versions ago so that when setting a property to the value it already held, it no longer sets the object to dirty. However, that is only relevant to step 2 above, their code. All of your BeforeSet and AfterSet interceptors will still fire, regardless of the fact that the property has not changed.

This has the effect of causing chained setters to do their processing, even when nothing has actually changed.
For example, suppose you have nullable MinDate and MaxDate properties of an object. These are bound to editable columns in a Grid. Silverlight's grid get's and set's the property every time you click. Don't like it? Tough. Unless you want to completely roll your own binding mechanism, that's just the way it is (for now?).
Anyway, suppose you have logic in an AfterSet event to make sure that if the user changes the MaxDate to before the MinDate, that you change the MinDate to that same day so the date range is not invalid. Your code in the MaxDate's AfterSet will have to check if both are null, if one's null and the other's not, if they're both not null and the MaxDate is less than the MinDate, change the MinDate down to the MaxDate value. Further, When the MaxDate is changed, you have to re-set some pricing in this object and its children based on that day's rate information.

So, you run your app and click in the MaxDate cell, then you click away. You did not change anything. The grid, however, dropped into edit mode (Get) and out again (Set). That Set (which happens automatically through the {Binding}), will fire the MaxDate's AfterSet interceptor, which will then, unnecessarily, run through the logic to deal with the MinDate, and it will run through the logic to set the pricing. And if there are Before/After Set interceptors on any of the pricing properties that are set, those will fire too, even though none of the pricing data changed because the date didn't change. In our case, we have to sprinkle if statements everywhere and hope that we can skip some of these because some of them are not insignificant processes. Grrr.

Personally, I would like ALL THREE steps to be skipped, but that is not how it is implemented today. Ward has suggested using the Set interceptor's args.Tag property to toss a value from before to after so you can do the necessary Cancel if no changes made. My code above provides one implementation of checking at the beginning of the BeforeSet stack to cancel if no change is made. I would much prefer that the framework stood guard and didn't do anything, ever, if the value is not being changed when it is being set.





Edited by skingaby - 11-Feb-2010 at 11:00am
Back to Top
 Post Reply Post Reply Page  <12

Forum Jump Forum Permissions View Drop Down