简体   繁体   中英

Misunderstanding databinding fundamentals and DataContexts — long story

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:

替代文字

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.

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM