#151 – Dependency Properties Remember Non-Coerced Values

When you set a dependency property to some value and it is coerced, the original base value that you set is remembered.  If you remove the conditions that led to the coercion, the property will take on the non-coerced value that you originally set.

Suppose we have a Person class with an Age property and a boolean SuperOld property.  Assume that Age is coerced to a value of 999 if SuperOld is true.

If you set Age to some local value and SuperOld is true, Age will get coerced to 999.  But if you later set SuperOld to false, the Age property will revert to the last local value that you tried to set.

            Person p = new Person("Methuselah");

            p.Age = 28;
            p.SuperOld = true;    // Age coerced to 999

            p.Age = 56;           // Age still 999
            p.SuperOld = false;   // Age now becomes 56

This assumes that the PropertyChanged callback for SuperOld calls CoerceValue on the Age property.  (Likely true).

#150 – An Example of Using PropertyChanged and CoerceValue Callbacks

The PropertyChangedCallback and CoerceValueCallback can be used to enforce relationships between properties on an object.  Here’s an example, showing the relationship between BirthYear, MarriageYear and DeathYear properties on a Person object.

The BirthYear property:

        public int BirthYear
        {
            get { return (int)GetValue(BirthYearProperty); }
            set { SetValue(BirthYearProperty, value); }
        }

        public static readonly DependencyProperty BirthYearProperty =
            DependencyProperty.Register("BirthYear", typeof(int), typeof(Person),
                new PropertyMetadata(
                    1900,       // Default
                    new PropertyChangedCallback(OnBirthYearChanged)));

        public static void OnBirthYearChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Person p = (Person)d;
            p.CoerceValue(DeathYearProperty);
            p.CoerceValue(MarriageYearProperty);
        }

The DeathYear property:

        public int DeathYear
        {
            get { return (int)GetValue(DeathYearProperty); }
            set { SetValue(DeathYearProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DeathYear.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DeathYearProperty =
            DependencyProperty.Register("DeathYear", typeof(int), typeof(Person),
                new PropertyMetadata(
                    1900,       // Default
                    new PropertyChangedCallback(OnDeathYearChanged),
                    new CoerceValueCallback(CoerceDeathYear)));

        public static void OnDeathYearChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Person p = (Person)d;
            p.CoerceValue(MarriageYearProperty);
        }

        public static object CoerceDeathYear(DependencyObject d, object value)
        {
            Person p = (Person)d;
            int deathYear = (int)value;

            if (deathYear < p.BirthYear)
                deathYear = p.BirthYear;

            return deathYear;
        }

The MarriageYear property:

        public int MarriageYear
        {
            get { return (int)GetValue(MarriageYearProperty); }
            set { SetValue(MarriageYearProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DeathYear.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MarriageYearProperty =
            DependencyProperty.Register("MarriageYear", typeof(int), typeof(Person),
                new PropertyMetadata(
                    1900,       // Default
                    null,
                    new CoerceValueCallback(CoerceMarriageYear)));

        public static object CoerceMarriageYear(DependencyObject d, object value)
        {
            Person p = (Person)d;
            int marriageYear = (int)value;

            if (marriageYear < p.BirthYear)
                marriageYear = p.BirthYear;
            if (marriageYear > p.DeathYear)
                marriageYear = p.DeathYear;

            return marriageYear;
        }

#149 – Use PropertyChanged and Coercion Callbacks to Force Dependencies Between Properties

When implementing dependency properties, you typically use the CoerceValueCallback and PropertyChangedCallbacks to enforce relationships between properties.

For example, assume we have a Person class with BirthYear, DeathYear and MarriageYear properties.  We want to ensure that MarriageYear is never earlier than BirthYear or later than DeathYear.  We want these checks done any time that we change any of the three properties.

The basic plan is:

  • Allow setting BirthYear to anything
  • When setting DeathYear, don’t let it be earlier than BirthYear
  • When setting MarriageYear, don’t let it be earlier than birth or later than death

When registering the dependency properties, we therefore do the following:

  • BirthYear
    • In PropertyChangedCallback, force coercion of DeathYear and MarriageYear
  • DeathYear
    • In PropertyChangedCallback, force coercion of MarriageYear
    • In CoerceValueCallback, if value earlier than birth, set to BirthYear
  • MarriageYear
    • In CoerceValueCallback, if marriage earlier than birth, set to birth year; if later than death, set to death year

Here’s an example.

#124 – One Example of WPF’s Use of Dependency Property Callbacks

There are three callbacks related to dependency properties that a class can respond to:

  • PropertyChangedCallback – react to a new value
  • ValidateValueCallback – determine whether a value is valid
  • CoerceValueCallback – coerce a requested value to some different value

The DataGrid.FrozenColumnCount property is an example of a dependency property in a WPF class that implements all three callbacks.

FrozenColumnCount is used to specified the leftmost n columns in a data grid that should not scroll horizontally.  These columns remain fixed while the other columns scroll.

DataGrid uses the dependency property callbacks for this property as follows:

  • PropertyChangedCallback – notify constituent controls in the DataGrid of the new value so that they can render properly.  (E.g. the control that renders the column headers).
  • ValidateValueCallback – validation fails if a negative value is used for FrozenColumnCount
  • CoerceValueCallback – if value greater than the number of columns is specified, coerce FrozenColumnCount to be equal to the number of columns.