关于.net:如何在RelativeSource中使用WPF绑定?

How do I use WPF bindings with RelativeSource?

如何将RelativeSource与wpf绑定一起使用,以及有哪些不同的用例?


如果要绑定到对象上的另一个属性:

1
{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}

如果要获取祖先的属性:

1
2
{Binding Path=PathToProperty,
    RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}

如果要获取模板化父级的属性(因此可以在ControlTemplate中执行双向绑定)

1
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}

或者,更短(这仅适用于单向绑定):

1
{TemplateBinding Path=PathToProperty}


1
2
3
4
Binding RelativeSource={
    RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...

RelativeSource的默认属性是Mode属性。这里给出了一组完整的有效值(来自msdn):

  • PreviousData允许您绑定正在显示的数据项列表中的上一个数据项(而不是包含该数据项的控件)。

  • templatedParent是指应用模板(其中存在数据绑定元素)的元素。这类似于设置TemplateBindingExtension,仅当绑定在模板内时才适用。

  • self引用要在其上设置绑定的元素,并允许将该元素的一个属性绑定到同一元素的另一个属性。

  • findancestor是指数据绑定元素的父链中的祖先。可以使用它绑定到特定类型或其子类的祖先。这是要指定ancestortype和/或ancestorlevel时使用的模式。


下面是MVVM体系结构上下文中更直观的解释:

enter image description here


假设这个例子,一个长方形,我们希望它的高度总是等于它的宽度,比如说一个正方形。我们可以使用元素名

1
2
3
4
5
<Rectangle Fill="Red" Name="rectangle"
                    Height="100" Stroke="Black"
                    Canvas.Top="100" Canvas.Left="100"
                    Width="{Binding ElementName=rectangle,
                    Path=Height}"/>

但在上面的例子中,我们必须指出绑定对象的名称,即矩形。我们可以用相对资源来达到同样的目的

1
2
3
4
<Rectangle Fill="Red" Height="100"
                   Stroke="Black"
                   Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Height}"/>

在这种情况下,我们没有义务提及绑定对象的名称,并且只要高度发生变化,宽度将始终等于高度。

如果要将宽度参数化为高度的一半,则可以通过向绑定标记扩展添加转换器来完成此操作。现在让我们设想另一个案例:

1
2
 <TextBlock Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Parent.ActualWidth}"/>

上述情况用于将给定元素的给定属性与其直接父元素之一绑定,因为此元素包含称为父元素的属性。这将引导我们进入另一个相对源模式,即findancestor模式。


Bechir Bejaoui在本文中公开了WPF中RelativeSources的用例:

