WPF, IsDirty, and INotifyPropertyChanged. Not supported?

Posts   
 
    
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 19-Feb-2012 21:00:03   

LLBL 3.1, .Net 4, Adapter, Oracle, LLBL Framework

I've discovered that an Entity is NOT firing OnPropertyChanged for IsDirty after I've modified a field. Or after saving. Or ever?

I search the source code for OnPropertyChanged("IsDirty"); and it's nowhere to be found.

So I did some debugging, and it looks like IsDirty is not even stored at the Entity level, but inside an encapsulated FieldsCollection object.

The reason I'm asking is that WPF is heavily dependent on binding and bound objects firing these PropertyChange events. On my DataGrid I had added a DataTrigger to my DataRow bound to IsDirty so as to change the background color of modified rows. But it didn't work. So debugging the code, I saw that changing fields passed through OnSetValueComplete so I overrode this is CommonEntityBase to just fire OnPropertyChanged("IsDirty"); . Great! Now my modified rows are changing colors. But then I saved. Didn't change back. After the refetch, I need another place to trigger this. Couldn't find anything overridable. Ultimately, I've made some changes to the core LLBL source to fire OnPropertyChanged("IsDirty") and everything is working now, but I'd like to get away from a customized version.

Am I missing any overrides or hooks that would allow me to solve this from within my generated code library?

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 20-Feb-2012 00:09:27   

happyfirst wrote:

I've discovered that an Entity is NOT firing OnPropertyChanged for IsDirty after I've modified a field. Or after saving. Or ever?

I search the source code for OnPropertyChanged("IsDirty"); and it's nowhere to be found.

OnPropertyChanged is already fired for all entity fields, when a value is set. Nothing is bounded to the IsDirty property. Maybe there is a way to your bound control to look into all fields of the entity, that way it will detect if any of the fields had changed.

happyfirst wrote:

Am I missing any overrides or hooks that would allow me to solve this from within my generated code library?

You can override the OnPropertyChanged in CommonEntityBase:

protected override void OnPropertyChanged(string propertyName)
{
    base.OnPropertyChanged(propertyName);
    // fire something else...
}
David Elizondo | LLBLGen Support Team
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 20-Feb-2012 01:37:41   

daelmo wrote:

You can override the OnPropertyChanged in CommonEntityBase

Thanks. Just tried that. But that doesn't completely work. The grid rows change colors when I modify the rows but they don't change back after I save. That's my problem. I couldn't figure out how from the outside to get OnPropertyChanged("IsDirty") to fire after a entity save or collection save.

It seemed like there was some backdoor stuff going on internally after a save inside the refetching code. That's where I had to go add my own custom code. Essentially, I had to add OnPropertyChanged("IsDirty") inside the EntityBase IsDirty setter and then inside DataAccessAdapterBase right after the AcceptChanges call (an internal function on the fields, not the entity), I had to just call entityToSave.IsDirty=false; myself.

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 20-Feb-2012 11:47:42   

WHen you save the entity, do you refetch it?

happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 20-Feb-2012 14:18:19   

I'm calling SaveEntityCollection on the DataAdapter passing in true to have it refetch the entities after save. IsDirty is false after SaveEntityCollection returns, but without my mods inside your framework, OnPropertyChanged("IsDirty") will not have fired, even following the suggestion above.

In the debugging your framework, you are calling an internal function AcceptChanges and then refetching from an internal function called PersistQueue.

I basically added my call to IsDirty=false right before your call to IsNew=false;

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 20-Feb-2012 16:09:00   

When I save an entity collection, IsDirty flag turns from true to false, for each of the entities inside the collection.

Which runtime library version are you using? (build number).

happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 20-Feb-2012 16:18:16   

I'm using your latest library. Sept '11 ?

Yes, the IsDirty flag does change from true to false. I acknowledged above that does work. That's NOT the problem.

The problem is that your framework does not fire OnPropertyChanged("IsDirty") so WPF DataBinding never finds out and so my modified rows stay yellow.

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 20-Feb-2012 18:02:11   

Now, I see what you mean. Let us check this out, and get back to you.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 21-Feb-2012 10:32:09   

It isn't done because the flag is a mirror of the entity field's IsDirty property, it's not a direct property of the entity itself. To be able to track the state of IsDirty for the PropertyChanged event it then needs an event binding itself which isn't done.

