WPF: Почему DataContextChanged не вызывается на логическом дочернем элементе?
У меня проблема с тем, что DataContextChanged не возникает на логическом дочернем элементе моего пользовательского элемента управления Panel. Я сузил это до этого:
Начиная с созданного мастером приложения WPF, я добавляю:
private void Window_Loaded( object sender, RoutedEventArgs e )
{
var elt = new FrameworkElement();
this.AddLogicalChild( elt );
DataContext = 42;
Debug.Assert( (int)elt.DataContext == 42 );
}
Как я понимаю, это работает, потому что DataContext является наследуемым свойством зависимости.
Теперь я добавляю обработчики событий для DataContextChanged как в Window (this), так и в его логическом дочернем элементе:
this.DataContextChanged +=
delegate { Debug.WriteLine( "this:DataContextChanged" ); };
elt.DataContextChanged +=
delegate { Debug.WriteLine( "elt:DataContextChanged" ); };
Если я запускаю это, будет выполняться только первый обработчик событий. Почему это? Если вместо AddLogicalChild (elt) я делаю следующее:
this.Content = elt;
оба обработчика будут выполнены. Но в моем случае это не вариант - я добавляю FrameworkContentElements в свой элемент управления, которые не должны быть визуальными дочерними элементами.
Что тут происходит? Должен ли я сделать что-то еще, кроме AddLogicalChild(), чтобы это работало?
(К счастью, существует довольно простой обходной путь - просто свяжите DataContext элемента с DataContext окна)
BindingOperations.SetBinding( elt, FrameworkElement.DataContextProperty,
new Binding( "DataContext" ) { Source = this } );
Спасибо.
2 ответа
Вы должны переопределить LogicalChildren
свойство тоже:
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return elt; }
}
Конечно, вы также захотите вернуть любые логические дочерние элементы, определенные базовой реализацией.
Я хотел бы добавить несколько советов к ответу Кента, если у кого-то возникнут похожие проблемы:
Если вы создаете настраиваемый элемент управления с несколькими объектами содержимого, вы должны убедиться:
- Объекты содержимого добавляются в LogicalTree через AddLogicalChild()
- Создайте свой собственный класс Enumerator и верните его экземпляр в переопределенном свойстве LogicalChildren.
Если вы не добавите объекты содержимого в логическое дерево, вы можете столкнуться с такими проблемами, как привязки с ElementNames, которые не могут быть решены (ElementName разрешается FindName, который, в свою очередь, использует LogicalTree для поиска элементов).
Что делает его еще более опасным, так это то, что, по моему опыту, если вы пропустите добавление объектов в логическое дерево, разрешение ElementName работает в некоторых сценариях и не работает в других сценариях.
Если вы не переопределяете LogicalChildren, DataContext не обновляется, как описано выше.
Вот небольшой пример с простым SplitContainer:
SplitContainer:
public class SplitContainer : Control
{
static SplitContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitContainer), new FrameworkPropertyMetadata(typeof(SplitContainer)));
}
/// <summary>
/// Identifies the <see cref="Child1"/> property.
/// </summary>
public static readonly DependencyProperty Child1Property =
DependencyProperty.Register(nameof(Child1), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child1PropertyChangedCallback));
/// <summary>
/// Left Container
/// </summary>
public object Child1
{
get { return (object)GetValue(Child1Property); }
set { SetValue(Child1Property, value); }
}
/// <summary>
/// Identifies the <see cref="Child2"/> property.
/// </summary>
public static readonly DependencyProperty Child2Property =
DependencyProperty.Register(nameof(Child2), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child2PropertyChangedCallback));
/// <summary>
/// Right Container
/// </summary>
public object Child2
{
get { return (object)GetValue(Child2Property); }
set { SetValue(Child2Property, value); }
}
private static void Child1PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
private static void Child2PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
protected override IEnumerator LogicalChildren
{
get
{
return new SplitContainerLogicalChildrenEnumerator(this);
}
}
}
SplitContainerLogicalChildrenEnumerator:
internal class SplitContainerLogicalChildrenEnumerator : IEnumerator
{
private readonly SplitContainer splitContainer;
private int index = -1;
public SplitContainerLogicalChildrenEnumerator(SplitContainer splitContainer)
{
this.splitContainer = splitContainer;
}
public object Current
{
get
{
if (index == 0)
{
return splitContainer.Child1;
}
else if (index == 1)
{
return splitContainer.Child2;
}
throw new InvalidOperationException("No child for this index available");
}
}
public bool MoveNext()
{
index++;
return index < 2;
}
public void Reset()
{
index = -1;
}
}
Стиль (например, в Themes / generic.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=PresentationFramework"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SplitContainerElementNameProblem">
<Style TargetType="{x:Type local:SplitContainer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SplitContainer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Child1}" />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Child2}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Пример, демонстрирующий, что каждая привязка работает нормально:
XAML:
<Window x:Class="SplitContainerElementNameProblem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SplitContainerElementNameProblem"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBox x:Name="text1" Text="abc" />
</Grid>
<local:SplitContainer Grid.Row="1">
<local:SplitContainer.Child1>
<TextBox x:Name="text2"
Text="{Binding ElementName=text1, Path=Text}" />
</local:SplitContainer.Child1>
<local:SplitContainer.Child2>
<StackPanel>
<TextBox x:Name="text3"
Text="{Binding ElementName=text2, Path=Text}" />
<TextBox x:Name="text4"
Text="{Binding MyName}" />
</StackPanel>
</local:SplitContainer.Child2>
</local:SplitContainer>
</Grid>
XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyName = "Bruno";
}
public string MyName
{
get;
set;
}
}