views:

97

answers:

1

I've been using databinding in several simple situations with pretty good success. Usually I just use INotifyPropertyChanged to enable my codebehind to modify the GUI values on screen, rather than implement dependency properties for everything.

I am playing with an LED control to learn more about databinding in user controls, and was forced to use dependency properties because VS2008 told me I had to. My application is straightforward -- I have a window that displays several LED controls, each with a number above it and optionally, one to its side. The LEDs should be definable with a default color, as well as change state.

I started by writing an LED control, which seemed to go perfectly fine. First, I started with code like this:

LED.xaml

<UserControl x:Class="LEDControl.LED"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="Auto" Width="Auto">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <!-- LED portion -->
        <Ellipse Grid.Column="0" Margin="3" Height="{Binding LEDSize}" Width="{Binding LEDSize}" Fill="{Binding LEDColor}" StrokeThickness="2" Stroke="DarkGray" />
        <Ellipse Grid.Column="0" Margin="3" Height="{Binding LEDSize}" Width="{Binding LEDSize}">
            <Ellipse.Fill>
                <RadialGradientBrush GradientOrigin="0.5,1.0">
                    <RadialGradientBrush.RelativeTransform>
                        <TransformGroup>
                            <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.5" ScaleY="1.5"/>
                            <TranslateTransform X="0.02" Y="0.3"/>
                        </TransformGroup>
                    </RadialGradientBrush.RelativeTransform>
                    <GradientStop Offset="1" Color="#00000000"/>
                    <GradientStop Offset="0.4" Color="#FFFFFFFF"/>
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <!-- label -->
        <TextBlock Grid.Column="1" Margin="3" VerticalAlignment="Center" Text="{Binding LEDLabel}" />
    </Grid>
</UserControl>

This draws an LED just fine. I then bound LEDSize, LEDLabel, and LEDColor to the Ellipse properties by setting this.DataContext = this like I always do:

LED.xaml.cs

/// <summary>
/// Interaction logic for LED.xaml
/// </summary>
public partial class LED : UserControl, INotifyPropertyChanged
{
    private Brush state_color_;
    public Brush LEDColor
    {
        get { return state_color_; }
        set { 
            state_color_ = value;
            OnPropertyChanged( "LEDColor");
        }
    }

    private int led_size_;
    public int LEDSize
    {
        get { return led_size_; }
        set {
            led_size_ = value;
            OnPropertyChanged( "LEDSize");
        }
    }

    private string led_label_;
    public string LEDLabel
    {
        get { return led_label_; }
        set {
            led_label_ = value;
            OnPropertyChanged( "LEDLabel");
        }
    }

    public LED()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged( string property_name)
    {
        if( PropertyChanged != null)
            PropertyChanged( this, new PropertyChangedEventArgs( property_name));
    }

    #endregion
}

At this point, I can change the property values and see that the LED changes size, color and its label. Great!

I want the LED control to be reusable in other widgets that I write over time, and the next step for me was to create another UserControl (in a separate assembly), called IOView. IOView is pretty basic at this point:

IOView.xaml

<UserControl x:Class="IOWidget.IOView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:led="clr-namespace:LEDControl;assembly=LEDControl"
    Height="Auto" Width="Auto">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" HorizontalAlignment="Center" Text="{Binding Path=Index}" />
        <led:LED Grid.Row="1" HorizontalContentAlignment="Center" HorizontalAlignment="Center" LEDSize="30" LEDColor="Green" LEDLabel="Test" />
    </Grid>
</UserControl>

Notice that I can modify the LED properties in XAML at design time and everything works as expected:

alt text

I then blindly tried to databind LEDColor to my IOView, and VS2008 kindly told me "A 'Binding' cannot be set on the 'LEDColor' property of type 'LED'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject." Oops! I hadn't even realized that since I haven't made my own GUI controls before. Since the LEDColor is already databound to the Ellipse, I added a DependencyProperty called Color.

LED.xaml.cs

    public static DependencyProperty ColorProperty = DependencyProperty.Register( "Color", typeof(Brush), typeof(LED));
    public Brush Color
    {
        get { return (Brush)GetValue(ColorProperty); }
        set { 
            SetValue( ColorProperty, value);
            LEDColor = value;
        }
    }

Note that I set the property LEDColor in the setter, since that's how the Ellipse knows what color it should be.

The next baby step involved setting the color of the LED in my IOView by binding to IOView.InputColor:

IOView.xaml.cs:

/// <summary>
/// Interaction logic for IOView.xaml
/// </summary>
public partial class IOView : UserControl, INotifyPropertyChanged
{
    private Int32 index_;
    public Int32 Index
    {
        get { return index_; }
        set {
            index_ = value;
            OnPropertyChanged( "Index");
        }
    }

    private Brush color_;
    public Brush InputColor
    {
        get { return color_; }
        set {
            color_ = value;
            OnPropertyChanged( "InputColor");
        }
    }

    private Boolean state_;
    public Boolean State
    {
        get { return state_; }
        set {
            state_ = value;
            OnPropertyChanged( "State");
        }
    }

    public IOView()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged( string property_name)
    {
        if( PropertyChanged != null)
            PropertyChanged( this, new PropertyChangedEventArgs( property_name));
    }

    #endregion
}

and in IOView.xaml, I changed the LED to this:

<led:LED Grid.Row="1" HorizontalContentAlignment="Center" HorizontalAlignment="Center" LEDSize="30" Color="{Binding InputColor}" />

