Print Page | Close Window

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

Printed From: IdeaBlade
Category: DevForce
Forum Name: DevForce 2009
Forum Discription: For .NET 3.5
URL: http://www.ideablade.com/forum/forum_posts.asp?TID=1404
Printed Date: 28-Mar-2024 at 5:34am


Topic: Entities marked as HasChanged when a property hasn't really changed
Posted By: skingaby
Subject: Entities marked as HasChanged when a property hasn't really changed
Date Posted: 31-Jul-2009 at 7:17am
When I execute this test:
[TestMethod]

public void TestEntityChangedProperties()
{
    Deal deal = Deal.Create();
    deal.FlowDateStart = new DateTime(2009, 4, 1);
    IdeaBlade.EntityModel.SaveResult result = deal.EntityAspect.EntityManager.SaveChanges();
    Assert.IsTrue(result.Ok);
    Assert.IsFalse(deal.EntityAspect.HasChanges());

    //now, change the property to itself and see if HasChanges has changed
    deal.FlowDateStart = new DateTime(2009, 4, 1);
    Assert.IsFalse(deal.EntityAspect.HasChanges()); //FAILS
    Assert.AreEqual(IdeaBlade.EntityModel.EntityState.Unchanged, deal.EntityAspect.EntityState);
}


The test fails. Why? If I am setting the date to 4/1 and then after the save, changing it back to 4/1, shouldn't the property setter not bother to set the value and not mark the entity as dirty?



Replies:
Posted By: kimj
Date Posted: 31-Jul-2009 at 9:26am
We've had an internal debate about whether property setters on the Entity should first check if the incoming value is equal to the current value.  So far, the winning side says that we do not make this check.  This means that even if the new value is the same as the old, we still mark the entity as dirty.   This behavior may change some day, especially if we get a lot of complaints about it.
 
One workaround, although I haven't tried it, might be to add an entity/property wide BeforeSet property interceptor which makes this check and cancels further setter actions if needed.
 


Posted By: skingaby
Date Posted: 31-Jul-2009 at 10:28am
GROOOANN!!!!
Cast my vote firmly against making an object dirty when a property hasn't really changed!
Yeuch.
And the workaround is going to be an AWFUL lot of tedious code.

Here's our real world problem:
1) Create a Silverlight grid with some date columns bound to Entities.
2) Tie functionality to the "Dirty" state of the row, perhaps enable/disable the row Save button.
3) Make single click edit functionality work in the cells. (I.e. skip the "click to select, then click again to edit" by dropping into edit mode on the cell when it gets focus.
4) Now, run the app. Click on the date field in several rows. Because of the silverlight binding behavior, as you click through the rows, with single-click edit, the field drops into and out of edit mode and the entity property gets read and set. Now, every row you touched is dirty, even though you haven't changed any thing. Inexplicably, all the save buttons light up. 555-1212 >> "Helpdesk? Yes, I didn't change anything but it wants me to save."

Maybe you can build this in and make a toggle so that developers can select to enable/disable the check with a selection in the Object Mapper.

Here's the code that I was generating in our custom datalayer before we switched to IdeaBlade:
<System.Runtime.Serialization.DataMemberAttribute()>  _

Public Overridable Property DateOfDeal() As Date Implements IDeal.DateOfDeal
     Get
          Return Me._dateOfDeal
     End Get
     Set
          If (Me._dateOfDeal <> value) Then
               Me._dateOfDeal = value
               Me.IsDirty = true
          End If
     End Set
End Property

<System.Runtime.Serialization.DataMemberAttribute()> _
Public Overridable Property DealNumber() As System.Nullable(Of Long) Implements IDeal.DealNumber
     Get
          Return Me._dealNumber
     End Get
     Set
          If ((value.HasValue = false) _
                         AndAlso (Me._dealNumber.HasValue = true)) Then
               Me._dealNumber = Nothing
               Me.IsDirty = true
          Else
               If ((value.HasValue = true) _
                              AndAlso (Me._dealNumber.HasValue = false)) Then
                    Me._dealNumber = value
                    Me.IsDirty = true
               Else
                    If (((value.HasValue = true) _
                                   AndAlso (Me._dealNumber.HasValue = true)) _
                                   AndAlso (value.Value <> Me._dealNumber.Value)) Then
                         Me._dealNumber = value
                         Me.IsDirty = true
                    End If
               End If
          End If
     End Set
End Property



Posted By: skingaby
Date Posted: 31-Jul-2009 at 11:33am
So I added a method to the BaseEntity class. We have created this class and used it in the Object Mapper as our Base Entity Class:
public class BaseEntity : IdeaBlade.EntityModel.Entity


The new method looks like this. It would seem to be rather crude though, and I imagine I will run into some types it can't parse, but I'll cross that bridge... Anyway, does this seem reasonable?

