Jak korzystać RelativeSource
z powiązań WPF i jakie są różne przypadki użycia?
Jak korzystać RelativeSource
z powiązań WPF i jakie są różne przypadki użycia?
Odpowiedzi:
Jeśli chcesz powiązać z inną właściwością obiektu:
{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
Jeśli chcesz uzyskać właściwość od przodka:
{Binding Path=PathToProperty,
RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
Jeśli chcesz uzyskać właściwość nadrzędnego szablonu (aby można było tworzyć wiązania 2-kierunkowe w ControlTemplate)
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
lub krótszy (działa to tylko w przypadku powiązań OneWay):
{TemplateBinding Path=PathToProperty}
AncestorType
.
FindAncestor
, przed AncestorType
, pojawia się następujący błąd: „RelativeSource nie jest w trybie FindAncestor”. (W VS2013, wersja wspólnotowa)
{Binding Path=DataContext.SomeProperty, RelativeSource=...
. Dla mnie jako początkującego było to nieco nieoczekiwane, gdy próbowałem powiązać z obiektem DataContext rodzica w DataTemplate.
Binding RelativeSource={
RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...
Domyślnym atrybutem RelativeSource
jest Mode
właściwość. Pełny zestaw prawidłowych wartości znajduje się tutaj ( z MSDN ):
PreviousData Umożliwia powiązanie poprzedniego elementu danych (nie formantu zawierającego element danych) z listy wyświetlanych elementów danych.
TemplatedParent Odnosi się do elementu, do którego stosuje się szablon (w którym istnieje element związany z danymi). Jest to podobne do ustawiania TemplateBindingExtension i ma zastosowanie tylko wtedy, gdy Powiązanie znajduje się w szablonie.
Self Odnosi się do elementu, na którym ustawia się wiązanie, i pozwala powiązać jedną właściwość tego elementu z inną właściwością tego samego elementu.
FindAncestor Odnosi się do przodka w łańcuchu macierzystym elementu związanego z danymi. Możesz użyć tego do powiązania z przodkiem określonego typu lub jego podklas. Jest to tryb, którego używasz, jeśli chcesz określić AncestorType i / lub AncestorLevel.
Oto bardziej wizualne wyjaśnienie w kontekście architektury MVVM:
{Binding Message}
(nieco prostsze ...)
Path=DataContext.Message
aby wiązanie zadziałało. Ma to sens, biorąc pod uwagę, że można tworzyć powiązania względne do szerokości / wysokości / itp. kontroli.
Bechir Bejaoui ujawnia przypadki użycia RelativeSources w WPF w swoim artykule tutaj :
RelativeSource to rozszerzenie znaczników, które jest używane w szczególnych przypadkach wiązania, gdy próbujemy powiązać właściwość danego obiektu z inną właściwością samego obiektu, gdy próbujemy powiązać właściwość obiektu z innym jego względnym rodzicem, podczas wiązania wartości właściwości zależności z kawałkiem XAML w przypadku niestandardowego opracowania sterowania i wreszcie w przypadku użycia różnicy serii powiązanych danych. Wszystkie te sytuacje są wyrażone jako względne tryby źródła. Wszystkie te przypadki ujawnię jeden po drugim.
- Tryb własny:
Wyobraźmy sobie ten przypadek: prostokąt, który chcemy, aby jego wysokość była zawsze równa jego szerokości, powiedzmy kwadrat. Możemy to zrobić za pomocą nazwy elementu
<Rectangle Fill="Red" Name="rectangle" Height="100" Stroke="Black" Canvas.Top="100" Canvas.Left="100" Width="{Binding ElementName=rectangle, Path=Height}"/>
Ale w powyższym przypadku jesteśmy zobowiązani podać nazwę wiążącego obiektu, a mianowicie prostokąta. Możemy osiągnąć ten sam cel inaczej, korzystając z RelativeSource
<Rectangle Fill="Red" Height="100" Stroke="Black" Width="{Binding RelativeSource={RelativeSource Self}, Path=Height}"/>
W takim przypadku nie jesteśmy zobowiązani do podania nazwy obiektu wiążącego, a szerokość będzie zawsze równa wysokości przy każdej zmianie wysokości.
Jeśli chcesz ustawić szerokość jako połowę wysokości, możesz to zrobić, dodając konwerter do rozszerzenia znaczników Binding. Wyobraźmy sobie teraz inny przypadek:
<TextBlock Width="{Binding RelativeSource={RelativeSource Self}, Path=Parent.ActualWidth}"/>
Powyższy przypadek służy do powiązania danej właściwości danego elementu z jedną z jego bezpośrednich nadrzędnych, ponieważ element ten zawiera właściwość o nazwie Nadrzędny. To prowadzi nas do innego względnego trybu źródła, którym jest FindAncestor.
- Mode FindAncestor
W takim przypadku właściwość danego elementu zostanie powiązana z jednym z jego rodziców, Of Corse. Główną różnicą w powyższym przypadku jest fakt, że to od Ciebie zależy określenie rodzaju przodka i rangi przodka w hierarchii, aby powiązać właściwość. Nawiasem mówiąc, spróbuj zagrać z tym kawałkiem 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>
Powyższa sytuacja dotyczy dwóch elementów TextBlock, które są osadzone w szeregu ramek i elementów canvas reprezentujących ich hierarchicznych rodziców. Drugi TextBlock wyświetli nazwę danego rodzica na względnym poziomie źródła.
Spróbuj więc zmienić AncestorLevel = 2 na AncestorLevel = 1 i zobacz, co się stanie. Następnie spróbuj zmienić typ przodka z AncestorType = Border na AncestorType = Canvas i zobacz, co się stanie.
Wyświetlany tekst zmieni się w zależności od typu i poziomu Przodka. Co się wtedy stanie, jeśli poziom przodka nie jest odpowiedni dla typu przodka? To dobre pytanie, wiem, że zaraz o to zapytasz. Odpowiedź jest taka, że nie zostaną zgłoszone wyjątki, a na poziomie TextBlock zostaną wyświetlone informacje.
- TemplatedParent
Ten tryb umożliwia powiązanie danej właściwości ControlTemplate z właściwością formantu, do którego zastosowano ControlTemplate. Aby dobrze zrozumieć problem, oto przykład poniżej
<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>
Jeśli chcę zastosować właściwości danej kontrolki do jej szablonu kontrolnego, mogę użyć trybu TemplatedParent. Istnieje również podobne do tego rozszerzenia znaczników, które jest szablonowym wiązaniem, które jest rodzajem krótkiego układu pierwszego, ale wiązanie szablonowe jest oceniane w czasie kompilacji, w przeciwieństwie do szablonu TemplatedParent, który jest oceniany tuż po pierwszym uruchomieniu. Jak można zauważyć na poniższym rysunku, tło i treść są stosowane z przycisku do szablonu kontrolnego.
ListView
. Rodzic ma jeszcze 2 ListView
poziomy poniżej. To pomogło mi dopuścić do przeniesienia danych do każdego kolejnego VM każdego ListView
„sDataTemplate
W RelativeSource
wiązaniu WPF ujawnia trzyproperties
do ustawienia:
1. Tryb: To enum
może mieć cztery wartości:
za. PreviousData (
value=0
): Przypisuje poprzednią wartość parametruproperty
powiązanejb. TemplatedParent (
value=1
): Jest używany podczas definiowaniatemplates
dowolnego formantu i chce powiązać wartość / właściwość obiektucontrol
.Na przykład zdefiniuj
ControlTemplate
:
<ControlTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</ControlTemplate>
do. Self (
value=2
): Kiedy chcemy powiązać zself
lubproperty
self.Na przykład: Wysłać sprawdził stan od
checkbox
jakCommandParameter
podczas ustawianiaCommand
naCheckBox
<CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />
re. FindAncestor (
value=3
): Kiedy chcesz powiązać od rodzicacontrol
wVisual Tree
.Na przykład: wiążą
checkbox
sięrecords
czygrid
, jeśliheader
checkbox
jest zaznaczone
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />
2. AncestorType: kiedy tryb FindAncestor
określa następnie typ przodka
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}
3. AncestorLevel: kiedy tryb jestFindAncestor
wtedy na jakim poziomie przodka (jeśli są dwa takie same typy rodzicavisual tree
)
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}
Powyżej są wszystkie przypadki użycia dla
RelativeSource binding
.
Warto zauważyć, że dla tych, którzy natkną się na myślenie o Silverlight:
Silverlight oferuje tylko ograniczony podzbiór tych poleceń
Utworzyłem bibliotekę, aby uprościć składnię powiązań WPF, w tym ułatwić korzystanie z RelativeSource. Oto kilka przykładów. Przed:
{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}
Po:
{BindTo PathToProperty}
{BindTo Ancestor.typeOfAncestor.PathToProperty}
{BindTo Template.PathToProperty}
{BindTo #MyTextBox.Text}
Oto przykład uproszczenia wiązania metod. Przed:
// 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}
Po:
// C# code
private void SaveObject() {
// do something
}
// XAML
{BindTo SaveObject()}
Bibliotekę znajdziesz tutaj: http://www.simplygoodcode.com/2012/08/simpler-wpf-binding.html
Zauważ, że w przykładzie „ZANIM”, którego używam do wiązania metody, kod został już zoptymalizowany przy użyciu tego, RelayCommand
który ostatnio sprawdziłem, nie jest natywną częścią WPF. Bez tego przykład „PRZED” byłby jeszcze dłuższy.
Kilka przydatnych fragmentów:
Oto jak to zrobić, głównie w kodzie:
Binding b = new Binding();
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, this.GetType(), 1);
b.Path = new PropertyPath("MyElementThatNeedsBinding");
MyLabel.SetBinding(ContentProperty, b);
W dużej mierze skopiowałem to z Binding Relative Source w kodzie Behind .
Ponadto strona MSDN jest całkiem dobra pod względem przykładów: Klasa RelativeSource
Właśnie opublikowałem inne rozwiązanie dostępu do DataContext elementu nadrzędnego w Silverlight, które działa dla mnie. Używa Binding ElementName
.
Nie przeczytałem każdej odpowiedzi, ale chcę tylko dodać tę informację w przypadku względnego powiązania przycisku polecenia źródłowego.
Gdy używasz źródła względnego z Mode=FindAncestor
, powiązanie musi wyglądać następująco:
Command="{Binding Path=DataContext.CommandProperty, RelativeSource={...}}"
Jeśli nie dodasz DataContext do swojej ścieżki, w czasie wykonywania nie będzie można pobrać właściwości.
To jest przykład użycia tego wzorca, który działał dla mnie na pustych siatkach danych.
<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>
Jeśli element nie jest częścią drzewa wizualnego, RelativeSource nigdy nie będzie działać.
W takim przypadku musisz wypróbować inną technikę, której pionierem jest Thomas Levesque.
Ma rozwiązanie na swoim blogu pod [WPF] Jak powiązać z danymi, gdy DataContext nie jest dziedziczony . I działa absolutnie genialnie!
W mało prawdopodobnym przypadku, gdy jego blog jest wyłączony, załącznik A zawiera kopię lustrzaną jego artykułu .
Nie komentuj tutaj, skomentuj bezpośrednio na jego blogu .
Właściwość DataContext w WPF jest niezwykle przydatna, ponieważ jest automatycznie dziedziczona przez wszystkie elementy podrzędne elementu, do którego ją przypisujesz; dlatego nie musisz ustawiać go ponownie dla każdego elementu, który chcesz powiązać. Jednak w niektórych przypadkach DataContext nie jest dostępny: dzieje się tak w przypadku elementów, które nie są częścią drzewa wizualnego lub logicznego. Powiązanie właściwości z tymi elementami może być bardzo trudne…
Zilustrujmy prostym przykładem: chcemy wyświetlić listę produktów w DataGrid. W siatce chcemy mieć możliwość pokazywania lub ukrywania kolumny Cena na podstawie wartości właściwości ShowPrice ujawnionej przez ViewModel. Oczywistym podejściem jest powiązanie Widoczności kolumny z właściwością ShowPrice:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding ShowPrice,
Converter={StaticResource visibilityConverter}}"/>
Niestety zmiana wartości ShowPrice nie ma wpływu, a kolumna jest zawsze widoczna… dlaczego? Jeśli spojrzymy na okno Output w Visual Studio, zauważymy następujący wiersz:
System.Windows.Data Błąd: 2: Nie można znaleźć rządzących FrameworkElement lub FrameworkContentElement dla elementu docelowego. BindingExpression: Path = ShowPrice; DataItem = null; elementem docelowym jest „DataGridTextColumn” (HashCode = 32685253); właściwość docelowa to „Widoczność” (wpisz „Widoczność”)
Wiadomość jest raczej tajemnicza, ale jej znaczenie jest dość proste: WPF nie wie, którego FrameworkElement użyć do uzyskania DataContext, ponieważ kolumna nie należy do wizualnego ani logicznego drzewa DataGrid.
Możemy spróbować poprawić powiązanie, aby uzyskać pożądany wynik, na przykład ustawiając RelativeSource na samą siatkę danych:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding DataContext.ShowPrice,
Converter={StaticResource visibilityConverter},
RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>
Lub możemy dodać CheckBox związany z ShowPrice i spróbować powiązać widoczność kolumny z właściwością IsChecked, podając nazwę elementu:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding IsChecked,
Converter={StaticResource visibilityConverter},
ElementName=chkShowPrice}"/>
Ale żadne z tych obejść nie wydaje się działać, zawsze uzyskujemy ten sam wynik…
W tym momencie wydaje się, że jedynym realnym podejściem byłaby zmiana widoczności kolumny z tyłu kodu, czego zwykle wolimy unikać, gdy używamy wzorca MVVM… Ale nie zamierzam się tak szybko poddawać, a przynajmniej nie podczas gdy istnieją inne opcje do rozważenia 😉
Rozwiązanie naszego problemu jest w rzeczywistości dość proste i wykorzystuje klasę Freezable. Głównym celem tej klasy jest zdefiniowanie obiektów, które mają stan modyfikowalny i tylko do odczytu, ale interesującą cechą w naszym przypadku jest to, że obiekty Freezable mogą dziedziczyć DataContext, nawet jeśli nie znajdują się w drzewie wizualnym lub logicznym. Nie znam dokładnego mechanizmu, który umożliwia takie zachowanie, ale wykorzystamy go, aby nasze wiążące działanie…
Chodzi o to, aby utworzyć klasę (nazwałem ją BindingProxy z powodów, które powinny wkrótce stać się oczywiste), która dziedziczy Freezable i deklaruje właściwość zależności danych:
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));
}
Następnie możemy zadeklarować wystąpienie tej klasy w zasobach DataGrid i powiązać właściwość Data z bieżącym DataContext:
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
Ostatnim krokiem jest określenie tego obiektu BindingProxy (łatwo dostępnego w StaticResource) jako źródła powiązania:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding Data.ShowPrice,
Converter={StaticResource visibilityConverter},
Source={StaticResource proxy}}"/>
Zauważ, że ścieżka wiązania została poprzedzona przedrostkiem „Data”, ponieważ ścieżka jest teraz względna do obiektu BindingProxy.
Powiązanie działa teraz poprawnie, a kolumna jest poprawnie wyświetlana lub ukryta na podstawie właściwości ShowPrice.