navigation
 Tuesday, August 12, 2008

In my previous blog about dependency properties, I detailed a helper class that you can add to any non-DependencyObject class to give you access to dependency properties in that class. To start off with, we need to add a couple of new static functions to our helper class to handle adding bindings with converters. Our updated helper class now looks like this.

/// <summary> /// Gives you a place to host a DependencyProperty without having the owner /// inherit from DependencyObject /// </summary> /// <typeparam name="T">The type of DependencyProperty</typeparam> public class DependencyPropertyHelper<T> : DependencyObject { #region AddBinding public static void AddBinding(string path, object source, BindingMode mode, DependencyObject target, DependencyProperty property) { AddBinding(path, source, mode, target, property, null); } public static void AddBinding(string path, object source, BindingMode mode, DependencyObject target, DependencyProperty property, IValueConverter converter) { AddBinding(path, source, mode, target, property, converter, null); } public static void AddBinding(string path, object source, BindingMode mode, DependencyObject target, DependencyProperty property, IValueConverter converter, object converterParameter) { Binding binding = new Binding(path); binding.Source = source; binding.Mode = mode; binding.Converter = converter; binding.ConverterParameter = converterParameter; BindingOperations.SetBinding(target, property, binding); } #endregion #region ValueChanged Event Handler private static void valueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DependencyPropertyHelper<T> helper = (d as DependencyPropertyHelper<T>); if (helper.ValueChanged != null) helper.ValueChanged(d, e); } #endregion #region ValueChanged Event /// <summary> /// This event is tied to the DependencyProperty Value changed event. /// </summary> public event DependencyPropertyChangedEventHandler ValueChanged; #endregion #region Value public readonly static DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof(T), typeof(DependencyPropertyHelper<T>), new PropertyMetadata(new PropertyChangedCallback(valueChanged))); /// <summary> /// This is a DependencyProperty /// </summary> public T Value { get { try { if (!this.Dispatcher.CheckAccess()) return (T)Application.Current.Dispatcher.Invoke( System.Windows.Threading.DispatcherPriority.Background, (DispatcherOperationCallback)delegate { return GetValue(ValueProperty); }, ValueProperty); else return (T)GetValue(ValueProperty); } catch { return (T)ValueProperty.DefaultMetadata.DefaultValue; } } set { if (!this.Dispatcher.CheckAccess()) Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, (SendOrPostCallback)delegate { SetValue(ValueProperty, value); }, value); else SetValue(ValueProperty, value); } } #endregion }

Converters can be used for not only converting one type into another but also to make logical decisions. It turns out that they are good places to put logic that would have been put into some event handler. For example, let's go back to our example from the previous blog. We have a dependency property implemented by our helper class DependencyPropertyHelper<double> and a window that looks like this: 

DPHelper1

This example showed how to bind a custom dependency property with various UI elements together using our helper class. In this example, let's add a warning label to the form to let us know when the value is getting too high.

<Label Margin="0,109,22,126" Name="labelHigh" Content="High" Foreground="Red" 
HorizontalAlignment="Right" Width="48"></Label>

We only want this to be visible when the value of our dependency property is "too high". We need a converter to convert the double value to either a Visibility.Visible or a Visibility.Hidden value depending upon some threshold. We can set the threshold with the parameter value of the converter. Below you can see our new converter.

class ValueToVisibilityConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double threshold = System.Convert.ToDouble(parameter); double doubleValue = System.Convert.ToDouble(value); return doubleValue > threshold ? Visibility.Visible : Visibility.Hidden; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } #endregion }

We don't need to implement the ConvertBack function because this is a one-way conversion. This is a simple implementation where we take the double value and compare it to a threshold passed in via the parameter parameter. If the value is greater than the threshold then the value is converted to Visibility.Visible; if it is less than then the value is converted to Visibility.Hidden. Now all we need to do is bind the Offset value to the Visibility property of our warning label. At the end of the Window1_Loaded event handler we add our binding code.

DependencyPropertyHelper<double>.AddBinding("Value", Offset, BindingMode.OneWay, labelHigh, Label.VisibilityProperty, new ValueToVisibilityConverter(), 80.0);

So this binds the Value property of our Offset to the Visibility property on our label using an instance of our ValueToVisibilityConverter. We set the parameter to 80.0 so that values above 80 will make the warning label visible. Now we run our example and move the sliders.

screen1

Not quiet high enough; try a little higher.

screen2

Now that we have that working, what about making it a little more maintainable by moving the parameter into xaml. Lets add the following code to our xaml file.

<Window.Resources> <system:Double x:Key="highThreshold">80</system:Double> </Window.Resources>

Now edit our binding code.

DependencyPropertyHelper<double>.AddBinding("Value", Offset, BindingMode.OneWay, labelHigh, Label.VisibilityProperty, new ValueToVisibilityConverter(), Resources["highThreshold"]);

The parameter is now in the xaml file and easier to maintain.

Now let's try setting up the binding and the converter in xaml. First add an instance of our converter to the resources section of our xaml. Also add a new double value for our threshold so that the resources section looks like this:

<Window.Resources> <local:ValueToVisibilityConverter x:Key="visConverter"/> <system:Double x:Key="mediumThreshold">40</system:Double> <system:Double x:Key="highThreshold">80</system:Double> </Window.Resources>

Add a new label to our xaml complete with the binding to our custom dependency property.

<Label Foreground="Orange" Margin="109,109,112,126" Name="labelMeduim" Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Offset.Value, Mode=OneWay, Converter={StaticResource visConverter}, ConverterParameter={StaticResource mediumThreshold}}">Medium</Label>

If you noticed, we are using two separate instances of our converter — one created in our code behind and one in our xaml. Can we use one instance for both? Sure, just update the code behind binding code to this:

DependencyPropertyHelper<double>.AddBinding("Value", Offset, BindingMode.OneWay, labelHigh, Label.VisibilityProperty, (ValueToVisibilityConverter)Resources["visConverter"], Resources["highThreshold"]);

Now run the example.

screen3

The value is low, no warnings.

screen4

Our xaml only binding/converter is working.

screen5

Now both warnings are visible.

You may like the xaml only binding with the converter, but for me it's a little hard to follow. My biggest complaint is when you start building the converter in xaml, you don't get any help from IntelliSense. With the code behind, you of course have IntelliSense and none of that RelativeSource stuff.

When you embrace binding and converters in WPF you can really simplify some complex issues.

 |  |