[BeforeSet(Order=-1.0)]

public void BeforeSettingAnyProperty(IPropertyInterceptorArgs args)
{
    var dataArgs = args as IDataEntityPropertyInterceptorArgs;
    if (dataArgs != null)
    {
        var oldValue = dataArgs.DataEntityProperty.GetValue(dataArgs.Instance, EntityVersion.Current);
        var newValue = dataArgs.Value;
        if (oldValue == null && newValue == null)
            //both are null, nothings changed, cancel the update
            dataArgs.Cancel = true;
        else if (oldValue == null && newValue != null || oldValue != null && newValue == null)
            //one is null, don't cancel
            dataArgs.Cancel = false;
        else if (oldValue.Equals(newValue))
            //they're equal, nothings changed, cancel the update
            dataArgs.Cancel = true;
    }
}


After implementing this, the Unit test above passes now.


Posted By: skingaby
Date Posted: 31-Jul-2009 at 11:41am
P.S. Another reason to NOT set properties when the value hasn't changed is that some property changes are a trigger for other behaviors, for example, recalcs, refreshes, redraws, etc. If the property hasn't really changed, then there is no need to do any of those re-actions, some of which may be time-consuming or chatty.
For example, if you have an Order with 100 Order details. If the RecalcOrderTotals method iterates through all line items and calculates the extended price, then sums the extended price, factors in the the discount, taxes and shipping (applying the shipping price break logic) and then sets the OrderTotal, SalesTaxTotal, and ShippingTotal properties. That seems like an awful lot of overhead because a user just changed a line item quantity from 1 to 1.


Posted By: WardBell
Date Posted: 31-Jul-2009 at 1:38pm
I FEEL you. It's driving me crazy too. The Silverlight DataForm automatically resets every bound property to itself when in edit mode ... dirtying every object it sees. I have gotten nowhere arguing with MS against this practice; I follow their reasoning and understand why it was economical for them. They actually expect bound objects to ignore a reset-to-same-value. Today, we do not.
 
Kim is correct about the internal debate ... which continues and has received new life from your stimulating example. No promises from me today. There are some use cases we have to feel good about first; we didn't arrive at the present behavior casually. But that behavior is being re-reviewed and we are all receptive to your argument. Stay tuned.
 
p.s.: yes, you wrote an interceptor that should work 99+% of the time; if you find an exception, it will be rare and you can insert a special purpose interceptor for that one.


Posted By: skingaby
Date Posted: 01-Aug-2009 at 8:20am
Thanks Ward. You are a wise man and I hope to see this new feature in an upcoming release, for now, I will cross my fingers on the interceptor.


Posted By: kimj
Date Posted: 01-Aug-2009 at 8:48am
You'll see this in the September release :)


Posted By: skingaby
Date Posted: 03-Aug-2009 at 8:14am
Yay!!


Posted By: skingaby
Date Posted: 25-Sep-2009 at 12:59pm
Problem: It would seem that this fix has been half implemented. If the change is to a scalar property I.e. Order.Price (a Decimal), then the HasChanges is not set when the property is set to the existing value.
However, if the property is a reference property (i.e. Order.Customer), then changing it to the same value still sets the HasChanges. Our main class has a ton of reference properties that are populated by entities in comboboxes in the UI. This still has the problem that started this thread that non-changes in the grid still register as changes in the entity.
Am I testing this wrong? Is this working and I have it configured wrong? Thanks.


Posted By: WardBell
Date Posted: 25-Sep-2009 at 2:00pm
Hmmm. I cannot reproduce with this little "Test" program; all the asserts pass. Maybe you can repro?
 
In the following example, a "Bar" has a parent "Bang"
 
    private void NotModifiedIfChangeToSameValueTests() {
      // Skingaby Forum post 9/25/09 regarding F1183
      Console.WriteLine("NotModifiedIfChangeToSameValueTests");

      var EM1 = new EntityManager();

      var bang = new DomainModel.Bang();
      bang.Id = 42;
      var bangName = "Bang";
      bang.Name = bangName;
      EM1.AttachEntity(bang);
      Assert.IsTrue(bang.EntityAspect.EntityState == IdeaBlade.EntityModel.EntityState.Unchanged);
 
      // setting value to same value should not change entity state
      bang.Name = bang.Name;
      Assert.IsTrue(bang.EntityAspect.EntityState == IdeaBlade.EntityModel.EntityState.Unchanged);
 
      var bar = new DomainModel.Bar();
      bar.Id = 82;
      bar.Bang_fk_Id = bang.Id;
      EM1.AttachEntity(bar);
      Assert.IsTrue(bar.EntityAspect.EntityState == IdeaBlade.EntityModel.EntityState.Unchanged);
      Assert.IsTrue(bar.Bang == bang);
 
      // setting fk id to same value should not change entity state
      bar.Bang_fk_Id = bang.Id;
      Assert.IsTrue(bar.EntityAspect.EntityState == IdeaBlade.EntityModel.EntityState.Unchanged);
 
      // setting parent entity to same entity should not change entity state
      // Skingaby post says that it DOES become modified which would be a bug
      bar.Bang = bang;
      Assert.IsTrue(bar.EntityAspect.EntityState == IdeaBlade.EntityModel.EntityState.Unchanged);
 
      Assert.IsFalse(EM1.HasChanges());
 
      HoldConsoleWindowOpen();
    }


