Como uso as ligações do WPF com o RelativeSource?

Respostas:

783

Se você deseja vincular a outra propriedade no objeto:

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

Se você deseja obter uma propriedade em um ancestral:

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

Se você deseja obter uma propriedade no pai modelo (para poder fazer ligações de duas vias em um ControlTemplate)

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

ou mais curto (isso funciona apenas para ligações OneWay):

{TemplateBinding Path=PathToProperty}
Abe Heidebrecht
fonte
15
Para este "{Binding Path = PathToProperty, RelativeSource = {RelativeSource AncestorType = {x: Digite typeOfAncestor}}}" ", parece que ele precisa ter" Mode = FindAncestor "antes de" AncestorType "
EdwardM
1
Para qual tecnologia? No WPF, isso é inferido quando você especifica um AncestorType.
Abe Heidebrecht 7/02
2
Eu concordo com @EdwardM. Quando omito FindAncestor, antes AncestorType, recebo o seguinte erro: "O RelativeSource não está no modo FindAncestor". (No VS2013, versão comunitária)
kmote 10/04
1
@kmote, isso funcionou para mim desde o .net 3.0 e, mais uma vez, verifiquei que funciona dessa maneira no kaxaml ... Mais uma vez, que tecnologia você está usando? O processador XAML é diferente para WPF / Silverlight / UWP, portanto, você pode ter resultados diferentes em diferentes tecnologias. Você também mencionou a Comunidade VS, talvez seja um aviso de IDE, mas funcione em tempo de execução?
Abe Heidebrecht
6
Só queria observar aqui que se você quiser se ligam a uma propriedade no DataContext do RelativeSource então você deve especificá-lo explicitamente: {Binding Path=DataContext.SomeProperty, RelativeSource=.... Isso foi algo inesperado para mim como novato quando estava tentando vincular ao DataContext de um pai dentro de um DataTemplate.
DrEsperanto
133
Binding RelativeSource={
    RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...

O atributo padrão de RelativeSourceé a Modepropriedade Um conjunto completo de valores válidos é fornecido aqui ( do MSDN ):

  • PreviousData Permite vincular o item de dados anterior (não o controle que contém o item de dados) na lista de itens de dados que estão sendo exibidos.

  • TemplatedParent Refere-se ao elemento ao qual o modelo (no qual o elemento ligado a dados existe) é aplicado. É semelhante à configuração de TemplateBindingExtension e é aplicável apenas se a Vinculação estiver dentro de um modelo.

  • Auto Refere-se ao elemento no qual você está definindo a ligação e permite vincular uma propriedade desse elemento a outra propriedade no mesmo elemento.

  • FindAncestor Refere-se ao ancestral na cadeia pai do elemento ligado a dados. Você pode usar isso para ligar-se a um ancestral de um tipo específico ou de suas subclasses. Este é o modo que você usa se deseja especificar AncestorType e / ou AncestorLevel.

Drew Noakes
fonte
128

Aqui está uma explicação mais visual no contexto de uma arquitetura MVVM:

insira a descrição da imagem aqui

Jeffrey Knight
fonte
19
Perdi alguma coisa? Como você pode considerar isso um gráfico simples e claro? 1: as caixas no significado da esquerda não estão realmente relacionadas às da direita (por que há um arquivo .cs dentro do ViewModel?) 2: para o que essas setas do DataContext apontam? 3: por que a propriedade Message não está no ViewModel1? e o mais importante: 5: Por que você precisa de uma Ligação de fonte relativa para acessar o DataContext da janela se o TextBlock já possui o mesmo DataContext? Estou claramente perdendo alguma coisa aqui, então ou sou bastante burro ou esse gráfico não é tão simples e claro como todos pensam! Por favor, me esclareça
Markus Hütter
2
@ MarkusHütter O diagrama mostra um grupo de Views aninhadas e ViewModels correspondentes. O DataContext de View1 é ViewModel1, mas deseja vincular a uma propriedade de BaseViewModel. Como BaseViewModel é o DataContext do BaseView (que é uma Janela), ele pode fazer isso localizando o primeiro contêiner pai que é uma Janela e levando seu DataContext.
Mcargille
6
@MatthewCargille Eu sei muito bem o que é suposto para dizer, que não era o meu ponto. Mas coloque-se na posição de alguém que não conhece bem o XAML e o MVVM e verá que isso não é simples e claro .
Markus Hütter
1
Eu tenho que concordar com o @ MarkusHütter, a propósito, a ligação à esquerda pode ser tão simples como isto: {Binding Message}(um pouco mais simples ...)
Florien
@ Florien Acho que não, pelo menos para o meu caso de uso. Eu tenho um DataTemplate que precisa se referir ao DataContext do MainWindow (classe my viewmodel) para obter uma lista de opções para um menu suspenso (carregado de um banco de dados). O DataTemplate está vinculado a um objeto de modelo que também é carregado no banco de dados, mas só tem acesso à opção selecionada. Eu tive que definir explicitamente Path=DataContext.Messagepara fazer a ligação funcionar. Isso faz sentido, já que você pode fazer ligações relativas à largura / altura / etc. de um controle.
DrEsperanto
47

Bechir Bejaoui expõe os casos de uso do RelativeSources no WPF em seu artigo aqui :

O RelativeSource é uma extensão de marcação que é usada em casos específicos de ligação quando tentamos vincular uma propriedade de um determinado objeto a outra propriedade do próprio objeto, quando tentamos vincular uma propriedade de um objeto a outra de seus pais relativos, ao vincular um valor de propriedade de dependência a um pedaço de XAML no caso de desenvolvimento de controle personalizado e, finalmente, no caso de usar um diferencial de uma série de dados vinculados. Todas essas situações são expressas como modos de fonte relativa. Vou expor todos esses casos, um por um.

  1. Auto do modo:

Imagine este caso, um retângulo que queremos que sua altura seja sempre igual à sua largura, um quadrado, digamos. Podemos fazer isso usando o nome do elemento

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

Mas neste caso acima, somos obrigados a indicar o nome do objeto de ligação, ou seja, o retângulo. Podemos alcançar o mesmo objetivo de maneira diferente usando o RelativeSource

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

Nesse caso, não somos obrigados a mencionar o nome do objeto de encadernação e a Largura será sempre igual à Altura sempre que a altura for alterada.

Se você desejar parametrizar a Largura como a metade da altura, poderá fazer isso adicionando um conversor à extensão de marcação Binding. Vamos imaginar outro caso agora:

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

O caso acima é usado para vincular uma determinada propriedade de um determinado elemento a um de seus pais diretos, pois esse elemento possui uma propriedade chamada Pai. Isso nos leva a outro modo de fonte relativo, que é o FindAncestor.

  1. Modo FindAncestor

Nesse caso, uma propriedade de um determinado elemento será vinculada a um de seus pais, Of Corse. A principal diferença com o caso acima é o fato de que depende de você determinar o tipo de ancestral e a classificação do ancestral na hierarquia para vincular a propriedade. A propósito, tente brincar com este pedaço de XAML

<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>

A situação acima é composta por dois elementos TextBlock incorporados a uma série de bordas e elementos de tela que representam seus pais hierárquicos. O segundo TextBlock exibirá o nome do pai fornecido no nível de origem relativo.

Portanto, tente alterar AncestorLevel = 2 para AncestorLevel = 1 e veja o que acontece. Em seguida, tente alterar o tipo do ancestral de AncestorType = Border para AncestorType = Canvas e veja o que está acontecendo.

O texto exibido será alterado de acordo com o tipo e o nível do ancestral. Então, o que acontece se o nível de ancestral não for adequado ao tipo de ancestral? Esta é uma boa pergunta, eu sei que você está prestes a perguntar. A resposta é que nenhuma exceção será lançada e nada será exibido no nível do TextBlock.

  1. TemplatedParent

Esse modo permite vincular uma determinada propriedade ControlTemplate a uma propriedade do controle ao qual o ControlTemplate é aplicado. Para entender bem a questão, aqui está um exemplo abaixo

<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>

Se eu quiser aplicar as propriedades de um determinado controle ao seu modelo de controle, posso usar o modo TemplatedParent. Também existe uma extensão semelhante a essa extensão de marcação, que é a TemplateBinding, que é uma espécie de mão curta da primeira, mas a TemplateBinding é avaliada em tempo de compilação, ao contrário do TemplatedParent, que é avaliado logo após o primeiro tempo de execução. Como você pode observar na figura abaixo, o plano de fundo e o conteúdo são aplicados de dentro do botão ao modelo de controle.

Cornel Marian
fonte
Exemplos muito bons para mim, usei o Find Ancestor para vincular a um comando no contexto de dados de um pai ListView. O pai tem mais 2 ListViewníveis abaixo dele. Isso me ajudou a impedir a transmissão de dados em cada vm subsequente de cada ListViewumDataTemplate
Caleb W.
34

No WPF, a RelativeSourceassociação expõe três propertiespara definir:

1. Modo: Este é um enumque pode ter quatro valores:

uma. PreviousData ( value=0): atribui o valor anterior dopropertyao vinculado

b. TemplatedParent ( value=1): É usado ao definirtemplatesqualquer controle e deseja vincular a um valor / Propriedade docontrol.

Por exemplo, defina ControlTemplate:

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

c. Self ( value=2): quando queremos ligar de umselfou de umpropertyself.

Por exemplo: Enviar verificado estado de checkboxcomo CommandParameterao definir o CommandonCheckBox

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

d. FindAncestor ( value=3): quando deseja ligar de um paicontrol emVisual Tree.

Por exemplo: Bind um checkboxem recordsque um grid, se header checkboxestiver marcada

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

2. AncestorType: quando mode é, FindAncestorentão, defina que tipo de ancestral

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

3. AncestorLevel: quando mode éFindAncestorqual o nível de ancestral (se houver dois mesmos tipos de paisvisual tree)

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

Acima estão todos os casos de uso para RelativeSource binding.

Aqui está um link de referência .

Kylo Ren
fonte
2
Incrível .. isso funcionou para mim: <DataGridCheckBoxColumn Header = "Paid" Width = "35" Binding = "{Ligação RelativeSource = {RelativeSource Mode = FindAncestor, AncestorType = {x: Type Window}}, Path = DataContext.SelectedBuyer.IsPaid , Mode = OneWay}"/> onde eu estava tentando vincular a propriedade selectedbuyer.IsPaid da janela pai
Michael K
21

Não se esqueça do TemplatedParent:

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

ou

{Binding RelativeSource={RelativeSource TemplatedParent}}
Bob King
fonte
16

Criei uma biblioteca para simplificar a sintaxe de ligação do WPF, facilitando o uso do RelativeSource. Aqui estão alguns exemplos. Antes:

{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}

Depois de:

{BindTo PathToProperty}
{BindTo Ancestor.typeOfAncestor.PathToProperty}
{BindTo Template.PathToProperty}
{BindTo #MyTextBox.Text}

Aqui está um exemplo de como a ligação do método é simplificada. Antes:

// 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}

Depois de:

// C# code
private void SaveObject() {
 // do something
}

// XAML
{BindTo SaveObject()}

Você pode encontrar a biblioteca aqui: http://www.simplygoodcode.com/2012/08/simpler-wpf-binding.html

Observe no exemplo 'ANTES' que eu uso para ligação de método que o código já foi otimizado usando a RelayCommandúltima verificação que não fiz parte do WPF. Sem isso, o exemplo 'ANTES' teria sido ainda mais longo.

Luis Perez
fonte
2
Esse tipo de exercício de segurar as mãos demonstra a fraqueza do XAML; maneira muito complicado.
precisa saber é o seguinte
16

Algumas partes úteis:

Veja como fazer isso principalmente no código:

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

Copiei isso amplamente da Binding Relative Source no código Behind .

Além disso, a página MSDN é muito boa no que diz respeito aos exemplos: Classe RelativeSource

Nathan Cooper
fonte
5
Minha vaga memória do WPF é que fazer ligações no código provavelmente não é normalmente a melhor coisa.
Nathan Cooper
12

Acabei de publicar outra solução para acessar o DataContext de um elemento pai no Silverlight que funciona para mim. Ele usa Binding ElementName.

Juve
fonte
10

Não li todas as respostas, mas só quero adicionar essas informações no caso de ligação de comando de origem relativa de um botão.

Quando você usa uma fonte relativa com Mode=FindAncestor, a ligação deve ser como:

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

Se você não adicionar o DataContext ao seu caminho, no tempo de execução, ele não poderá recuperar a propriedade.

Kevin VDF
fonte
9

Este é um exemplo do uso desse padrão que funcionou para mim em datagrids vazios.

<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>
Edd
fonte
6

Se um elemento não fizer parte da árvore visual, o RelativeSource nunca funcionará.

Nesse caso, você precisa tentar uma técnica diferente, pioneira em Thomas Levesque.

Ele tem a solução em seu blog em [WPF] Como vincular aos dados quando o DataContext não é herdado . E funciona absolutamente brilhante!

No caso improvável de seu blog estar inativo, o Apêndice A contém uma cópia espelhada de seu artigo .

Por favor, não comente aqui, por favor, comente diretamente em seu blog .

Apêndice A: Espelho da postagem do blog

A propriedade DataContext no WPF é extremamente útil, porque é automaticamente herdada por todos os filhos do elemento em que você o atribui; portanto, você não precisa defini-lo novamente em cada elemento que deseja vincular. No entanto, em alguns casos, o DataContext não está acessível: isso acontece com elementos que não fazem parte da árvore visual ou lógica. Pode ser muito difícil vincular uma propriedade nesses elementos…

Vamos ilustrar com um exemplo simples: queremos exibir uma lista de produtos em um DataGrid. Na grade, queremos poder mostrar ou ocultar a coluna Preço, com base no valor de uma propriedade ShowPrice exposta pelo ViewModel. A abordagem óbvia é vincular a visibilidade da coluna à propriedade ShowPrice:

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

Infelizmente, alterar o valor de ShowPrice não tem efeito e a coluna está sempre visível ... por quê? Se olharmos para a janela Saída no Visual Studio, notamos a seguinte linha:

System.Windows.Data Erro: 2: Não é possível localizar o FrameworkElement ou o FrameworkContentElement que regem o elemento de destino. BindingExpression: Path = ShowPrice; DataItem = nulo; o elemento de destino é 'DataGridTextColumn' (HashCode = 32685253); a propriedade de destino é 'Visibilidade' (digite 'Visibilidade')

A mensagem é um tanto enigmática, mas o significado é bastante simples: o WPF não sabe qual FrameworkElement usar para obter o DataContext, porque a coluna não pertence à árvore visual ou lógica do DataGrid.

Podemos tentar ajustar a ligação para obter o resultado desejado, por exemplo, configurando o RelativeSource para o próprio DataGrid:

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

Ou podemos adicionar uma CheckBox vinculada a ShowPrice e tentar vincular a visibilidade da coluna à propriedade IsChecked especificando o nome do elemento:

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

Mas nenhuma dessas soluções alternativas parece funcionar, sempre obtemos o mesmo resultado…

Neste ponto, parece que a única abordagem viável seria alterar a visibilidade da coluna no code-behind, o que geralmente preferimos evitar ao usar o padrão MVVM ... Mas não vou desistir tão cedo, pelo menos não enquanto existem outras opções a considerar 😉

A solução para o nosso problema é realmente bastante simples e tira proveito da classe Freezable. O objetivo principal desta classe é definir objetos que tenham um estado modificável e somente leitura, mas o recurso interessante em nosso caso é que objetos Freezable podem herdar o DataContext mesmo quando não estão na árvore visual ou lógica. Não sei o mecanismo exato que permite esse comportamento, mas vamos tirar proveito dele para fazer nossa ligação funcionar ...

A idéia é criar uma classe (chamei-o de BindingProxy por razões que devem se tornar óbvias muito em breve) que herda o Freezable e declara uma propriedade de dependência de dados:

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));
}

Em seguida, podemos declarar uma instância dessa classe nos recursos do DataGrid e vincular a propriedade Data ao DataContext atual:

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

A última etapa é especificar esse objeto BindingProxy (facilmente acessível com StaticResource) como a Origem da ligação:

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

Observe que o caminho de ligação foi prefixado com "Dados", pois o caminho agora é relativo ao objeto BindingProxy.

A ligação agora funciona corretamente e a coluna é mostrada ou ocultada corretamente com base na propriedade ShowPrice.

Contango
fonte