Binding To a Parent Element in UWP and WinUI 3
Posted: January 20, 2022 Filed under: MVVM, UWP, WinUI, XAML | Tags: MVVM, UWP, Windows App SDK, WinUI, XAML 2 CommentsIn a WPF application, it’s easy to bind to a property of a parent element in the visual tree by setting the RelativeSource
property of the binding to a RelativeSource object that specifies the type of ancestor to bind to.
For example, the following XAML markup binds the Text
property of the TextBlock
to the Tag
property of the parent window:
<Window ... Tag="aWindow"> <StackPanel> <TextBlock Text="{Binding Tag, RelativeSource={RelativeSource AncestorType=Window}}" /> ...
The above XAML attribute syntax is a shorthand for using the following object element syntax:
<TextBlock> <TextBlock.Text> <Binding Path="Tag"> <Binding.RelativeSource> <RelativeSource Mode="FindAncestor" AncestorType="Window" /> </Binding.RelativeSource> </Binding> </TextBlock.Text> </TextBlock>
When you migrate your application to UWP or WinUI 3, there is indeed a RelativeSource
property and object available but it doesn’t support the FindAncestor mode.
In some cases, you could set the ElementName
property of the binding to the name of the ancestor element to bind to instead of using a RelativeSource
:
<TextBlock Text="{Binding Tag, ElementName=aWindow}" />
In other cases, namely when the target and source elements reside in different XAML namescopes or when the source element has been created dynamically, this doesn’t work.
Consider the following sample XAML markup where the intention is to bind the Text
property of a TextBlock
in the ItemTemplate
of an ItemsRepeater
to the Tag
property of the latter. This binding will fail:
<ItemsRepeater x:Name="ir" Tag="aTag"> <ItemsRepeater.ItemTemplate> <DataTemplate> <!-- NOTE; This binding is invalid: --> <TextBlock Text="{Binding Tag, ElementName=ir}" /> </DataTemplate> </ItemsRepeater.ItemTemplate> </ItemsRepeater>
A workaround to be able to bind to a property of an ancestor in the visual tree is to create an attached property that sets the DataContext
of the target element (the TextBlock
in this case) to a parent element of a specific type. Here is a sample implementation of such a property:
public static class AncestorSource { public static readonly DependencyProperty AncestorTypeProperty = DependencyProperty.RegisterAttached( "AncestorType", typeof(Type), typeof(AncestorSource), new PropertyMetadata(default(Type), OnAncestorTypeChanged) ); public static void SetAncestorType(FrameworkElement element, Type value) => element.SetValue(AncestorTypeProperty, value); public static Type GetAncestorType(FrameworkElement element) => (Type)element.GetValue(AncestorTypeProperty); private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { FrameworkElement target = (FrameworkElement)d; if (target.IsLoaded) SetDataContext(target); else target.Loaded += OnTargetLoaded; } private static void OnTargetLoaded(object sender, RoutedEventArgs e) { FrameworkElement target = (FrameworkElement)sender; target.Loaded -= OnTargetLoaded; SetDataContext(target); } private static void SetDataContext(FrameworkElement target) { Type ancestorType = GetAncestorType(target); if (ancestorType != null) target.DataContext = FindParent(target, ancestorType); } private static object FindParent(DependencyObject dependencyObject, Type ancestorType) { DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject); if (parent == null) return null; if (ancestorType.IsAssignableFrom(parent.GetType())) return parent; return FindParent(parent, ancestorType); } }
When the dependency property is set to a Type
, the OnAncestorTypeChanged
property changed callback will be invoked. It waits until the target element (which again is the TextBlock
in this particular example) has been loaded and then uses the VisualTreeHelper.GetParent
method to get a reference to first parent element of the specified type. Finally, it sets the DataContext
of the target element to the element returned by the FindParent
method.
Here is how the modified XAML markup that uses the custom attached property would look like:
<ItemsRepeater ItemsSource="{Binding Items}" Tag="aTag"> <ItemsRepeater.ItemTemplate> <DataTemplate> <TextBlock local:AncestorSource.AncestorType="ItemsRepeater" Text="{Binding Tag}" /> </DataTemplate> </ItemsRepeater.ItemTemplate> </ItemsRepeater>
The binding to the Tag
property of the parent ItemsRepeater
now works as expected as the attached property programmatically sets the DataContext
of the TextBlock
to the parent ItemsRepeater
and the Text
property of the TextBlock
is bound directly to the Tag
property of its current DataContext
.
There is one caveat with this approach. If you intend to bind several properties of the same target element to different source objects, setting the DataContext
of the target element won’t really work.
Consider the following WPF example where the ItemTemplate
contains a Button
that executes a command of the parent view model and passes a reference to the current item in the ListBox
as a parameter to that command:
<ListBox ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate> <Button Content="Save" Command="{Binding DataContext.SaveCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
The Command
property is bound to a SaveCommand
property of the DataContext
of the ListBox
and the CommandParameter
property is bound to the current object itself in the Items
collection.
If you want to migrate this Button
to an ItemsRepeater
in UWP or WinUI 3, you could introduce an invisible element that uses the attached property from above to “capture” a reference to the parent ItemsRepeater
. You then use the ElementName
property to bind the Command
property of the Button
to the captured ancestor element’s DataContext
:
<ItemsRepeater ItemsSource="{Binding Items}"> <ItemsRepeater.ItemTemplate> <DataTemplate> <Grid> <TextBlock x:Name="proxy" local:AncestorSource.AncestorType="ItemsRepeater" /> <Button Content="Save" Command="{Binding DataContext.DataContext.SaveCommand, ElementName=proxy}" CommandParameter="{Binding}"> </Button> </Grid> </DataTemplate> </ItemsRepeater.ItemTemplate> </ItemsRepeater>
If you want to try this out for yourself, I’ve uploaded the sample code to GitHub. It targets version 1.0 preview 3 (1.0.0-preview3) of the Windows App SDK which was latest release of the preview channel for version 1.0 of the Windows App SDK available at the time of writing this in the middle of January 2022.
Excellent solution. Thank you very much!
I am searching for a solution to this since hours, thank you. WinUI 3 and DataGrid command binding in my case. Not a single clue after one hour on StackOverflow posts.. You saved me :)