Silverlight binding to IsDirty doesn't change when other properties are changed

Silverlight binding to IsDirty doesn't change when other properties are changed

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


Jaans posted on Monday, June 22, 2009

I've used the XAML below to bind a business object's IsDirty property value.

<TextBlock Text="*" Visibility="{Binding IsDirty, Converter={StaticResource VisibilityConverter}}" Margin="2" />

The issue I'm getting is that it's not changing (via Binding) when I change another property's value (thereby making the business object dirty). However, when I save the object, the change does come through the binding.

Ps: Through a breakpoint, I have confirmed that the property's value is changed to True, but the bound value is unaware.

Any ideas? Am I missing something?

Thanks
Jaans

RockfordLhotka replied on Monday, June 22, 2009

I don't think a PropertyChanged event is raised when the various Is___ properties change. This is a holdover from .NET behavior.

However, CslaDataProvider exposes the Is___ properties (and many other useful properties) for data binding.

Jaans replied on Friday, July 03, 2009

Thanks Rocky

The properties on the CslaDataProvider are useful, but unfortunately doesn't include the IsDirty / IsSelfDirty / IsValid / IsSelfValid properties.

Not to worry... I found a crude hack by subscribing to the object's PropertyChanged event and "polling" the IsDirty property upon every event notification.

Regards,
Jaans

RockfordLhotka replied on Friday, July 03, 2009

Thanks, I'll add that to the wish list.

-----Original Message-----
From: Jaans [mailto:cslanet@lhotka.net]
Sent: Friday, July 03, 2009 12:55 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] Silverlight binding to IsDirty doesn't change when
other properties are changed

Thanks Rocky

The properties on the CslaDataProvider are useful, but unfortunately doesn't
include the IsDirty / IsSelfDirty / IsValid / IsSelfValid properties.

Not to worry... I found a crude hack by subscribing to the object's
PropertyChanged event and "polling" the IsDirty property upon every event
notification.

Regards,
Jaans

skagen00 replied on Friday, July 03, 2009

Ditto this - I'm using a similar hack. Thanks!

mesh replied on Wednesday, December 23, 2009

I can't believe that binding to IsValid and IsDirty is not working in SL, it is so much needed feature.

RockfordLhotka replied on Wednesday, December 23, 2009

This is supported by binding to the CslaDataProvider or ViewModel class.

 

The limitation is that those properties (along with other metaproperties) don’t raise PropertyChanged when they change.

 

I may alter that behavior at some point. The choice to not raise the event for those properties can be traced back to data binding, and in part to Windows Forms.

 

There’s a Browsable(false) attribute on those properties, which prevents them from appearing automatically when the object is dragged onto a form using the VS designer tools. That’s generally a good thing, as otherwise you’d have to manually remove a bunch of properties every time you drag on object onto a form.

 

What was unexpected (even by the Msft PM who originally owned this area) is that binding is disabled for these properties even if you manually set up the binding later…

 

So raising PropertyChanged for unbindable properties is pointless, so CSLA doesn’t do it.

 

The thing is, the properties should be bindable, but should not be Browsable – because otherwise the UI design experience is terrible. Sadly, Browsable(false) blocks binding – at least in Windows Forms.

 

In XAML I don’t think it blocks binding, which is why I may reconsider raising PropertyChanged for those properties. That still won’t fix Windows Forms – I think the “fix” for Windows Forms is to let it slowly fade away as we all move to XAML :)

 

Jaans replied on Tuesday, July 27, 2010

Hi Rocky

Any news on this? Apologies if this is something that's been implemented for CSLA 4.0 - haven't had the chance to test this out on 4.0 yet.

I'm have a Silverlight project using CSLA 3.8.4 and the issue remains. I'm binding directly to the business object (MVVM will have to wait for later when we can do the whole project), and the property change notifications for  the IsDirty, IsSavable, etc. properties aren't happening.

I have a question (based on your earlier comments) --> since Windows Forms binding considers the properties "unbindable", would it not be OK/safe to have CSLA raise the property change notifications anyway?

That way Silverlight / WPF can gain the benefit, without breaking the Windows Forms story? Is this something that can be considered for >= CLSA 3.8.4 ?

Thank you.
Jaans

mesh replied on Wednesday, December 30, 2009

