CSLA .NET

Vibrant discussion about CSLA .NET and using the framework to build great business applications.

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

This post has 27 Replies | 6 Followers

Top 50 Contributor
Posts 187
Jaans replied on Tue, Jul 27 2010 10:29 PM

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

Top 10 Contributor
Posts 9,282

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.

Rocky

Top 50 Contributor
Posts 187
Jaans replied on Tue, Jul 27 2010 10:54 PM

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?

Top 10 Contributor
Posts 9,282

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).

Rocky

Top 50 Contributor
Posts 187
Jaans replied on Tue, Jul 27 2010 11:47 PM

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

Top 50 Contributor
Posts 187
Jaans replied on Wed, Jul 28 2010 4:32 AM

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?

Top 150 Contributor
Posts 53
Devman replied on Thu, Jul 29 2010 8:44 AM

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.

Top 25 Contributor
Posts 385
Jav replied on Sun, Aug 8 2010 1:48 PM

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

 

Top 50 Contributor
Posts 187
Jaans replied on Sun, Aug 8 2010 6:47 PM

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
    }

Top 10 Contributor
Posts 9,282

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

Rocky

Top 25 Contributor
Posts 385
Jav replied on Mon, Aug 9 2010 2:38 PM

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

Top 50 Contributor
Posts 187
Jaans replied on Tue, Aug 10 2010 7:36 PM

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.

Top 25 Contributor
Posts 385
Jav replied on Tue, Aug 10 2010 8:12 PM

Jaans,

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

Thanks

Jav

Page 2 of 2 (28 items) < Previous 1 2 | RSS

Copyright (c) 2006-2010 Marimer LLC. All rights reserved.
Email admin@lhotka.net for support.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems