Condividi tramite


The Control Local Values Bug!

Intro

WPF dependency properties are properties that are registered through the WPF property system. By registering with the property system, the property will be provided a set of services such as data binding, styling, change notifications, animation, expressions, invalidation, and coercion. What I will be discussing here is dynamic value resolution and the issue of setting local values in controls.

Background

A dependency property’s value is dependent on multiple providers where there is a level of precedence for each provider. The BaseValueSource enum captures these providers (with the first being the lowest precedence and last being the highest)

· Unknown

· Default

· Inherited

· ThemeStyle

· ThemeStyleTrigger

· Style

· TemplateTrigger

· StyleTrigger

· ImplicitStyleReference

· ParentTemplate

· PatentTemplateTrigger

· Local

In addition to these base value sources, the dependency property can also come from an expression, animation, or coercion (which the internal WPF code represents them as ModifiedValues). Note that ModifiedValues are mutually exclusive from BaseValueSource. For example you can have a value that is set locally and coerced at the same time.

So how does this relate to the control local values bug?

As a control author, dependency properties are the general mechanism to communicate the state and behavior of the control. They will be used in data binding scenarios, visual representation, interaction, etc. This means that you will possibly and most likely be setting values on your dependency properties internally as state changes about your control. So as you are setting new values to a dependency property that you own, you may not realize that you are introducing a very subtle bug relating to dynamic value resolution precedence. I’ll use an example to clarify.

The DatePicker control from WPFToolkit has a DP called IsDropDownOpen. It is used by the DatePicker control to update when the calendar popup opens and closes. It listens for a change event and sets the Popup.IsOpen accordingly.

private static void OnIsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

  DatePicker dp = d as DatePicker;

  Debug.Assert(dp != null);

  bool newValue = (bool)e.NewValue;

  if (dp._popUp != null && dp._popUp.IsOpen != newValue)

  {

    dp._popUp.IsOpen = newValue;

  }

}

 

The Popup also listens for when it opens and closes and makes sure IsDropDownOpen is toggled to the correct state.

private void PopUp_Opened(object sender, EventArgs e)

{

  if (!this.IsDropDownOpen)

  {

    this.IsDropDownOpen = true;

  }

  …

}

 

Notice the bug? The bug lies in PopUp_Opened when setting IsDropDownOpen to a local value. This really appears harmless and you probably won’t actually see the bug in action until you hand it over to the app developer who will actually set different value providers on your control DPs. The problem is that setting a DP locally as an internal operation in your control will trump any and all value providers set on that DP (as local is the highest precedence). That means that the app developer who uses your control and decides to put some fancy styles, binding, triggers, and all that goodness will find that it will stop working for some reason and not immediately know what caused it.

To further describe how this can occur, here is an example using that same DatePicker (note that this has already been fixed in later version of the toolkit).   

<Window.Resources>

  <Style x:Key="DatePickerStyle" TargetType="{x:Type toolkit:DatePicker}">

    <Setter Property="IsDropDownOpen" Value="{Binding ElementName=cb_IsDropDownOpen, Path=IsChecked}" />

  </Style>

</Window.Resources>

<Grid>

  <Grid.RowDefinitions>

    <RowDefinition Height="Auto"/>

    <RowDefinition Height="*"/>

  </Grid.RowDefinitions>

  <CheckBox Content="IsDropDownOpen" Name="cb_IsDropDownOpen" />

  <toolkit:DatePicker Grid.Row="1"

                       Name="datePicker"

                       Style="{StaticResource DatePickerStyle}" />

</Grid>

 

I’ve setup a style on the DatePicker where I’ve bound a CheckBox.IsChecked DP to the DatePicker.IsDropDownOpen DP. If you click on the CheckBox, the popup opens as expected. If you take a look at the BaseValueSource on IsDropDownOpen it will read Style. However, as soon as you close the popup, the binding does not work anymore which is expected since a local value is set on IsDropDownOpen.

Summary

To make it really short and sweet, setting dependency properties to local values in internal operations of a control can lead to the dreaded control local values bug. I’m sure you’re next question is, how can this be prevented? Well, I will be doing a follow-up post on what has been added in dev10 to easily prevent this bug so stay tuned!

UPDATE: Check out the follow up post here.

Comments

  • Anonymous
    March 24, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    March 24, 2009
    How come ComboBox does not suffer from the bug? Because it uses DataBinding to control IsOpen/IsDropDownOpen. Developers who wrote DatePicker should have done more research and taken the proven route.

  • Anonymous
    May 21, 2009
    Intro Previously I did a post on the “Control Local Values bug” and how a subtle bug can be introduced

  • Anonymous
    June 14, 2009
    That's great that .NET 4.0 is introducing a straightforward solution to this serious design flaw. In the meantime, how do we work around the problem when doing our own controls development?