Posted By: pk55
Date Posted: 28-Sep-2009 at 10:48am

I'm seeing different behavior, not with tests from code, but with the UI just binding to scalar properties of an entity using a plain textbox (no dataform involved here).  

After loading the entity, I've validated that HasChanges is FALSE.  When I change the value of a scalar field (just a int) and tab off the field (causing a property changed event to fire) the entity is marked as having changes.  If I then change the value back to the original value and again tab off, the entity is still marked as having changes (both the EntityManager.HasChanges and the EntityAspect.HasChanges are TRUE).
 
If I issue a Save after setting the value back to it's original value, I see (using SQL profiler) that an update was issued with no columns from the entity included  in the SET (Entity Framework just sets an inline-declared dummy int variable @p to 0 so the SQL will not fail):
 
exec sp_executesql N'declare @p int
update [dbo].[myTable]
set @p = 0
where (([myKey] = @0) and ([timestamp] = @1))
select [timestamp]
from [dbo].[myTable]
where @@ROWCOUNT > 0 and [myKey] = @0',N'@0 int,@1 binary(8)',@0=123,@1=0x0000000002066FBA
 
If I change the field but then change it back before causing a property change to fire (in my case, tabbing off the field), then the entity is NOT marked as having changes.
 
In essence, "HasChanges" is really "HasBeenEditedAndAPropertyChangedEventHasBeenFired".  Is that the intented behavior?


Posted By: WardBell
Date Posted: 28-Sep-2009 at 11:45am

Thank you for clarifying the scenario. Let me restate it for other readers with an example.

string oldBar = foo.Bar
foo.Bar = oldBar ; // no change; foo remains in "unchanged" EntityState
foo.Bar = oldBar + "something"; // foo enters "modified" EntityState
foo.Bar = oldBar; // restored original value ... but foo is still in "modified" EntityState
And, yes, that is the intended behavior; I believe this is also the behavior of most (all?) other technologies in the same space, e.g., RIA Services and CSLA ... but I haven't confirmed.
 
Interesting name choice. I can think of others that are shorter but you've made your point :-)
 
If this really bothers you, you can filter the entities in the client-side SavingHandler; for each entity, compare each property's current value to its corresponding original value (which every DevForce entity knows); if there are no differences, you undo changes on that entity and exclude it from the list of entities to save.
 
It would take maybe 5 lines of code in total if you didn't get fancy about the field comparison / exclusion strategies. It's up to you if you think this is worthwhile.


Posted By: pk55
Date Posted: 28-Sep-2009 at 12:15pm
Then what was the reason for the change in the Sept release?  The example you've given to restate the problem seems to look a lot like the original posters problem minus the unit tests assertion.


Posted By: pk55
Date Posted: 28-Sep-2009 at 12:22pm
Ignore my last post.  I see that what you're really saying is the "current" value has changed; not the "original" value.  It becomes a user training issue. 


Posted By: skingaby
Date Posted: 29-Sep-2009 at 1:08pm
Sorry Ward, you are right. My unit test was setting a Navigation property on an Entity. Unbeknownst to me, another developer had added an AfterSet interceptor to the entity that was catching that property change and changing other properties. My mistake.

However, and there's always a however isn't there, why are the AfterSet interceptors being called if the property is not actually changing?
I was handling the check in 5.2.1 in a method with this signature:
[BeforeSet(Order = -999999)] public void BeforeSettingAnyPropertyCheckToNotDirtyForNoReason(IPropertyInterceptorArgs args).
In that interceptor, I was setting args.Cancel = true if the entity property was being set to its existing value, which would abort running any AfterSet interceptors.
Now it seems like I have to put a guard in the other AfterSet's to make sure that they don't make unnecessary changes as they fire. Is that the case?


Posted By: WardBell
Date 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.


Posted By: skingaby
Date 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


Posted By: skingaby
Date 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();
}


Posted By: kimj
Date 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. :))


Posted By: skingaby
Date Posted: 30-Sep-2009 at 11:11am
Hmmm... Yeh. So who's right?


Posted By: WardBell
Date 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.


Posted By: WardBell
Date 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.


Posted By: mulpurir
Date Posted: 10-Feb-2010 at 6:01pm
Is this feature is released.


Posted By: skingaby
Date 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.






Print Page | Close Window