Override CallValidateEntityAfterSave to have a tap-in method into the save process to raise the event.

In general I'd simply refresh the UI after a save action took place. This is much more efficient, as you otherwise get multiple redraws during the save process. Say you have 30 entities on screen, you save them, and every time an entity is saved, the UI gets an event and redraws the portion of it (which can be costly). This isn't efficient. better is to collect the events and redraw everything at once. The user won't notice, as it's done in 1 go.

Frans Bouma | Lead developer LLBLGen Pro
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 21-Feb-2012 17:25:46   

I saw that the flag is just digging into the Fields.IsDirty property. Still, I think it would be nice if the Entity listened to when the Fields.IsDirty changed and then itself called OnPropertyChanged("IsDirty") ONLY as needed, when it changed, not every single time it keeps getting set to the same value.

I definitely need the IsDirty Changed to fire when first modified in order for the WPF data bindings to work and have my rows change color. For the save, I worked around it by just telling the view that the grid was bound to, to refresh itself from my view model save command and that made the rows change back.

But everything is trade-offs. By telling the view to refresh, that means WPF is now going through every row and requerying every property. If I'd only changed one row and if the entity could properly fire IsDirty changed ONLY WHEN IT REALLY CHANGES, that just seems like the better solution.

I just didn't like having a customized library. However, even with the custom library there was no flickering.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 21-Feb-2012 22:55:48   

Did you try to fire the event on an CallValidateEntityAfterSave's override?

David Elizondo | LLBLGen Support Team
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 21-Feb-2012 23:11:06   

Yes, that does work. And I commented out my view refresh after a save. But there are still other holes that need to be plugged.

For example, if they hit cancel, I refetch the entities. Without a view refresh, my modified rows stay yellow. Actually even the changed cell values don't revert. Seems like OnPropertyChange are not firing in the event of a refetch even for field updates?

So in the case of cancel, if I have a huge grid, and the user just changed one cell, and then hit cancel, I still have to rely on view refresh there to get everything back to what it was.

Walaa avatar
Walaa
Support Team
Posts: 14950
Joined: 21-Aug-2005
# Posted on: 22-Feb-2012 09:46:58   

I think when you refetch all entities the data bound conrol need to be refreshed too (re-binding with the datasource), so you should not see old modified data which was not persisted.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 22-Feb-2012 11:26:36   

How do you refetch? You fetch new entities? Even with the context, the entities aren't marked as 'reset' or something. Simply unbind and rebind (or use a method on the grid if possible, to refetch/reset the grid to refetch all data) so everything is reset in 1 go. That's far more efficient than doing it per row.

Frans Bouma | Lead developer LLBLGen Pro
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 22-Feb-2012 13:52:54   

I loop through all the loaded entities and if they are dirty, I refetch. And by refetch, I just call adapter.FetchEntity on the already loaded and dirty entity. This does not cause any of my custom hooks to fire so I can't fire OnPropertyChanged("IsDirty").

I can't unbind and rebind because the binding is in XAML. I'm using MVVM. I could fake a rebinding by getting mvvm to see my view go null and then back again, but that probably would cause flicker.

The only solution I've found here, besides me customizing the llbl framework internals to fire OnPropertyChanged("IsDirty"), is to call Refresh on the view. There is a small delay on grids with large amounts of data if the user only was cancelling a change to one row.

So I have a half baked solution where some custom hooks in your framework are allowing WPF binding and MVVM to do their job and work automatically, and other spots where I have to step in and force updates to happen.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 23-Feb-2012 05:59:22   

According to my test, if you save a dirty entity and then refetch, the IsDirty property is not false, as the entity is already saved, just the state of the entity is changed. Maybe you would need to work on some DataAccessAdapter fetch's events to catch a refetch.

How do yo do it right now modifying the RTL sourcode anyway?

David Elizondo | LLBLGen Support Team
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 23-Feb-2012 13:16:39   

daelmo wrote:

According to my test, if you save a dirty entity and then refetch, the IsDirty property is not false, as the entity is already saved, just the state of the entity is changed. Maybe you would need to work on some DataAccessAdapter fetch's events to catch a refetch.

I'm a bit confused by what you're stating. "Save dirty entity, then refetch, the IsDirty is NOT false" as in IsDirty=TRUE ???

