WPF中用户控件DataContext/Binding和依赖属性的问题

ProgrammingDude(asked Dec 8, 2015 at 21:24)

Ok, so my problem is I have a user control. In the xaml I bind some colors to color properties that I have created as shown below.

好,我的问题是,我有一个用户控件,在 Xaml 中,我绑定了一些颜色到颜色属性,如下所示:.

<GradientStop x:Name="stop1" Color="{Binding Color1}" Offset="0"/>
<GradientStop x:Name="stop2" Color="{Binding Color2}" Offset="1"/>

In my code behind I have a DependencyProperty that I have declared as shown below.

在后台代码中,我声明了一个依赖属性,如下所示:

public static readonly DependencyProperty IsActiveProperty =
    DependencyProperty.Register("IsActive", typeof(bool), typeof(Bin),
        new PropertyMetadata(new PropertyChangedCallback(Bin.IsActivePropertyChanged)));

The dependency property has a PropertyChangedCallback that it calls called IsActivePropertyChanged as shown below.

该依赖属性有一个 PropertyChangedCallback 方法,名称为 IsActivePropertyChanged,如下所示:

private static void IsActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Bin b = (Bin)d;
    if((bool)e.NewValue)
    {
        b.Color1 = Color.FromArgb(0xFF, 0x3E, 0x3E, 0x3E);
        b.Color2 = Colors.Red;
        b.Color3 = Colors.Red;
        b.Color4 = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
    }
    else
    {
        b.Color1 = Color.FromArgb(0xFF, 0x3E, 0x3E, 0x3E);
        b.Color2 = Color.FromArgb(0xFF, 0x83, 0x83, 0x83);
        b.Color3 = Color.FromArgb(0xFF, 0x63, 0x63, 0x63);
        b.Color4 = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
    }
}

If I use the constructor below, the color changes inside of the constructor work fine, however, my IsActivePropertyChangedEvent never gets fired. I am assuming because of the DataContext assignment in the constructor.

如果我使用下面的构造函数,在构造函数中的颜色改变工作正常,然而,我的 IsActivePropertyChangedEvent 从未被触发。我估计是因为在构造函数中指定了 DataContext。

public Bin()
{
    Color1 = Color.FromArgb(0xFF, 0x3E, 0x3E, 0x3E);
    Color2 = Color.FromArgb(0xFF, 0x83, 0x83, 0x83);
    Color3 = Color.FromArgb(0xFF, 0x63, 0x63, 0x63);
    Color4 = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
    InitializeComponent();
    DataContext = this;
}

If I comment out the DataContext assignment and use the constructor below, my Color assignments do not work, but the IsActivePropertyChanged event fires fine.

如果我注释掉 DataContext 赋值,使用如下的构造函数,我的颜色赋值就没起作用,但 IsActivePropertyChanged 事件能够被触发。

public Bin()
{
    Color1 = Color.FromArgb(0xFF, 0x3E, 0x3E, 0x3E);
    Color2 = Color.FromArgb(0xFF, 0x83, 0x83, 0x83);
    Color3 = Color.FromArgb(0xFF, 0x63, 0x63, 0x63);
    Color4 = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);
    InitializeComponent();
    //DataContext = this;
}

My question is how can i get the binding to work correctly and have my event fire as well. I have tried setting the DataContext="{Binding RelativeSource={RelativeSource Self}}" (instead of setting the DataContext in the code behind) of the items that are bound to the Color Properties in XAML, a rectangle and a polygon, but that didn't seem to work. Thanks in advance for any help.

我的问题就是,怎样能让绑定和事件触发都正常工作。我尝试了为 Xaml 中绑定到 Color 属性的元素设置 DataContext="{Binding RelativeSource={RelativeSource Self}}" (而不是在后台代码中设置 DataContext),一个矩形和一个多边形,但似乎不起作用。提前感谢任何提供的帮助。

回答

vesan(answered Dec 9, 2015 at 22:48)

When writing your own control, you shouldn't mess with the DataContext of the control itself.

Instead, on the binding of the GradientStop, you can use RelativeSource={RelativeSource AncestorType=Bin} (assuming Bin is your control). Or you can define a template and use TemplateBinding. Check this answer I wrote a while back for a similar question - it has more detailed description of how this works.

在写你自己的控件时,你不应当把控件自身的 DataContext 弄乱。

取而代之的是,在绑定 GradientStop 时,你可以使用 RelativeSource={RelativeSource AncestorType=Bin} (假设 Bin 是你的控件)。或者,你可以定义一个模板并且使用 TemplateBinding。查看我不久之前为一个类似的问题写的 这个回答 ,其中有关于这如何工作的更多详细描述。

