Як я можу використовувати RelativeSource
WPF прив'язки та які різні випадки використання?
Як я можу використовувати RelativeSource
WPF прив'язки та які різні випадки використання?
Відповіді:
Якщо ви хочете прив’язати до іншого властивості об’єкта:
{Binding Path=PathToProperty, RelativeSource={RelativeSource Self}}
Якщо ви хочете отримати майно на предка:
{Binding Path=PathToProperty,
RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
Якщо ви хочете отримати властивість на батьківському шаблоні (тому ви можете зробити двосторонні прив'язки в ControlTemplate)
{Binding Path=PathToProperty, RelativeSource={RelativeSource TemplatedParent}}
або, коротше (це працює лише для прив’язок OneWay):
{TemplateBinding Path=PathToProperty}
AncestorType
.
FindAncestor
раніше AncestorType
, я отримую таку помилку: "RelativeSource не знаходиться в режимі FindAncestor". (У VS2013, версія для спільноти)
{Binding Path=DataContext.SomeProperty, RelativeSource=...
. Це було дещо несподівано для мене як для новачків, коли я намагався прив’язатись до батьківського DataContext у DataTemplate.
Binding RelativeSource={
RelativeSource Mode=FindAncestor, AncestorType={x:Type ItemType}
}
...
Атрибут за замовчуванням RelativeSource
- це Mode
властивість. Тут подано повний набір допустимих значень ( від MSDN ):
PreviousData Дозволяє прив'язувати попередній елемент даних (не той елемент управління, який містить елемент даних) у списку елементів, що відображаються.
TemplatedParent Посилається на елемент, до якого застосовується шаблон (у якому існує елемент, пов'язаний з даними). Це аналогічно встановленню TemplateBindingExtension і застосовується лише в тому випадку, якщо Прив'язка знаходиться в шаблоні.
Self Посилається на елемент, для якого ви встановлюєте прив'язку, і дозволяє вам прив'язувати одну властивість цього елемента до іншої властивості того ж елемента.
FindAncestor Посилається на пращура в батьківському ланцюжку елемента, пов'язаного з даними. Ви можете використовувати це для прив'язки до предка певного типу або його підкласів. Це режим, який ви використовуєте, якщо ви хочете вказати AncestorType та / або AncestorLevel.
Ось більш наочне пояснення в контексті архітектури MVVM:
{Binding Message}
(трохи простіше ...)
Path=DataContext.Message
щоб отримати прив’язку до роботи. Це має сенс, враховуючи, що ви можете робити відносні прив’язки до ширини / висоти / тощо. контролю.
Бешир Bejaoui викриває випадки використання RelativeSources в WPF в своїй статті тут :
RelativeSource - це розширення розмітки, яке використовується в особливих обов'язкових випадках, коли ми намагаємось прив’язати властивість даного об'єкта до іншої властивості самого об'єкта, коли ми намагаємося прив’язати властивість об'єкта до іншого його родича, при прив'язуванні значення властивості залежності до фрагмента XAML у разі розробки користувальницького керування та, нарешті, у випадку використання диференціала серії пов'язаних даних. Усі ці ситуації виражаються як режими відносного джерела. Я викладу всі ці випадки по одному.
- Режим власного режиму:
Уявіть собі цей випадок, прямокутник, який ми хочемо, щоб його висота завжди дорівнювала його ширині, скажімо, квадрат. Це можна зробити за допомогою імені елемента
<Rectangle Fill="Red" Name="rectangle" Height="100" Stroke="Black" Canvas.Top="100" Canvas.Left="100" Width="{Binding ElementName=rectangle, Path=Height}"/>
Але в цьому вище випадку ми зобов'язані вказати назву об'єкта, що зв’язує, а саме прямокутник. Ми можемо досягти тієї самої мети по-різному, використовуючи RelativeSource
<Rectangle Fill="Red" Height="100" Stroke="Black" Width="{Binding RelativeSource={RelativeSource Self}, Path=Height}"/>
У такому випадку ми не зобов’язані згадувати назву об'єкта, що зв'язує, і Ширина завжди буде дорівнює Висоті щоразу, коли висота буде змінена.
Якщо ви хочете, щоб параметр Width був половиною висоти, ви можете зробити це, додавши перетворювач до розширення розмітки Binding. Давайте уявимо інший випадок:
<TextBlock Width="{Binding RelativeSource={RelativeSource Self}, Path=Parent.ActualWidth}"/>
Вищеописаний випадок використовується для прив’язки даного властивості даного елемента до одного з його прямих батьків, оскільки цей елемент містить властивість, яка називається батьківською. Це призводить нас до іншого режиму відносного джерела, який є FindAncestor.
- Режим FindAncestor
У цьому випадку властивість певного елемента буде прив’язана до одного з батьків, Корса. Основна відмінність вищезгаданого випадку полягає в тому, що визначити тип предка та звання предка в ієрархії залежати від власності. До речі спробуйте пограти з цим твором 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>
Вищеописана ситуація складається з двох елементів TextBlock, які вбудовані в ряд меж і елементів полотна, які представляють їх ієрархічних батьків. Другий TextBlock відобразить ім'я даного батька на відносному рівні джерела.
Тому спробуйте змінити AncestorLevel = 2 на AncestorLevel = 1 і подивіться, що станеться. Потім спробуйте змінити тип предка з AncestorType = Межа на AncestorType = Полотно і побачити, що відбувається.
Виведений текст буде змінюватися залежно від типу та рівня предка. Тоді що станеться, якщо рівень предка не підходить до типу предка? Це гарне запитання, я знаю, що ви ось-ось задасте його. Відповідь немає, винятки будуть викинуті, а на рівні TextBlock відобразяться повідомлення.
- TemplatedParent
Цей режим дозволяє прив’язати задане властивість ControlTemplate до властивості елемента управління, до якого застосовується ControlTemplate. Щоб добре зрозуміти проблему, тут наведено приклад нижче
<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>
Якщо я хочу застосувати властивості даного елемента управління до його шаблону управління, я можу використовувати режим TemplatedParent. Існує також подібне до цього розширення розмітки, яке є TemplateBinding, яке є своєрідною короткою рукою першого, але TemplateBinding оцінюється під час компіляції на відміну від TemplatedParent, який оцінюється відразу після першого часу запуску. Як ви можете зауважити на малюнку нижче, фон і вміст застосовуються за допомогою кнопки до шаблону керування.
ListView
. У батьків є ще 2 ListView
рівні під ним. Це допомогло мені запобігти передачі даних у кожен наступний vm кожного ListView
зDataTemplate
У WPF RelativeSource
прив'язка виставляє три properties
для встановлення:
1. Режим: Це значення, enum
яке може мати чотири значення:
а. PreviousData (
value=0
): присвоює попереднє значенняproperty
зв'язаномуб. TemplatedParent (
value=1
): Це використовується при визначенніtemplates
будь-якого елемента керування і хочеться прив’язати до значення / Властивостіcontrol
.Наприклад, визначте
ControlTemplate
:
<ControlTemplate>
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</ControlTemplate>
c. Я (
value=2
): Коли ми хочемо зв’язати себеself
абоproperty
з себе.Наприклад: Відправити перевіряється стан в
checkbox
якості підCommandParameter
час установкиCommand
наCheckBox
<CheckBox ...... CommandParameter="{Binding RelativeSource={RelativeSource Self},Path=IsChecked}" />
г. FindAncestor (
value=3
): коли потрібно зв’язати з батькомcontrol
уVisual Tree
.Наприклад: Прив’яжіть
checkbox
в,records
якщо agrid
, якщоheader
checkbox
встановлено прапорець
<CheckBox IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}, Path=DataContext.IsHeaderChecked, Mode=TwoWay}" />
2.FindAncestor
Тип предків: коли в режимі, то визначте, який тип предка
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid}}
3. AncestorLevel: коли режим - цеFindAncestor
який рівень предка (якщо в ньому є два однотипних батьківvisual tree
)
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type iDP:XamDataGrid, AncestorLevel=1}}
Вище наведено всі випадки використання для
RelativeSource binding
.
Варто зазначити, що для тих, хто спотикається над цим мисленням Silverlight:
Silverlight пропонує лише зменшений підмножина з цих команд
Я створив бібліотеку, щоб спростити синтаксис прив’язки WPF, включаючи полегшення використання RelativeSource. Ось кілька прикладів. Перед:
{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}
Після:
{BindTo PathToProperty}
{BindTo Ancestor.typeOfAncestor.PathToProperty}
{BindTo Template.PathToProperty}
{BindTo #MyTextBox.Text}
Ось приклад того, як спрощується прив'язка методу. Перед:
// 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}
Після:
// C# code
private void SaveObject() {
// do something
}
// XAML
{BindTo SaveObject()}
Бібліотеку можна знайти тут: http://www.simplygoodcode.com/2012/08/simpler-wpf-binding.html
Зауважте у прикладі "ДО ПЕРЕД", який я використовую для прив'язки методу, цей код уже оптимізовано, використовуючи RelayCommand
останній, який я перевірив, - це не рідна частина WPF. Без цього приклад "ДО ПЕРЕД" був би ще довшим.
Деякі корисні шматочки:
Ось як це зробити в основному в коді:
Binding b = new Binding();
b.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, this.GetType(), 1);
b.Path = new PropertyPath("MyElementThatNeedsBinding");
MyLabel.SetBinding(ContentProperty, b);
Я багато в чому скопіював це з прив’язки відносного джерела в коді позаду .
Крім того, сторінка MSDN досить хороша, що стосується прикладів: Клас відносних ресурсів
Я щойно опублікував ще одне рішення для доступу до DataContext батьківського елемента в Silverlight, який працює для мене. Він використовує Binding ElementName
.
Я не читав кожної відповіді, але просто хочу додати цю інформацію у випадку відносної команди, пов'язаної з кнопкою.
Коли ви використовуєте відносне джерело з Mode=FindAncestor
, прив'язка повинна бути такою:
Command="{Binding Path=DataContext.CommandProperty, RelativeSource={...}}"
Якщо ви не додаєте DataContext у свій шлях, під час виконання він не може отримати властивість.
Це приклад використання цього шаблону, який працював для мене на порожніх мережах даних.
<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] Як прив’язати до даних, коли DataContext не успадковується . І це працює абсолютно геніально!
У тому випадку, коли його блог не працює, додаток А містить дзеркальну копію своєї статті .
Будь ласка, не коментуйте тут, будь ласка, коментуйте його безпосередньо в блозі .
Властивість DataContext в WPF надзвичайно зручна, оскільки вона автоматично успадковується всіма дітьми елемента, де ви її призначите; тому вам не потрібно встановлювати його знову на кожному елементі, який потрібно прив’язати. Однак у деяких випадках DataContext недоступний: це відбувається для елементів, які не входять до візуального чи логічного дерева. Тоді може бути дуже важко пов'язати властивість на цих елементах ...
Проілюструємо простий приклад: ми хочемо відобразити список продуктів у DataGrid. У сітці ми хочемо мати змогу показати або приховати стовпець Ціна, виходячи зі значення властивості ShowPrice, викритого ViewModel. Очевидним підходом є прив’язання стовпця «Видимість» до властивості ShowPrice:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding ShowPrice,
Converter={StaticResource visibilityConverter}}"/>
На жаль, зміна значення ShowPrice не впливає, і стовпець завжди видно ... чому? Якщо ми подивимось на вікно Вихід у Visual Studio, то помітимо наступний рядок:
Помилка System.Windows.Data: 2: Неможливо знайти керуючі FrameworkElement або FrameworkContentElement для цільового елемента. BindingExpression: Шлях = ShowPrice; DataItem = null; цільовим елементом є «DataGridTextColumn» (HashCode = 32685253); цільовим властивістю є "Видимість" (тип "Видимість")
Повідомлення є доволі виразним, але сенс насправді досить простий: WPF не знає, який FrameworkElement використовувати для отримання DataContext, оскільки стовпець не належить до візуального чи логічного дерева DataGrid.
Ми можемо спробувати налаштувати прив'язку, щоб отримати бажаний результат, наприклад, встановивши RelativeSource для самої DataGrid:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding DataContext.ShowPrice,
Converter={StaticResource visibilityConverter},
RelativeSource={RelativeSource FindAncestor, AncestorType=DataGrid}}"/>
Або ми можемо додати CheckBox, прив’язаний до ShowPrice, і спробувати прив’язати видимість стовпця до властивості IsChecked, вказавши ім'я елемента:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding IsChecked,
Converter={StaticResource visibilityConverter},
ElementName=chkShowPrice}"/>
Але жоден з цих способів вирішення не здається, ми завжди отримуємо однаковий результат ...
На даний момент здається, що єдиним життєздатним підходом було б змінити видимість стовпців у коді позаду, чого ми зазвичай вважаємо за краще уникати при використанні шаблону MVVM… Але я не збираюсь здаватись так скоро, принаймні ні хоча є й інші варіанти, які слід розглянути 😉
Рішення нашої проблеми насправді досить просте, і використовує переваги класу Freezable. Основна мета цього класу - визначити об'єкти, які мають модифікований стан та стан лише для читання, але цікава особливість у нашому випадку полягає в тому, що об'єкти Freezable можуть успадковувати DataContext навіть тоді, коли вони не перебувають у візуальному чи логічному дереві. Я не знаю точного механізму, який дозволяє цю поведінку, але ми скористаємося цим, щоб зробити нашу обов'язкову роботу ...
Ідея полягає у створенні класу (я назвав його BindingProxy з причин, який повинен стати очевидним дуже скоро), який успадковує Freezable і оголошує властивість залежності даних:
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 і прив’язати властивість Data до поточного DataContext:
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
Останній крок - вказати цей об'єкт BindingProxy (легко доступний за допомогою StaticResource) як Джерело для прив'язки:
<DataGridTextColumn Header="Price" Binding="{Binding Price}" IsReadOnly="False"
Visibility="{Binding Data.ShowPrice,
Converter={StaticResource visibilityConverter},
Source={StaticResource proxy}}"/>
Зауважте, що шлях прив'язки був префіксом "Дані", оскільки шлях тепер відносно об'єкта BindingProxy.
Прив’язка тепер працює правильно, і стовпець правильно відображається або приховується на основі властивості ShowPrice.