MaterialDesignInXamlToolkit icon indicating copy to clipboard operation
MaterialDesignInXamlToolkit copied to clipboard

RelativeSource FindAncestor binding errors

Open JorisCleVR opened this issue 6 months ago • 4 comments

Bug explanation

I am currently having a lot of binding errors. For example: System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.FrameworkElement', AncestorLevel='1''. BindingExpression:Path=(0); DataItem=null; target element is 'GroupBox' (Name=''); target property is 'Foreground' (type 'Brush')

Some context Earlier I had them in some occasions, but was not able to find the root cause of them. But since I now enabled virtualization on all of my ListBoxes I am getting them more cinsitently. And it became even more reproducable now that I am introducing a LazyLoadContentControl.

For the completeness: What the LazyLoadContentControl does is it first loads a more leightweight placeholder control (defined in a DataTemplate). Then when the GUI has time (using the GuiDispatcher with DispatcherPriority.Background) it renders the (complex) actual control (also defined in a DataTemplate). The results of this control are great and it feels the same as most web applications do with loading their contents.

Reference code example (simplified) The following example is a simplified version of the code. Note it can not actually be run, but is meant as a reference to get a more clear understanding of how the MaterialDesignCardGroupBox style causing the error is used

<ListBox Style="{StaticResource MaterialDesignListBox}" Margin="0" ItemsSource="{Binding Projects}" VirtualizingPanel.IsVirtualizin="True" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.IsVirtualizingWhenGrouping="True" VirtualizingPanel.ScrollUnit="Pixel"
         VirtualizingPanel.CacheLengthUnit="Item" VirtualizingPanel.CacheLength="10"
HorizontalContentAlignment="Stretch" ScrollViewer.PanningMode="VerticalFirst" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto">
    <ListBox.Resources>
        <DataTemplate x:Key="ItemLoadingContentTemplate">
            <GroupBox Header="{Binding}" Background="{DynamicResource MaterialDesign.Brush.Background}" Height="78" Padding="0">
                <GroupBox.Style>
                    <Style TargetType="GroupBox" BasedOn="{StaticResource MaterialDesignCardGroupBox}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding IsFocused}" Value="true">
                                <Setter Property="md:ColorZoneAssist.Mode" Value="PrimaryLight" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </GroupBox.Style>
                <GroupBox.HeaderTemplate>
                    <DataTemplate>
						<TextBlock Style="{StaticResource MaterialDesignSubtitle2TextBlock}" Text="{Binding Name}" />
                    </DataTemplate>
                </GroupBox.HeaderTemplate>
            </GroupBox>
        </DataTemplate>
        <DataTemplate x:Key="ItemLoadedContentTemplate">
            <v:AvailableProjectView DataContext="{Binding}" />
        </DataTemplate>
    </ListBox.Resources>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignListBoxItem}">
            <Setter Property="Margin" Value="0" />
            <Setter Property="Padding" Value="0" />
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel Margin="4" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <c:LazyLoadContentControl Style="{StaticResource StyleLazyLoadContentControl}"
                                      DataContext="{Binding}" Content="{Binding}"
                                      LoadingContentTemplate="{StaticResource ItemLoadingContentTemplate}" LoadedContentTemplate="{StaticResource ItemLoadedContentTemplate}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Error cause As for what I found on this topic is that in some cases (e.g. when Virtualization is enabled on a ListBox) the items are temporarily removed from the Visual Tree but still kept alive because of the VirtualizationMode.Recycling. Since it has no parent anymore it will not have an Ancestor of type FrameworkElement an therefore resulting in the given binding error.

Fix proposal What I found to prevent this is to define a FallbackValue. So in this case the binding in MaterialDesignCardGroupBox would change from:

<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}, Path=(TextElement.Foreground)}" />

to:

<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}, Path=(TextElement.Foreground), FallbackValue=Black}" />

Of course this fix needs to be applied in other styles using RelativeSource and AncestorType as well.

Wrap up Please let me know what you think of defining a FallbackValue for the cases where we do a RelativeSource lookup by defining an AncestorType. If this is an acceptable solution I will create a PR that implements this in the places necessary.

Furthermore if you have any other toughs on the issue please let me know.

Version

Master branch

JorisCleVR avatar May 28 '25 12:05 JorisCleVR

Hi @JorisCleVR I am perfectly fine with the solution of adding a fallback value. I will admit there are probably some other styles that could benefit from a similar change as well.

Keboo avatar May 30 '25 07:05 Keboo

Oke, then I will go through the styles and check where a fallback value can be applied. I will get back to you when I have a PR available for it.

JorisCleVR avatar Jun 03 '25 07:06 JorisCleVR

Just now created a pull request with changes for all bindings that use AncestorType for the look up: https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/pull/3862

JorisCleVR avatar Jun 03 '25 14:06 JorisCleVR

When looking further into the issues we are having with these bindings I found a more conclusive answer to why it goes wrong inside of a DataTemplate and wanted to place it here for completeness. The reason of the error is that the ancestor lookup stops at a ContentPresenter (that is used to depict a DataTemplate). This means that the Ancestor lookup for a FrameworkElement of a in the examples case a GroupBox fails because the GroupBox is the root element of the DataTemplate. To solve this issue an element that does an Ancestor look up should not be placed in the root of a DataTemplate. Nesting it inside of a FrameworkElement (e.g. Grid, StackPanel, DockPanel, Decorator) resolves this. Note that placing it in a ContentControl does not resolve it, because then again it is depicted within the ContentPresenter of the ContentControl.

For my own code I created a DataTemplateContainer class (inheriting from Decorator) that is always the root element of my DataTemplates. This resolves the issue in all cases because now the GroupBox finds the DataTemplateContainer as the ancestor.

JorisCleVR avatar Jun 06 '25 15:06 JorisCleVR

Closing this as it seems fixed with @JorisCleVR PR #3862. If that is not the case, feel free to re-open.

nicolaihenriksen avatar Nov 08 '25 20:11 nicolaihenriksen