(译者注:他上面提到的详细回答如下)

vesan(answered May 28, 2015 at 2:26)

Although this has now been solved there seems to be some, in my opinion, inappropriate use of the DataContext.

When developing a custom reusable control, you should not set DataContext at all. What the DataContext will be, that is for the user of the control to decide, not for the developer. Consider the following common pattern of code:

虽然这个现在已经被解决了(译者注:另一个回答被标记为答案),在我看来,似乎有一些,对 DataContext  的不正确使用。

当开发一个定制的可复用的控件,你根本不应该设置 DataContext  。DataContext 将会是什么,应当是控件使用者来决定的,而不是开发者。思考下面的常见模式代码:

<Grid DataContext="{Binding Data}">
    <TextBox Text="{Binding TextValue1}" />
    <!-- Some more controls -->
</Grid>

Notice that here, you are using the Grid control. The developer of the control (in this case, the WPF team), didn't touch the DataContext at all - that is up to you. What does it mean for you as a control developer? Your DependencyProperty definition is fine, but you shouldn't touch the DataContext. How will you then bind something inside your control to the DependencyProperty value? A good way is using a template (namespaces omitted):

注意这里,你正在使用 Grid 控件。控件的开发者(这个例子中,是 WPF 团队),根本没有碰 DataContext —— 这个是你来用的。那么这对于作为控件开发者的你来说意味着什么呢?你的依赖属性定义是没问题的,但你不应该碰 DataContext 。那么你之后怎么将控件中的一些东西绑定到依赖属性的值呢?一个好的方式就是使用模板(命名空间省略了):

<MyTimePicker>
    <MyTimePicker.Template>
        <ControlTemplate TargetType="MyTimePicker">
            <!-- Stuff in your control -->
            <TextBlock Text="{TemplateBinding Time}" />
            <TextBox Text="{Binding Time, RelativeSource={RelativeSource TemplatedParent}}" />
        </ControlTemplate>
    <MyTimePicker.Template>
</MyTimePicker>

Note that TemplateBinding is always one-way only, so if you need any editing at all, you need to use normal binding (as you can see on the TextBox in the example).

This only means that the TextBlock/Box inside your control will get its Time value from your custom control itself, ignoring any DataContext you might have set.

注意 TemplateBinding 总是 one-way 的,所以如果你需要任何编辑,你需要使用普通的 binding (正如你在 TextBox 示例中看到的那样)。

这仅仅意味着你的控件内部的 TextBlock/Box 会从你自定义控件自身获得它的 Time 值,忽略任何你可能设置的 DataContext 。

Then, when you use the control, you do it like this (added to my first example):

然后,当你使用这个控件时,你会这样做(被添加到我的第一个示例):

<Grid DataContext="{Binding Data}">
    <TextBox Text="{Binding TextValue1}" />
    <!-- Some more controls -->
    <MyTimePicker Time="{Binding TimeValue}" />
</Grid>

What just happened here is that the MyTimePicker does not have DataContext set anywhere at all - it gets it from the parent control (the Grid). So the value goes like this: Data-->(binding)-->MyTimePicker.Time-->(template binding)-->TextBlock.Text.

这里发生的是, MyTimePicker 的 DataContext 根本没有在任何地方被设置,而是从父控件(Grid)获取。所以数值(译者注:这里是 TimeValue)像这样流转:Data-->(binding)-->MyTimePicker.Time-->(template binding)-->TextBlock.Text 

And above all, avoid doing this in the constructor of your custom control:

首要的是,避免在你的自定义控件的构造函数中这样做:

public MyTimePicker()
{
    InitializeComponent();
    DataContext = this;
}

This will override any DataContext set in XAML, which will make binding a huge pain (because you'll have to always set Source manually). The previous example would not work, and this wouldn't work either:

这会覆盖在 Xaml 中设置的任何 DataContext ,会使得绑定变成一个大痛苦(因为你将不得不总是手动设置 Source )。之前的示例将不能工作,下面这个也不能工作:

<MyTimePicker DataContext="{Binding Data}" Time="{Binding TimeValue}" />

You would think this is OK, but the DataContext will be resolved in the InitializeComponent() call, so the value will be immediately overwritten. So the binding to TimeValue will look for it in the control instead (which will, of course, fail).

你可能认为这是可以的,但 DataContext 会在 InitializeComponent() 调用中被重新处理,所以值会被立即重写。所以目标为 TimeValue 的绑定反而会在控件中搜寻(这个当然会失败)。

Just don't touch the DataContext when developing a control and you'll be fine.

(总之,)开发一个控件时就别碰 DataContext ,你将会一切顺利。