Unanswered Override MetaData Problem, a WPF bug?

  • Wednesday, February 21, 2007 2:30 AM
     
     

    I am creating a few custom controls (inherited from Controls.Control) in WPF. There is a base class for all my custom controls. I would like to limit the Width and Height of all my custom controls, so I override the CoerceValueCallback in metadata of WidthProperty and HeightProperty in the base class, see code:

      Static MyControlBase()
      {
                FrameworkPropertyMetadata widthPropertyMetadata = new FrameworkPropertyMetadata(null, new CoerceValueCallback(CoerceWidth));
                WidthProperty.OverrideMetadata(typeof(MyControlBase), widthPropertyMetadata);

                FrameworkPropertyMetadata heightPropertyMetadata = new FrameworkPropertyMetadata(null, new CoerceValueCallback(CoerceHeight));
                HeightProperty.OverrideMetadata(typeof(MyControlBase), heightPropertyMetadata);
      }
               
    in one of the custom controls that are derived from the base class, I override the metadata of WidthProperty and HeightProperty just to set the default values:

      Static MyDerivedControl1()
      {
                FrameworkPropertyMetadata actualWidthPropertyMetadata = new FrameworkPropertyMetadata(DefaultWidth);
                WidthProperty.OverrideMetadata(typeof(MyDerivedControl1), actualWidthPropertyMetadata);

                FrameworkPropertyMetadata actualHeightPropertyMetadata = new FrameworkPropertyMetadata(DefaultHeight);
                HeightProperty.OverrideMetadata(typeof(MyDerivedControl1), actualHeightPropertyMetadata);
             }  
               
    There is another derived custom control that doesn't override the metadata of WidthProperty and HeightProperty

      Static MyDerivedControl2()
      {
      //doesn't override metadata of WidthProperty and heightProperty
      }
      
    Now let's test MyDerivedControl1 and MyDerivedControl2 in a simple WPF window application.
     (1) create an instance of MyDerivedControl1 and change its Width and Height. The CoerceValueCallback functions for Width (CoerceWidth) and for Height (CoerceHeight) in the base class are never triggered. Test Fails !
     (2) create an instance of MyDerivedControl2 and change its Width and Height. The CoerceValueCallback functions for Width (CoerceWidth) and for Height (CoerceHeight) in the base class are triggered. Test Passes!
     (3) Create an Instance of MyDerivedControl2, then create an instance of MyDerivedControl1, and change the width and height of MyDerivedControl1, The CoerceValueCallback functions for Width (CoerceWidth) and for Height (CoerceHeight) in the base class are triggered. ahhh, Why ?

    Have any one faced simaliar problem? Can any one explain what is going wrong?

    Plus: the same thing happens to Canvas.LeftProperty and Canvas.TopProperty

