添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接


​​[翻译] WPF 中用户控件 DataContext/Binding 和依赖属性的问题​​

目录

​提问​

​回答​

​​User Control DataContext/Binding Issue with Dependency Property WPF​​


​[译者] 独立观察员 2022 年 3 月 24 日​


提问

​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​ ​ ,你将会一切顺利。



原创文章, 载请注明: ​ 转载自 ​ ​独立观察员​