I think that binding (raising ProperyChanged) does work in WPF but not in Silverlight. So, problem is that I can't have the same GUI look and feel shared between WPF and Silverlight (eg. some kind of toolbar with icons showing object status).  I have tried both CslaDataProvider and ViewModel binding.

RockfordLhotka replied on Wednesday, December 30, 2009

Which property of ViewModel are you binding to?

I ask, because I have several examples where I have basically the same UI in WPF and SL, so the concept is sound. If there's a specific property that's not working that could be a bug.

mesh replied on Wednesday, December 30, 2009

Sorry, I was wrong. I'm trying to bind to IsDirty property. Looking thru my code, I can see that I'm actually exposing ViewModelBase<T>.Model<T>.IsDirty (BusinessBase.IsDirty) property in my ViewModel, and that is not working.  Binding to everything else in ViewModelBase is OK. In my WPF project, I used ObjectStatus and CslaDataProvider to get IsDirty and IsValid.
So, question actually is how to get working IsValid and IsDirty binding if I’m using ViewModel in Silverlight?

RockfordLhotka replied on Wednesday, December 30, 2009

Look at the properties on ViewModel itself - CanSave, CanCancel, etc. Those are designed to support typical UI scenarios.

mesh replied on Thursday, December 31, 2009

My scenario is simple, I have toolbar component with button that is displaying all errors on object. So, if there are no errors, the best thing would be to hide it (bind his visibility to IsValid).

I’m using another couple of icons to visually show to user that he has changed the object (IsDirty) and/or that object has any errors (IsValid). By using CanSave(or any other property on ViewModel) I don’t know how to get such functionality.

So I do think that binding to IsValid and IsDirty are very important for building proper UI experience.

Thanks

RockfordLhotka replied on Thursday, December 31, 2009

CanSave is basically IsSavable, which is a combination of
IsDirty/IsValid/is-authorized.

Jaans replied on Thursday, January 14, 2010

The CanSave/CanXXX methods do work very well and also updates the UI binding when the underlying business object becomes Dirty/Valid/Auth.

But today on another project, I again needed the ability to not only bind to "IsDirty" but also have that binding refresh when the business object becomes dirty. It's a scenario where CanSave does not suffice because the object was dirty but not valid. So here CanSave would be false, but I needed to indicate to the user that there had been a change and that he/she should not just rely on the CanSave indicator to perhaps figure out that a dependant property needs to be updated or something to make it valid.

The key thing for me is to be able to indicate to the user that there are changes (IsDirty) irrespective of whether the object is valid yet.

So while I can bind to the IsDirty property in Silverlight, it doesn't do a PropertyChanged for IsDirty effectively leaving the bound UI out of date.

As Rocky states it's all about the [Browsable(false)] attribute - I recall a post flying by a short while ago about using an alternate attribute to get a same/similar result but for the life of me I cannot find that post or remember the attribute name. (Perhaps is was a dream/nightmare)

Does anyone now of an alternate attribute / implementation that may help us have our windows forms cake and eat it under a silverlight sky?

cds replied on Monday, February 22, 2010

I'm late to the party on this one, but I had a similar situation. I wanted to bind to the IsValid property to display in a Silverlight DataGrid. It only showed the original value and didn't update the grid cell when IsValid changed. Luckily I have a custom base class for all my business objects, so I just did this:

 

 

 

protected

 

override void OnValidationComplete()

{

 

 

// IsValid isn't Bindable - changes to the IsValid state don't raise a PropertyChanged event, so make it do so.

OnPropertyChanged(

"IsValid");

 

 

base.OnValidationComplete();

}

RockfordLhotka replied on Tuesday, July 27, 2010

The only answer in 3.8 or 4 is to have an intermediate object (CslaDataProvider or ViewModelBase<T> or something of your own design) that "elevates" the properties to a bindable status.

Jaans replied on Tuesday, July 27, 2010

Thanks Rocky

I'm a little unclear how I would be able to do this for something like IsDirty. I mean, how would my view model know that this property has changed in the underlying model?

RockfordLhotka replied on Tuesday, July 27, 2010

Look at ViewModelBase to see how it works.

Basically it listens for PropertyChanged and ChildChanged, and then checks to see if IsDirty changed (among other metastate properties).

Jaans replied on Tuesday, July 27, 2010

Had a look and it seems interesting enough.

You have quite a bit of work in the CSLA baseclass to re-wrap existing properties from the business object. To extend that I could create an intermediate generic base class for that above and inherit from it. I wil inturn inherit from the CSLA ViewModelBase<T>.
(PS: As a policy we treat CSLA as a black box, leave it unmodified and only reference by assembly - this is easier on the juniors).