All Replies

  • Thursday, February 22, 2007 9:31 AM
     
     

    Finally I know where the problem comes from.

    the root cause is, the static constructor of derived classes are called before the base class static constructor. I assumed that the static constructor of base class is called before the static constructor of derived classes was called, i was wrong.

    I understand that I can call a static field/method of the base class in the first line of the static construcor of a derived class, in order to force the base class static constructor to be executed before the derived class static constructor. However, That's does't sounds a good solution. Is there any better way?

  • Thursday, February 22, 2007 3:02 PM
     
     
    To limit Width and Height you should set the MinWidth, MaxWidth etc. properties.
  • Friday, February 23, 2007 2:25 PM
     
     

    the static constructor of derived classes are called before the base class static constructor. I assumed that the static constructor of base class is called before the static constructor of derived classes was called, i was wrong.

    It's not what is happenign. A static constructor, per the CLR specification, is initialized at the latest the first time a type is used. Static constructors are type-specific and inheritance doesn't impact them at all. Whenever your type gets loaded, static constructors get called. As it happens, in your child element, the dependency property defined on the parent type gets called, and in turn the parent constructor gets called, returns, followed by the return of the child constructor. Small code sample:

       17     public class TestControl : System.Windows.Controls.Control

       18     {

       19         public bool MyProperty

       20         {

       21             get { return (bool)GetValue(MyPropertyProperty); }

       22             set { SetValue(MyPropertyProperty, value); }

       23         }

       24 

       25         public static readonly DependencyProperty MyPropertyProperty =

       26             DependencyProperty.Register("MyProperty", typeof(bool), typeof(TestControl), new FrameworkPropertyMetadata(false));

       27 

       28         static TestControl()

       29         {

       30             Debug.WriteLine(string.Format("START - TestControl: {0}", DateTime.Now.ToString()));

       31             DefaultStyleKeyProperty.OverrideMetadata(typeof(TestControl), new FrameworkPropertyMetadata(typeof(TestControl)));

       32             Debug.WriteLine(string.Format("STOP - TestControl: {0}", DateTime.Now.ToString()));

       33         }

       34 

       35     }

       36     public class ChildTestControl : TestControl

       37     {

       38         static ChildTestControl()

       39         {

       40             Debug.WriteLine(string.Format("START - ChildTestControl: {0}", DateTime.Now.ToString()));

       41             MyPropertyProperty.OverrideMetadata(typeof(ChildTestControl), new FrameworkPropertyMetadata(true));

       42             Debug.WriteLine(string.Format("STOP - ChildTestControl: {0}", DateTime.Now.ToString()));

       43         }

       44     }

    Which, when put in an application, generates the following debug output:

    START - ChildTestControl: 23/02/2007 14:07:23

    START - TestControl: 23/02/2007 14:07:23

    STOP - TestControl: 23/02/2007 14:07:23

    STOP - ChildTestControl: 23/02/2007 14:07:23

    So of course, your parent dependency property is initialized before your child try to override it's metadata. Otherwise, the dependency property itself would be null and would be throwing a null reference exception!

    But in the example you provide, both the child types use a common parent dependency property. The property owner stays FrameworkElement. The fact that OverrideMetadata tries to merge the metadata along the inheritance chain is the reason why, in your example, when you rparent object wasn't initialized the child didnt get the callback.

    The solution of course is to ensure that your parent type is the owner of its dependency property, and that the children override their parent's dp instad of the FrameworkElement one. The following will solve your issue:

       17     public class TestControl : System.Windows.Controls.Control

       18     {

       19         public new static readonly DependencyProperty WidthProperty;

       20         static TestControl()

       21         {

       22             WidthProperty = FrameworkElement.WidthProperty.AddOwner(typeof(TestControl), new FrameworkPropertyMetadata((double)2));

       23         }

       24 

       25     }

       26     public class ChildTestControl : TestControl

       27     {

       28         static ChildTestControl()

       29         {

       30             TestControl.WidthProperty.OverrideMetadata(typeof(ChildTestControl), new FrameworkPropertyMetadata((double)1));

       31         }

       32     }

  • Friday, February 23, 2007 6:45 PM
     
     
        Well, all your test makes sense to me, and the behaviour you see is just what the WPF DP metadata is designed for, DP metadata is created per type basis, if you don't explicitly call OverrideMetadata to provide a new copy of the metadata for your derived class, your derived class will automatically pick up those metadata "inherited" from its base class. this can explain why the CoerceValueCallback delegate get called whenever you create an instance of MyDerivedControl2(in test1 and test3), because CoerceValueCallback along with PropertyChangedCallback are available as part of a specified DP's metadata definition, and since MyDerivedControl2 doesn't override any metadata for its DPs, it will automatically pick up its base class's metadata, so the two CoerceValueCallbacks get called.
        And as for how the static constructors get called, some programming languages such as JAVA provides a semantic that whenever you called a type constructor, all its base class's type constructor gets called too, but CLR doesn't provide such a semantic, but CLR does provide the ability to programmatically call an arbitrary's type constructor using System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor method. because WPF's property system comes specially, WPF does needs to make all "derived" DPs available before doing anything meaningful, so I believe WPF does call this method internally.

    Sheva
  • Monday, February 26, 2007 9:52 AM
     
     

    WPF does needs to make all "derived" DPs available before doing anything meaningful, so I believe WPF does call this method internally.

    I don't believe it does, as the example provided shows. As long as you use OverrideMetadata instead of AddOwner, the parent type constructor never gets called because the type itself never gets initialized. The reason being that ParentElement.MyProperty doesn't actually exist, it is provided as a shortcut to FrameworkElement.MyProperty. ParentElement never gets initialized, even though the child element does.