That's not happenening in my case, although I do not refetch on my own after a save, I just pass in true to have your framework refetch. In my case, IsDirty=false all the time, but keep in mind, my issue has NEVER been that the state of IsDirty was ever wrong. IsDirty has always been the correct value.

The issue is that when your framework correctly does change IsDirty, it doesn't fire OnPropertyChanged("IsDirty"). I understand the issues you'd face trying to solve that since IsDirty is not really stored at the entity level. Still it would be a nice problem to solve, because WPF and xaml bindings rely heavily on objects notifying when something has changed so that they can update themselves.

daelmo wrote:

How do yo do it right now modifying the RTL sourcode anyway?

I added an OnPropertyChanged("IsDirty") to your EntityBase IsDirty setter. And then I just specifically added some calls to set this to False in some other places in your code. I don't think this is the right fix, just a fix I was trying to see if that's why my GUI wasn't updating. With this fix, my gui started working.

There are very few places in your code where you call IsDirty setter at the entity level, maybe just two or three. And the fields object doesn't have a reference back to the entity, not that I saw. That would have been ideal, just have the fields.IsDirty call an internal on the parent entity to raise the property notification. I think less than ideal is having the entity hook some event, I'd think that would affect performance. There are only about a dozen places or so in your code where you call the Fields IsDirty setter, and most are within EntityBase2 code, so another fix is to just go and call OnPropertyChanged("IsDirty") next to all of them, this is the fix I am tempted to go with as it seems the safest and the least likely to impact performance.

EDIT:

Although I am torn on whether or not I should add an OnIsDirtyChanged virtual and IsDirtyChanged event to the Fields that the entitybase hooks internally that the Fields is smart about raising ONLY when IsDirty does change. Then the entity can fire OnPropertyChanged("IsDirty") from it's own setter or the fields IsDirtyChanged event hook. Then the fix would not be splattered all over the app.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39614
Joined: 17-Aug-2003
# Posted on: 24-Feb-2012 10:46:45   

The problem with binding the fields object to the containing entity class is this: the fields object is used as a DTO internally. So a class is instantiated, and its Fields object is passed to the fetch core to get filled, then it gets back. In theory you can set it to a different fields object if you want to, it's not tied to the containing entity class through events.

if you solve that with events and proper de-wiring, there's another problem and it is about performance. the problem with events is that if you get a lot of them, bound code will execute over and over again. Our designer had the same problem: the entity model in-memory relies heavily on events, so there's almost no coupling between classes. The downside was that when something changes deep down in the object graph, it would ripple through and cause many UI re-paints, sometimes parts of grids, sometimes whole grids. As we didn't want to implement code inside the classes to limit events for UI code it has no notion of, we implemented an event throttler class, which simply keeps 1 event and throws away the rest, then raises the one event to start the repaint and that's it. (it's in Algorithmia, if you want to use it).

So we know what you will run into sooner or later simple_smile namely: event overload. If you save 10 entities, you'll get 10 times a repaint for a row. With 1 entity it might not be visible, but with many entities it will, up to a point where you e.g. click a checkbox and it will cause so many events that it will stall the UI for 2 seconds (we had that problem: check a checkbox, it would trigger events all over the place due to changes left and right, repaints happening over and over again. The event throttler solved all that).

If you really want to proceed with this, do this instead of adding events and calls all over the place in the runtime code: - add partial class to CommonEntityBase. - add override to OnValidateEntityAfterSave:

protected override OnValidateEntityAfterSave()
{
      base.OnValidateEntityAfterSave();
      OnPropertyChanged("IsDirty");
}

That will be called always when an entity is successfully saved, so it's not called when the entity is in a collection which is saved but the entity isn't dirty.

Though, as said, I'd go for the 1 repaint after the save action. You know when the save action happens (as the user does something) so you can call the repaint there.

Frans Bouma | Lead developer LLBLGen Pro
happyfirst
User
Posts: 215
Joined: 28-Nov-2008
# Posted on: 24-Feb-2012 15:05:32   

I don't disagree with anything you've said. With events, it's just a matter of making sure you only throw them when needed, which obviously, requires more code! If somebody changes 10 different fields on the same entity, I would only "want" the IsDirty property changed to fire once.

Thanks for the suggestions. I will look into them.