From a guidance point of view, would you agree an appropriate implementation would override the OnModelChanged method to re-use the existing logic to unhook and rehook the change events from the (changed) underlying model instance?

Maybe something along these lines...

#region

Custom Property Remapping

 private bool _isDirty = false; /// <summary>
/// Gets a value indicating whether the Model is dirty or not
/// </summary>
public bool IsDirty
{

 get { return _isDirty; }

private set
{

if ( _isDirty != value )
{

_isDirty = value;

OnPropertyChanged( "IsDirty" ); // Inherited from base class

}

}

}

#endregion

#region

Overrides  

/// <summary>
/// Called when the underlying model changes.
/// </summary>
/// <param name="oldValue">The old model value.</param>
/// <param name="newValue">The new model value.</param>
protected override void OnModelChanged( T oldValue, T newValue )
{

 

// Retain existing logic that unhooks the change events from the old model object and rehooks it for the new one.
base.OnModelChanged( oldValue, newValue );

// Extend with additional property change "listening"
SetCustomProperties();

}

 

private void SetCustomProperties()
{

ITrackStatus targetObject = Model as ITrackStatus;
if ( Model != null && targetObject != null )
{

IsDirty = targetObject.IsDirty;

}

}
#endregion

Looking at it all, most of the properties currently on the ViewModelBase are sufficient, and I just have a recurring need to track and show the current values for IsDirty, IsSelfDirty, IsValid, IsSelfValid as the object is edited - CanSave doesn't quite do it. The same goes for ease of use items that are negated like IsNotDirty, IsNotValid.

Not sure if others have a similar need for it - if it turns out to be so, perhaps it is something for the wish list - don't want to bloat the API unnessecarily.

Thanks for the help,
Jaans

Jaans replied on Wednesday, July 28, 2010

I discovered a major oversight in the above and that's the fact that I'm not participating in the property changed event.

So my alternatives would be to either:

a) In the override for OnModelChanged, duplicate the logic from the ViewModelBase class and hook + unhook my own events so that I can do my own "SetProperties()" for the additional properties I would like; or

b) if it ViewModelBase was a bit more extensible, for example like making the "private void SetProperties()" virtual so that I can override it, it would certainly be a lot easier. An alternate thought here is to introduce a "protected virtaul OnPropertiesSet()" method and have SetProperties call it after setting all the properties.

Thoughts?

Devman replied on Thursday, July 29, 2010

Jaans

b) if it ViewModelBase was a bit more extensible, for example like making the "private void SetProperties()" virtual so that I can override it, it would certainly be a lot easier. An alternate thought here is to introduce a "protected virtaul OnPropertiesSet()" method and have SetProperties call it after setting all the properties.

+ 1

We too require "IsDirty" and "IsValid" at View Model level so making SetProperties() virtual would be ideal. Currently we have had to copy and modifiy CSLAs ViewModelBase which is not ideal.

Jav replied on Sunday, August 08, 2010

In Csla 2.x we used IsValid property extensively to provide useful feedback.  In my App, IsSavable is great but it would be useful to have bindable access to IsValid.  After going through this thread, I added the following to my ViewModel class from which all other VMs are subclassed.

       private bool _IsValid = false;

        public bool IsValid
        {
            get
            {
                return _IsValid;
            }
            private set
            {
                _IsValid = value;
                OnPropertyChanged("IsValid");
            }
        }

        protected override void OnModelChanged(T oldValue, T newValue)
        {
            base.OnModelChanged(oldValue, newValue);
            ITrackStatus targetObject = Model as ITrackStatus;
            if (Model != null && targetObject != null)
            {
                var npc = newValue as INotifyPropertyChanged;
                if (npc != null)
                    IsValid = targetObject.IsValid;
             }
        }

I have set breakpoints in the IsValid.Set and in the OnModelChanged Method. As a test I have a couple of checkboxes with the following Binding:
            IsChecked="{Binding IsValid, Source={StaticResource RootObjectViewModel}}"
When the Object Graph is loaded from the DB, the checkboxes are appropriately checked, and the code breaks - more than once actually.

The part of object graph that I am working with is:
              Root - Child - Child - ChildCollection - Child
