调试WPF中XAML绑定失败:切换工具时出现找不到源错误
基于DotNet8.0和WPF开发的大型C#应用程序,视图包含多个ComboBox,示例代码如下:
<ComboBox Name="cboAdapter" Grid.Column="2" Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Stretch" Style="{StaticResource CompanyComboBox}" IsEnabled="{Binding IsAdapterCboEnabled}" DisplayMemberPath="AdapterName" ItemsSource="{Binding AvailableAdapters, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Path=SelectedAdapter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
该视图用于查看不同工具的属性,当从TreeView中选择新工具时,后台出现不影响视图渲染的XAML绑定失败提示:
Cannot find source: RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ItemsControl', AncestorLevel='1'.
失败次数与之前打开ComboBox的次数一致。尝试设置Horizontal和VerticalContentAlignment无效,怀疑是ComboBox关闭时DataContext为空导致,但在ComboBox模板和控件本身设置无效果。
选择工具的TreeView代码:
<TreeView x:Name="LevelItemsTreeView" Grid.Row="2" ItemTemplate="{StaticResource level0}" ItemsSource="{Binding LevelItems, Mode=TwoWay}" Background="{StaticResource BrushCompanyGrayLight40}"> <i:Interaction.Behaviors> <Behaviors:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </i:Interaction.Behaviors> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource TreeViewItemStyleCompany}"> <Setter Property="Behaviors:TreeViewItemBehavior.IsBroughtIntoViewWhenSelected" Value="True" /> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Setter Property="IsEnabled" Value="{Binding IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Setter Property="FontWeight" Value="Normal" /> </Style> </TreeView.ItemContainerStyle> </TreeView>
ComboBox使用的样式代码:
<Style x:Key="CompanyComboBox" TargetType="{x:Type ComboBox}"> <Setter Property="UIElement.SnapsToDevicePixels" Value="True"/> <Setter Property="FrameworkElement.OverridesDefaultStyle" Value="True"/> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/> <Setter Property="ScrollViewer.CanContentScroll" Value="True"/> <Setter Property="TextElement.Foreground" Value="{StaticResource BrushCompanyWhite}"/> <Setter Property="TextElement.FontSize" Value="22"/> <Setter Property="FrameworkElement.FocusVisualStyle" Value="{x:Null}"/> <Setter Property="MinWidth" Value="80"/> <Setter Property="MinHeight" Value="20"/> <Setter Property="HorizontalAlignment" Value="Stretch"/> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="IsEditable" Value="False"/> <Setter Property="Padding" Value="4,0,4,0"/> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="true"> <AdornedElementPlaceholder Name="ComboAdorner" VerticalAlignment="Center" > <Border BorderBrush="{StaticResource BrushCompanyError}" BorderThickness="2" CornerRadius="8" SnapsToDevicePixels="True"/> </AdornedElementPlaceholder> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate TargetType="ComboBox"> <Grid> <ToggleButton Background="{StaticResource BrushCompanyGrayLight40}" Name="ToggleButton" Grid.Column="2" ClickMode="Press" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Template="{StaticResource CompanyToggleButton}"/> <ContentPresenter Name="ContentSite" Margin="5, 3, 23, 3" IsHitTestVisible="False" HorizontalAlignment="Left" VerticalAlignment="Center" Content="{TemplateBinding ComboBox.SelectionBoxItem}" ContentTemplate="{TemplateBinding ComboBox.SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"> <ContentPresenter.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="Height" Value="36" /> </Style> </ContentPresenter.Resources> </ContentPresenter> <TextBox Name="PART_EditableTextBox" Background="{StaticResource BrushCompanyGrayLight40}" Foreground="{StaticResource BrushCompanyWhite}" SelectionBrush="{StaticResource BrushCompanyGreen}" Margin="3, 3, 23, 3" IsReadOnly="{TemplateBinding IsReadOnly}" Visibility="Hidden" HorizontalAlignment="Left" VerticalAlignment="Center" Focusable="True"> <TextBox.Template> <ControlTemplate TargetType="TextBox"> <Border Name="PART_ContentHost" Focusable="False"/> </ControlTemplate> </TextBox.Template> </TextBox> <!-- Popup showing items --> <Popup Name="Popup" Placement="Bottom" Focusable="False" AllowsTransparency="True" IsOpen="{TemplateBinding ComboBox.IsDropDownOpen}" PopupAnimation="Slide"> <Grid Name="DropDown" SnapsToDevicePixels="True" MinWidth="{TemplateBinding FrameworkElement.ActualWidth}" MaxHeight="{TemplateBinding ComboBox.MaxDropDownHeight}"> <Border Name="DropDownBorder" Background="White" Margin="0, 1, 0, 0" CornerRadius="8" BorderThickness="1" SnapsToDevicePixels="True" BorderBrush="{StaticResource BrushCompanyWhite}"/> <ScrollViewer Margin="4" SnapsToDevicePixels="True"> <ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" > </ItemsPresenter> </ScrollViewer> </Grid> </Popup> </Grid> <ControlTemplate.Triggers> <Trigger Property="ItemsControl.HasItems" Value="False"> <Setter Property="FrameworkElement.MinHeight" TargetName="DropDownBorder" Value="95"/> </Trigger> <Trigger Property="UIElement.IsEnabled" Value="False"> <Setter Property="Opacity" Value="0.25"/> </Trigger> <Trigger Property="ItemsControl.IsGrouping" Value="True"> <Setter Property="ScrollViewer.CanContentScroll" Value="False"/> </Trigger> <Trigger Property="ComboBox.IsEditable" Value="True"> <Setter Property="KeyboardNavigation.IsTabStop" Value="False"/> <Setter Property="UIElement.Visibility" TargetName="PART_EditableTextBox" Value="Visible"/> <Setter Property="UIElement.Visibility" TargetName="ContentSite" Value="Hidden"/> </Trigger> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
针对这个绑定失败问题,可按以下步骤排查解决:
检查ComboBox的ItemTemplate或全局样式:错误提示中的
RelativeSource FindAncestor绑定大概率来自ComboBox下拉项的模板(自定义ItemTemplate或全局ComboBoxItem样式)。当TreeView切换选中项导致DataContext变更时,已打开过的ComboBox下拉Popup未被正确销毁,绑定找不到父级ItemsControl而报错。给这类绑定添加FallbackValue={x:Null},或替换为直接绑定到Item自身属性,避免依赖父级控件。切换TreeView选中项时强制关闭ComboBox下拉Popup:在TreeView选中项变更事件中,遍历视图内所有ComboBox,将
IsDropDownOpen设为false,确保下拉Popup被正确关闭清理。示例后台代码:
private void LevelItemsTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { foreach (var combo in FindVisualChildren<ComboBox>(this)) { combo.IsDropDownOpen = false; } } private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject { if (depObj == null) yield break; for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) { var child = VisualTreeHelper.GetChild(depObj, i); if (child is T t) yield return t; foreach (var childOfChild in FindVisualChildren<T>(child)) yield return childOfChild; } }
优化DataContext清理逻辑:如果ViewModel实现了
IDisposable,在Dispose方法中清空AvailableAdapters等集合属性,切断旧数据与视图的绑定。切换TreeView选中项时,确保旧ViewModel被正确释放,避免残留绑定引用。给易出错的绑定添加容错配置:对于所有使用
RelativeSource的绑定,添加FallbackValue和TargetNullValue,即使找不到源也不会抛出错误。例如:
{Binding Path=SomeProperty, RelativeSource={RelativeSource FindAncestor, AncestorType=ItemsControl}, FallbackValue={x:Null}}
- 覆盖全局ComboBoxItem样式:如果应用中有全局的
ComboBoxItem样式,检查是否包含依赖父级ItemsControl的绑定。可以给ComboBox显式设置ItemContainerStyle,覆盖全局样式,避免继承带来的绑定问题。
内容的提问来源于stack exchange,提问作者MaxH

