c# - 如何从 xaml 中的 ItemTemplate 访问在代码中定义的用户控件的依赖项属性?
问题描述
我有一个 ListBox,它的 ListBoxItems 是从 UserControl 派生的控件。这些控件中的每一个都包含一个派生自 Button 的 UserControl 和一些 TextBox。我想通过使用 ListBox 中的 ItemTemplate 将这些绑定到我的模型中的元素,但是我无法从 ItemTemplate 为按钮分配值。
ListViewer 包含 ListBox 及其 DataTemplate:
<UserControl x:Class="TestListBoxBinding.ListViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:model="clr-namespace:Model"
xmlns:vm="clr-namespace:ViewModel"
xmlns:local="clr-namespace:TestListBoxBinding"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:ListViewerViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<DataTemplate DataType="{x:Type model:Person}">
<Grid Name="theListBoxItemGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:PersonButton Grid.Column="0" x:Name="thePersonButton"
FirstName="Ken"
LastName="Jones"/>
<TextBlock Grid.Column="1" Name="theFirstName" Text="{Binding FirstName}" Margin="5,0,5,0"/>
<TextBlock Grid.Column="2" Name="theLastName" Text="{Binding LastName}" Margin="0,0,5,0"/>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="theHeader" Text="These are my people"/>
<ListBox Grid.Row="1" Name="theListBox"
ItemsSource="{Binding MyPeople}"/>
<StackPanel Grid.Row="2" Name="theFooterPanel" Orientation="Horizontal">
<TextBlock Name="theFooterLabel" Text="Count = "/>
<TextBlock Name="theFooterCount" Text="{Binding MyPeople.Count}"/>
</StackPanel>
</Grid>
</UserControl>
及其背后的代码:
using Model;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace TestListBoxBinding
{
/// <summary>
/// Interaction logic for ListViewer.xaml
/// </summary>
public partial class ListViewer : UserControl
{
public ListViewer()
{
InitializeComponent();
}
}
}
namespace ViewModel
{
public class ListViewerViewModel
{
// Properties
public People MyPeople { get; private set; }
// Constructors
public ListViewerViewModel()
{
MyPeople = (People)Application.Current.Resources["theOneAndOnlyPeople"];
MyPeople.Add(new Person("Able", "Baker"));
MyPeople.Add(new Person("Carla", "Douglas"));
MyPeople.Add(new Person("Ed", "Fiducia"));
}
}
}
namespace Model
{
public class Person
{
// Properties
public string FirstName { get; set; }
public string LastName { get; set; }
// Constructors
public Person()
{
FirstName = "John";
LastName = "Doe";
}
public Person(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
}
public class People : ObservableCollection<Person>
{
}
}
PersonButton 在后面的代码中定义了 FirstName 和 LastName DependencyProperties,我最终将使用它们来设置按钮的显示内容。这是它的xml:
<UserControl x:Class="TestListBoxBinding.PersonButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestListBoxBinding"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Button Name="theButton" Padding="5,0,5,0"/>
</Grid>
</UserControl>
及其背后的代码:
using System.Windows;
using System.Windows.Controls;
namespace TestListBoxBinding
{
/// <summary>
/// Interaction logic for PersonButton.xaml
/// </summary>
public partial class PersonButton : UserControl
{
// Properties
public static readonly DependencyProperty FirstNameProperty = DependencyProperty.Register("FirstName", typeof(string), typeof(PersonButton));
public string FirstName { get { return (string)GetValue(FirstNameProperty); } set { SetValue(FirstNameProperty, value); } }
public static readonly DependencyProperty LastNameProperty = DependencyProperty.Register("LastName", typeof(string), typeof(PersonButton));
public string LastName { get { return (string)GetValue(LastNameProperty); } set { SetValue(LastNameProperty, value); } }
// Constructors
public PersonButton()
{
InitializeComponent();
FirstName = "Ira";
LastName = "Jacobs";
theButton.Content = FirstName + " " + LastName;
}
}
}
从输出中可以看出:
从 DataTemplate 为 ListBox 设置 PersonButton 的 FirstName 和 LastName 属性的尝试失败。我在属性集上设置了断点,它们仅在构造控件时触发,但从不从 DataTemplate 触发。我使用 Live Visual Tree 来确认属性实际上仍然在初始化时设置。
我还应该提到,最初,我收到一个 Intellisense 警告,指出无法从 DataTemplate 访问这些属性,但在我删除 bin 和 obj 文件夹,清理并重建解决方案后,它就消失了。
更新
我相信这与评论中建议的副本不同。
至于 PersonButton 没有任何意义,这整个代码集合是一个更复杂的应用程序的一个大大简化的表示,我创建它只是为了以更容易阅读的形式显示我遇到的问题。我已经测试了这段代码,它复制了我遇到的问题。PersonButton 表示的实际控件要复杂得多,后面代码中定义的属性的使用完全适合该控件。PersonButton 这样做的唯一原因是为了便于判断这些属性是否由分配设置。
现在,要在这里解决我的问题,它是“为什么 PersonButton.FirstName 和 PersonButton.LastName 没有被我的 ListViewer 控件中的 theListBox 的 DataTemplate 中的 thePersonButton 的赋值语句改变?”
正如我之前提到的,我已经运行了几个测试,它们都验证了实际控件中的这些值没有被赋值设置。自从看到 Clemens 的回复后,我什至实现了 PropertyChangedCallback 函数,它们也确认了 ListBox 分配没有设置 PersonButton 属性。
我特别感兴趣的是,这些分配本身将成为绑定,允许我通过 ItemsSource 机制捕获对模型中列表中每个单独项目的更改。在我的原始应用程序中,这些绑定有效,但我的问题是实际上将它们分配给 PersonButton 控件。
解决方案
我发现了问题!它与依赖属性值优先级有关。当相同的依赖属性被快速连续设置多次(足够接近以至于它们可能相互干扰)时,系统会对其处理应用优先级。本质上,本地设置值的处理优先级高于远程设置值。
在这种情况下,当 ListViewViewModel 初始化模型中的 People 集合时,属性更改处理程序会快速连续命中两次(通常小于 1 毫秒):一次是在构造 Person 时,一次是在将 Person 添加到集合时(以及,因此,列表框)。
public ListViewerViewModel()
{
MyPeople = (People)Application.Current.Resources["theOneAndOnlyPeople"];
MyPeople.Add(new Person("Able", "Baker"));
MyPeople.Add(new Person("Carla", "Douglas"));
MyPeople.Add(new Person("Ed", "Fiducia"));
}
因此,它们相互冲突,系统优先考虑由 PersonButton 的构造函数设置的值,因为它是本地的。
我在 PersonButton 构造函数中注释掉了默认值的分配,瞧,来自 ListBox 的分配起作用了。
public Person()
{
FirstName = "John";
LastName = "Doe";
}
这是一个追逐的PITA,但现在我有了答案,这也很有趣。