I add new child to ChildCollection (AddNewCore - Child.NewChild() - DataPortal.CreateChild() - base.Child_Create and then CheckRules), Child appears in the UI, with a broken rule indication for a required field.  I do not see the code breaking in my code.  Using another breakpoint, I can tell that the RootObjectViewModel is NOT valid. But the test checkBoxes are still checked. (which is not a surprise because my code in vm didn't run). The Save button with a binding to CanSave is now disabled.

Instead of entering data in child, I click Delete which call ChildCollection.Remove(), the rest of the execution is not in my code.  The child disappears.  My RootObjectViewModel is Valid again. No breaks in my code.  The CheckBoxes didn't even blink.  Save button is enabled again.

I am thinking:
    a.  My code above may not be exactly right.
    b.  Something is missing in my object codes  - Everything seems to run fine all the way to DB.
    c.  something alse ?

Jav

 

Jaans replied on Sunday, August 08, 2010

Hi Jav

Based on what you've posted above I suspect that you (like I did in my initial post) missed the hooking up of the various "changed" events for a model so that you could catch the property notifications.

Note that essentially OnModelChanged only fires when the actual "Model" object instance is replaced with a whole new object like what happens when you Save or Fetch. Given that, while you are working with the same instance you need to be hooked into about 3 or 4 different change events to ensure you can catch the property changes (of the given instance).

Hooking into these events is all fine and dandy, but its quite important to only subsribe to these events when the model changes so that you can be hooked into the change events of the "new" model instance, but equally as important you need to unsubscribe from the "old' events to prevent not only the listening to events from the wrong object instance but also to avoid a type of memory leak scenario where you keep the object referenced and the garbage collector won't pick it up when it comes around.

Here's our implementation of the ViewModel Base class that we inherit from (Note this inturn inherits from CSLA's ViewModel<T>). There are a couple of other properties that I've found I regularly need to bind to in XAML and also some properties that have been "inverted" for ease of use in XAML again. Feel free to discard them.

