1. 问题来源
在开发自定义控件 SubTitles 时,我遇到了一个问题:ItemsControl 中展示的中英文字幕总是显示相同的文本,尽管我绑定的是不同的字幕。
经过排除不相关因素后,我发现问题并不在 SubTitles 控件本身,而是在 ItemsControl 的 ItemTemplate 中使用的另一个自定义控件 SentenceBox。
SentenceBox 用于显示句子文本,我使用两个 SentenceBox 来展示不同的中英文对应句子。然而,无论每个 SentenceBox 的 Text 内容设置为何,所有 SentenceBox 总是显示最后一个 SentenceBox 的 Text。
显然,问题在于这些 SentenceBox 的 Text 数据共享了同一个对象。
2. 错误代码
public static readonly DependencyProperty WordsProperty =
DependencyProperty.Register("Words", typeof(ObservableCollection<string>),
typeof(SentenceBox), new PropertyMetadata(new ObservableCollection<string>));
依赖属性概念回顾
- 定义:在WPF中,依赖属性是一种特殊的属性,其值不仅可以通过常规方式设置,还可以通过数据绑定、样式和模板等方式动态获取。它们允许属性的值依赖于其他输入值。
- 注册:通过调用
DependencyProperty.Register
方法来注册一个依赖属性。注册时需要指定属性名、属性类型、拥有者类型以及属性元数据。容易误将注册依赖属性时设置的静态默认值当作每个实例的初始值。实际上,这个静态默认值仅用于属性未显式设置时的后备值,而非每个实例的初始状态。
静态默认值与实例默认值混淆
- 静态默认值:在注册依赖属性时,可以通过
PropertyMetadata
的构造函数指定一个默认值。然而,这个默认值对所有实例都是共享的,并不适合作为每个实例特有的初始值。如本例所示,如果依赖属性的默认值是一个可变集合(如ObservableCollection<T>
),且该默认值在属性注册时静态创建,则所有使用该属性的实例将共享这个集合实例,导致数据混乱。 - 实例默认值:对于需要在实例创建时动态设置的默认值,应该避免在属性注册时直接指定默认值,而是在某个属性更改处理程序中(如本例中的
TextPropertyChanged
)检查并设置。
3. 示例代码修正
// WordsProperty 的注册,移除默认值
public static readonly DependencyProperty WordsProperty =
DependencyProperty.Register("Words", typeof(ObservableCollection<string>),
typeof(SentenceBox), new PropertyMetadata(null));
// TextPropertyChanged 处理程序
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var box = d as SentenceBox;
if (box!= null)
{
if (box.Words == null)
{
box.Words = new ObservableCollection<string>(); // 动态创建实例级默认值
}
box.UpdateWords();
}
}
4. 总结
- 在注册依赖属性时,对于不可变类型或基本数据类型,可以直接设置静态默认值。
- 对于可变类型(如集合),应避免在属性注册时设置静态默认值。应在属性更改处理程序中动态检查并设置实例级默认值。