The RelativeSource is a markup extension that is used in particular
binding cases when we try to bind a property of a given object to
another property of the object itself, when we try to bind a property
of a object to another one of its relative parents, when binding a
dependency property value to a piece of XAML in case of custom control
development and finally in case of using a differential of a series of
a bound data. All of those situations are expressed as relative source
modes. I will expose all of those cases one by one.

  • Mode Self:
  • Imagine this case, a rectangle that we want that its height is always
    equal to its width, a square let's say. We can do this using the
    element name

    1
    2
    3
    4
    5
    <Rectangle Fill="Red" Name="rectangle"
                    Height="100" Stroke="Black"
                    Canvas.Top="100" Canvas.Left="100"
                    Width="{Binding ElementName=rectangle,
                    Path=Height}"/>

    But in this above case we are obliged to indicate the name of the
    binding object, namely the rectangle. We can reach the same purpose
    differently using the RelativeSource

    1
    2
    3
    4
    <Rectangle Fill="Red" Height="100"
                   Stroke="Black"
                   Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Height}"/>

    For that case we are not obliged to mention the name of the binding
    object and the Width will be always equal to the Height whenever the
    height is changed.

    If you want to parameter the Width to be the half of the height then
    you can do this by adding a converter to the Binding markup extension.
    Let's imagine another case now:

    1
    2
     <TextBlock Width="{Binding RelativeSource={RelativeSource Self},
                   Path=Parent.ActualWidth}"/>

    The above case is used to tie a given property of a given element to
    one of its direct parent ones as this element holds a property that is
    called Parent. This leads us to another relative source mode which is
    the FindAncestor one.

  • Mode FindAncestor
  • In this case, a property of a given element will be tied to one of its
    parents, Of Corse. The main difference with the above case is the fact
    that, it's up to you to determine the ancestor type and the ancestor
    rank in the hierarchy to tie the property. By the way try to play with
    this piece of XAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <Canvas Name="Parent0">
        <Border Name="Parent1"
                 Width="{Binding RelativeSource={RelativeSource Self},
                 Path=Parent.ActualWidth}"
                 Height="{Binding RelativeSource={RelativeSource Self},
                 Path=Parent.ActualHeight}">
            <Canvas Name="Parent2">
                <Border Name="Parent3"
                Width="{Binding RelativeSource={RelativeSource Self},
               Path=Parent.ActualWidth}"
               Height="{Binding RelativeSource={RelativeSource Self},
                  Path=Parent.ActualHeight}">
                   <Canvas Name="Parent4">
                   <TextBlock FontSize="16"
                   Margin="5" Text="Display the name of the ancestor"/>
                   <TextBlock FontSize="16"
                     Margin="50"
                Text="{Binding RelativeSource={RelativeSource  
                           FindAncestor,
                           AncestorType={x:Type Border},
                           AncestorLevel=2},Path=Name}"
                           Width="200"/>
                    </Canvas>
                </Border>
            </Canvas>
         </Border>
       </Canvas>

    The above situation is of two TextBlock elements those are embedded
    within a series of borders and canvas elements those represent their
    hierarchical parents. The second TextBlock will display the name of
    the given parent at the relative source level.

    So try to change AncestorLevel=2 to AncestorLevel=1 and see what
    happens. Then try to change the type of the ancestor from
    AncestorType=Border to AncestorType=Canvas and see what's happens.

    The displayed text will change according to the Ancestor type and
    level. Then what's happen if the ancestor level is not suitable to the
    ancestor type? This is a good question, I know that you're about to
    ask it. The response is no exceptions will be thrown and nothings will
    be displayed at the TextBlock level.

  • TemplatedParent
  • This mode enables tie a given ControlTemplate property to a property
    of the control that the ControlTemplate is applied to. To well
    understand the issue here is an example bellow

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <Window.Resources>
    <ControlTemplate x:Key="template">
            <Canvas>
                <Canvas.RenderTransform>
                    <RotateTransform Angle="20"/>
                    </Canvas.RenderTransform>
                <Ellipse Height="100" Width="150"
                     Fill="{Binding
                RelativeSource={RelativeSource TemplatedParent},
                Path=Background}">

                  </Ellipse>
                <ContentPresenter Margin="35"
                      Content="{Binding RelativeSource={RelativeSource  
                      TemplatedParent},Path=Content}"/>
            </Canvas>
        </ControlTemplate>
    </Window.Resources>
        <Canvas Name="Parent0">
        <Button   Margin="50"
                  Template="{StaticResource template}" Height="0"
                  Canvas.Left="0" Canvas.Top="0" Width="0">
            <TextBlock FontSize="22">Click me</TextBlock>
        </Button>
     </Canvas>

    If I want to apply the properties of a given control to its control
    template then I can use the TemplatedParent mode. There is also a
    similar one to this markup extension which is the TemplateBinding
    which is a kind of short hand of the first one, but the
    TemplateBinding is evaluated at compile time at the contrast of the
    TemplatedParent which is evaluated just after the first run time. As
    you can remark in the bellow figure, the background and the content
    are applied from within the button to the control template.


    在wpf RelativeSource绑定中,将三个properties公开设置:

    1。模式:这是一个enum,可以有四个值:

    a. PreviousData(value=0): It assigns the previous value of the property to
    the bound one

    b. TemplatedParent(value=1): This is used when defining the templates of
    any control and want to bind to a value/Property of the control.

    For example, define ControlTemplate:

    1
    2
    3
      <ControlTemplate>
            <CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
     </ControlTemplate>

    c. Self(value=2): When we want to bind from a self or a property of self.

    For example: Send checked state of checkbox as CommandParameter while setting the Command on CheckBox

    1
    <CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />

    d. FindAncestor(value=3): When want to bind from a parent control
    in Visual Tree.

    For example: Bind a checkbox in records if a grid,if header checkbox is checked

    1
    <CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />

    2。ancestor type:当模式为FindAncestor时,定义什么类型的祖先

    1
    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}

    三。AncestorLevel:当模式为FindAncestor时,那么祖先的级别是什么(如果visual tree中有两个相同类型的父级)

    1
    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}

    Above are all use-cases for RelativeSource binding.

    这是一个参考链接。


    不要忘记模板父级:

    1
    <Binding RelativeSource="{RelativeSource TemplatedParent}"/>

    1
    {Binding RelativeSource={RelativeSource TemplatedParent}}

    我创建了一个库来简化WPF的绑定语法,包括使使用RelativeSource更容易。下面是一些例子。之前:

    1
    2
    3
    4
    {Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
    {Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
    {Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
    {Binding Path=Text, ElementName=MyTextBox}

    后:

    1
    2
    3
    4
    {BindTo PathToProperty}
    {BindTo Ancestor.typeOfAncestor.PathToProperty}
    {BindTo Template.PathToProperty}
    {BindTo #MyTextBox.Text}

    下面是一个如何简化方法绑定的示例。之前:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // C# code
    private ICommand _saveCommand;
    public ICommand SaveCommand {
     get {
      if (_saveCommand == null) {
       _saveCommand = new RelayCommand(x => this.SaveObject());
      }
      return _saveCommand;
     }
    }

    private void SaveObject() {
     // do something
    }

    // XAML
    {Binding Path=SaveCommand}

    后:

    1
    2
    3
    4
    5
    6
    7
    // C# code
    private void SaveObject() {
     // do something
    }

    // XAML
    {BindTo SaveObject()}

    您可以在以下位置找到库:http://www.simplygoodcode.com/2012/08/simper-wpf-binding.html

    请注意,在"之前"的示例中,我使用方法绑定时,代码已经通过使用RelayCommand进行了优化,我最后检查的不是wpf的本机部分。如果没有这一点,"以前"的例子会更长。


    值得注意的是,对于那些跌跌撞撞地想到Silverlight的人:

    Silverlight只提供这些命令的简化子集


    一些有用的碎片:

    下面主要介绍如何在代码中执行此操作:

    1
    2
    3
    4
    Binding b = new Binding();
    b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, this.GetType(), 1);
    b.Path = new PropertyPath("MyElementThatNeedsBinding");
    MyLabel.SetBinding(ContentProperty, b);

    我基本上是从代码隐藏中的绑定相对源复制的。

    另外,就示例而言,msdn页面非常好:relativesource类


    我刚刚发布了另一个解决方案,用于访问Silverlight中为我工作的父元素的DataContext。它使用Binding ElementName


    我没有阅读每个答案,但我只想在按钮的相对源命令绑定的情况下添加这些信息。

    当您对Mode=FindAncestor使用相对源时,绑定必须如下所示:

    1
    Command="{Binding Path=DataContext.CommandProperty, RelativeSource={...}}"

    如果不在路径中添加DataContext,则在执行时无法检索属性。


    这是一个使用此模式的示例,它在空数据报上对我有效。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <Style.Triggers>
        <DataTrigger Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}}" Value="0">
            <Setter Property="Background">
                <Setter.Value>
                    <VisualBrush Stretch="None">
                        <VisualBrush.Visual>
                            <TextBlock Text="We did't find any matching records for your search..." FontSize="16" FontWeight="SemiBold" Foreground="LightCoral"/>
                        </VisualBrush.Visual>
                    </VisualBrush>
                </Setter.Value>
            </Setter>
        </DataTrigger>
    </Style.Triggers>

    如果元素不是可视树的一部分,那么RelativeSource将永远不会工作。

    在这种情况下,你需要尝试一种不同的技术,由托马斯·莱夫斯基首创。

    他在自己的博客上的[wpf]下有一个解决方案,即当数据上下文未被继承时如何绑定到数据。而且它工作得非常出色!

    在他的博客不太可能被关闭的情况下,附录A包含了他的文章的镜像副本。

    请不要在这里评论,请直接在他的博客上评论。

    附录A:博客帖子镜像

    WPF中的DataContext属性非常方便,因为它由分配它的元素的所有子元素自动继承;因此,您不需要在要绑定的每个元素上重新设置它。但是,在某些情况下,DataContext是不可访问的:它发生在不属于可视化或逻辑树的元素上。在这些元素上绑定一个属性是非常困难的…

    让我们用一个简单的例子来说明:我们希望在数据报中显示产品列表。在网格中,我们希望能够根据ViewModel公开的ShowPrice属性的值显示或隐藏Price列。显而易见的方法是将列的可见性绑定到ShowPrice属性:

    1
    2
    3
    <DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding ShowPrice,
                    Converter={StaticResource visibilityConverter}}"/>

    不幸的是,更改ShowPrice的值没有任何效果,列总是可见的……为什么?如果我们在Visual Studio中查看输出窗口,我们会注意到以下行:

    System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=ShowPrice; DataItem=null; target element is ‘DataGridTextColumn’ (HashCode=32685253); target property is ‘Visibility’ (type ‘Visibility’)

    The message is rather cryptic, but the meaning is actually quite simple: WPF doesn’t know which FrameworkElement to use to get the DataContext, because the column doesn’t belong to the visual or logical tree of the DataGrid.

    我们可以尝试调整绑定以获得所需的结果,例如通过将RelativeSource设置为DataGrid本身:

    1
    2
    3
    4
    <DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding DataContext.ShowPrice,
                    Converter={StaticResource visibilityConverter},
                    RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>

    或者,我们可以添加绑定到ShowPrice的复选框,并尝试通过指定元素名称将列可见性绑定到IsChecked属性:

    1
    2
    3
    4
    <DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding IsChecked,
                    Converter={StaticResource visibilityConverter},
                    ElementName=chkShowPrice}"/>

    但这些解决方法似乎都不起作用,我们总是得到同样的结果…

    此时,似乎唯一可行的方法是更改代码隐藏中的列可见性,这是我们在使用MVVM模式时通常希望避免的……但我不会这么快放弃,至少不会在有其他选项需要考虑时放弃??

    我们的问题的解决方案实际上非常简单,并且利用了可自由类。此类的主要目的是定义具有可修改和只读状态的对象,但在我们的示例中,有趣的特性是,可冻结对象可以继承DataContext,即使它们不在可视树或逻辑树中。我不知道实现这种行为的确切机制,但我们将利用它使绑定工作…

    其思想是创建一个继承Freezable并声明数据依赖属性的类(我之所以称之为bindingProxy,是因为它很快就会变得明显):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable

        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }

    然后,我们可以在DataGrid的资源中声明此类的实例,并将数据属性绑定到当前的DataContext:

    1
    2
    3
    <DataGrid.Resources>
        <local:BindingProxy x:Key="proxy" Data="{Binding}" />
    </DataGrid.Resources>

    最后一步是指定此bindingproxy对象(可通过staticresource轻松访问)作为绑定的源:

    1
    2
    3
    4
    <DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
                    Visibility="{Binding Data.ShowPrice,
                    Converter={StaticResource visibilityConverter},
                    Source={StaticResource proxy}}"/>

    请注意,绑定路径的前缀是"data",因为该路径现在是相对于bindingproxy对象的。

    绑定现在工作正常,列根据ShowPrice属性正确显示或隐藏。