I can email you the class file if the formatting below is too screwed up.

    public abstract class ViewModel<T> : Csla.Silverlight.ViewModel<T>
    {
        #region Custom Property Remapping

        /// <summary>
        /// Gets a value indicating whether this instance's model is empty.
        /// </summary>
        /// <value>
        ///  <c>true</c> if this instance is model empty; otherwise, <c>false</c>.
        /// </value>
        public bool IsModelEmpty
        {
            get { return Model == null; }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is not busy.
        /// </summary>
        /// <value>
        ///  <c>true</c> if this instance is not busy; otherwise, <c>false</c>.
        /// </value>
        /// <remarks>
        /// Refer to the overridden property changed event to hook this property's change notification to IsBusy
        /// </remarks>
        public bool IsNotBusy
        {
            get { return !IsBusy; }
        }

        private bool _isDirty = false;

        /// <summary>
        /// Gets a value indicating whether the Model is dirty or not
        /// </summary>
        public bool IsDirty
        {
            get
            {
                return _isDirty;
            }
            private set
            {
                if ( _isDirty != value )
                {
                    _isDirty = value;
                    OnPropertyChanged( "IsDirty" ); // Inherited from base class
                    OnPropertyChanged( "IsNotDirty" );
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is not dirty.
        /// </summary>
        /// <value>
        ///  <c>true</c> if this instance is not dirty; otherwise, <c>false</c>.
        /// </value>
        public bool IsNotDirty
        {
            get { return !IsDirty; }
        }

        private bool _isValid = true;

        /// <summary>
        /// Gets a value indicating whether the Model is valid or not
        /// </summary>
        public bool IsValid
        {
            get
            {
                return _isValid;
            }
            private set
            {
                if ( _isValid != value )
                {
                    _isValid = value;
                    OnPropertyChanged( "IsValid" ); // Inherited from base class
                    OnPropertyChanged( "IsNotValid" );
                    OnPropertyChanged( "ValidationRuleSummary" );
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is not valid.
        /// </summary>
        /// <value>
        ///  <c>true</c> if this instance is not valid; otherwise, <c>false</c>.
        /// </value>
        public bool IsNotValid
        {
            get { return !IsValid; }
        }

        /// <summary>
        /// Gets the validation rule summary.
        /// </summary>
        /// <value>The validation rule summary.</value>
        public virtual string ValidationRuleSummary
        {
            get
            {
                var businessBase = Model as BusinessBase;
                if ( businessBase == null )
                    return string.Empty;

                var brokenRules = new StringBuilder();
                foreach ( var brokenRule in businessBase.BrokenRulesCollection )
                    brokenRules.AppendLine( string.Format( "{0}", brokenRule.Description ) );

                return brokenRules.ToString();
            }
        }

        #endregion

        #region Overrides - Add additional hooks for properties on Model not available on ViewModelBase

        /// <summary>
        /// Called when the underlying model changes.
        /// </summary>
        /// <param name="oldValue">The old model value.</param>
        /// <param name="newValue">The new model value.</param>
        protected override void OnModelChanged( T oldValue, T newValue )
        {
            // Retain existing logic that unhooks the change events from the old model object and re-hooks it for the new one.
            base.OnModelChanged( oldValue, newValue );

            if ( ReferenceEquals( oldValue, newValue ) )
                return;

            // Extend with additional property change "listening"
            base.OnPropertyChanged( "IsModelEmpty" );

            // Unhook events from old value
            if ( oldValue != null )
            {
                var npc = oldValue as INotifyPropertyChanged;
                if ( npc != null )
                    npc.PropertyChanged -= Model_PropertyChanged;
                var ncc = oldValue as INotifyChildChanged;
                if ( ncc != null )
                    ncc.ChildChanged -= Model_ChildChanged;
                var nb = oldValue as INotifyBusy;
                if ( nb != null )
                    nb.BusyChanged -= Model_BusyChanged;
                var cc = oldValue as INotifyCollectionChanged;
                if ( cc != null )
                    cc.CollectionChanged -= Model_CollectionChanged;
            }

            // Hook events on new value
            if ( newValue != null )
            {
                var npc = newValue as INotifyPropertyChanged;
                if ( npc != null )
                    npc.PropertyChanged += Model_PropertyChanged;
                var ncc = newValue as INotifyChildChanged;
                if ( ncc != null )
                    ncc.ChildChanged += Model_ChildChanged;
                var nb = newValue as INotifyBusy;
                if ( nb != null )
                    nb.BusyChanged += Model_BusyChanged;
                var cc = newValue as INotifyCollectionChanged;
                if ( cc != null )
                    cc.CollectionChanged += Model_CollectionChanged;
            }

            SetCustomProperties();
        }

        private void Model_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            SetCustomProperties();
        }

        private void Model_BusyChanged( object sender, BusyChangedEventArgs e )
        {
            SetCustomProperties();
        }

        private void Model_ChildChanged( object sender, ChildChangedEventArgs e )
        {
            SetCustomProperties();
        }

        private void Model_PropertyChanged( object sender, PropertyChangedEventArgs e )
        {
            SetCustomProperties();
        }

        private void SetCustomProperties()
        {
            ITrackStatus targetObject = Model as ITrackStatus;
            if ( Model != null && targetObject != null )
            {
                if ( CanEditObject )
                    IsDirty = targetObject.IsDirty;

                if ( CanEditObject )
                    IsValid = targetObject.IsValid;
            }
        }

        protected override void OnPropertyChanged( string propertyName )
        {
            base.OnPropertyChanged( propertyName );

            // Extend with additional logic
            if ( propertyName == "IsBusy" )
                base.OnPropertyChanged( "IsNotBusy" );
        }

        #endregion
    }

RockfordLhotka replied on Monday, August 09, 2010

fwiw, this is on the wish list: http://www.lhotka.net/cslabugs/edit_bug.aspx?id=817

Jaans replied on Tuesday, August 10, 2010

RockfordLhotka

fwiw, this is on the wish list: http://www.lhotka.net/cslabugs/edit_bug.aspx?id=817

Thanks Rocky!

Ps: You can use the sample implementation I posted here if it makes the above easier / quicker to do one day. We've tested it quite extensively.

Jav replied on Tuesday, August 10, 2010

Jaans,

I plugged you code in and it worked the very first time.

Thanks

Jav

Jav replied on Monday, August 09, 2010

Jaans,

Thank you so much for your detailed explanation and the code.  It's perfectly legible.  It would be an immense help. 

Rocky - thanks for putting it on the wish list.  Those of us who grew up using Csla are essentially hooked on this Isvalid/IsDirty thing, and our users have loved the visual feedback telling them when a given object in a large object graph is in a valid or invalid state, especially if those objects are scattered over multiple navigation pages.

Jav

Copyright (c) Marimer LLC