But it's not working, because of the following error in the Output window:

BindingExpression path error: 'InputColor' property not found on 'object' ''LED' (Name='')'. BindingExpression:Path=InputColor; DataItem='LED' (Name=''); target element is 'LED' (Name=''); target property is 'Color' (type 'Brush')

Hmm... so for some reason, my DataBinding is messed up. I can get the LED to work on its own with databinding, but once I wrap it in another control and set its datacontext, it doesn't work. I'm not sure what to try at this point.

I'd love to get as detailed an answer as possible. I know that I could have just retemplated a CheckBox to get the same results, but this is an experiment for me and I'm trying to understand how to databind to controls' descendants.

+5  A: 

There's a lot to say about all this, but let me see if I can provide pointers that addresses some of your misunderstandings:

  • In order for a property to be the target of a binding, that property must be a dependency property. WPF (and Silverlight) use dependency properties as a means for tracking changes, supporting value precedence (for animations and the like), and a bunch of other useful things. Note that I said "target". The source of a binding can be any old object that supports change notification.
  • Setting a UserControl's DataContext within the UserControl itself is considered bad practice because any consumer of your control can change it, and doing so will break any bindings within your control that depend on that context
  • In addition to the above point, the other issue is that you will break any bindings in consuming code that depend on the data context "above" the user control. This explains the issue you're seeing with InputColor not successfully binding. The InputColor property is in the data context provided by the host control (IOView) but the data context of the LED is set to the LED itself, so the property can't be found without qualifying the binding further.

Following this advice leads to the following implementation (not tested):

LED.xaml.cs:

public partial class LED : UserControl
{
    public static readonly DependencyProperty LEDColorProperty = DependencyProperty.Register(
        "LEDColor",
        typeof(Brush),
        typeof(LED));

    public Brush LEDColor
    {
        get { return this.GetValue(LEDColorProperty) as Brush; }
        set { this.SetValue(LEDColorProperty, value); }
    }

    // LEDSize and LEDLabel omitted for brevity, but they're very similar to LEDColor

    public LED()
    {
        InitializeComponent();
    }
}

LED.xaml:

<UserControl
    x:Name="root"
    x:Class="LEDControl.LED"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto">

    <Grid DataContext="{Binding ElementName=root}>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <!-- LED portion -->
        <Ellipse Grid.Column="0" Margin="3" Height="{Binding LEDSize}" Width="{Binding LEDSize}" Fill="{Binding LEDColor}" StrokeThickness="2" Stroke="DarkGray" />
        <Ellipse Grid.Column="0" Margin="3" Height="{Binding LEDSize}" Width="{Binding LEDSize}">
            <Ellipse.Fill>
                <RadialGradientBrush GradientOrigin="0.5,1.0">
                    <RadialGradientBrush.RelativeTransform>
                        <TransformGroup>
                            <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.5" ScaleY="1.5"/>
                            <TranslateTransform X="0.02" Y="0.3"/>
                        </TransformGroup>
                    </RadialGradientBrush.RelativeTransform>
                    <GradientStop Offset="1" Color="#00000000"/>
                    <GradientStop Offset="0.4" Color="#FFFFFFFF"/>
                </RadialGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <!-- label -->
        <TextBlock Grid.Column="1" Margin="3" VerticalAlignment="Center" Text="{Binding LEDLabel}" />
    </Grid>
</UserControl>

IOView.xaml:

<UserControl x:Name="root"
    x:Class="IOWidget.IOView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:led="clr-namespace:LEDControl;assembly=LEDControl"
    Height="Auto" Width="Auto">

    <Grid DataContext="{Binding ElementName=root}">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" HorizontalAlignment="Center" Text="{Binding Path=Index}" />
        <led:LED Grid.Row="1" HorizontalContentAlignment="Center" HorizontalAlignment="Center" LEDSize="{Binding I_Can_Bind_Here_All_I_Like}" LEDColor="{Binding I_Can_Bind_Here_All_I_Like}" LEDLabel="{Binding I_Can_Bind_Here_All_I_Like}" />
    </Grid>
</UserControl>

HTH,
Kent

Kent Boogaart
@Kent thanks for the thorough explanation. I'll definitely have to not blindly set the datacontext like I do for ViewModels since it's a totally different situation. I applied your changes, and was wondering how using the XAML approach to setting the DataContext prevents a UserControl's consumer from still changing it from code-behind (maybe it doesn't). Your changes *almost* work, and perhaps I'm missing something. I've got a timer that changes the IOView's State property, and this should change the InputColor from green to red depending upon its value. It changes *once* only...
Dave
@Dave: you're welcome. It's not using XAML to set the `DataContext` that matters - it's the fact that I've set it on a *child* of the `UserControl` rather than the `UserControl` itself. You could equally do this in code, but XAML is just easier. As for your other question, I think I'd need to see the code to answer accurately...
Kent Boogaart
@Kent I followed your code examples *exactly*, and in IOView's XAML, I set the databinding for LEDColor and LEDLabel like this: `LEDColor={Binding Color} LEDLabel={Binding Index}`, where `Color` and `Index` are properties in IOView (which implements INotifyPropertyChanged as in my post). The interesting thing is that the LEDLabel databinds just fine, but the LEDColor doesn't do it quite right. I'll update my post to show how I try to change the color from code-behind. Thanks again for your time.
Dave
@Kent LOL! Ok, in posting my code example, I realized that my timer (for testing purposes) was just setting the state to a constant value, so that's why it only changed once. This totally works now!
Dave