动态赋值DataContext时Avalonia控件OnAttached()方法不触发问题求助
Avalonia TextEditor 动态DataContext绑定问题
问题场景
使用Avalonia TextEditor时,通过附加属性实现Text与ViewModel的绑定,遇到以下异常情况:
- TextEditor位于UserControl内的TabControl标签页中。应用初始加载时,
TextEditorDocumentBindingBehavior.OnAttached()触发,但此时ViewModel为NULL,方法直接返回(符合预期,因为SelectedItem.SelectedChildItem初始为NULL)。 - 父UserControl加载完成后,
SelectedItem.SelectedChildItem获得值,转换器触发并返回对应的ViewModel作为DataContext,但此时OnAttached()并未触发,导致ViewModel中的文本无法显示在TextEditor中。 - 切换到第二个标签页再切回第一个标签页时,
OnAttached()触发,ViewModel中的Source文本能正常显示。
请教问题
- 该现象的原因是什么?
- 是否需要强制触发
OnAttached()? - 在父控件动态为ViewModel赋值DataContext时,如何确保
OnAttached()触发以加载初始值?
ViewModel 属性代码
[Reactive] public string? Source {get; set;} = string.Empty; public string? SourceText { get { return Source; } set { if (value != Source) { Source = value; } } }
TextEditorProperties 附加属性代码
public class TextEditorProperties { public static readonly AttachedProperty<string> BindTextToProperty = AvaloniaProperty.RegisterAttached<TextEditor, string>("BindTextTo", typeof(TextEditorProperties)); public static string GetBindTextTo(TextEditor p_textEditor) { return p_textEditor.GetValue(BindTextToProperty); } public static void SetBindTextTo(TextEditor p_textEditor, string p_value) { p_textEditor.SetValue(BindTextToProperty, p_value); } }
TextEditorDocumentBindingBehavior 行为实现代码
public class TextEditorDocumentBindingBehavior : Behavior<TextEditor> { private TextDocument? TextEditorDocument; protected override void OnAttached() { base.OnAttached(); if (AssociatedObject is TextEditor textEditor) { if (AssociatedObject.DataContext is MyViewModel viewModel) { var propertyToBind = TextEditorProperties.GetBindTextTo(AssociatedObject); if (string.IsNullOrWhiteSpace(propertyToBind)) { throw new InvalidOperationException("BindTextTo property cannot be null or empty."); } TextEditorDocument = new TextDocument(); TextEditorDocument.TextChanged += (sender, e) => { var property = viewModel.GetType().GetProperty(propertyToBind); if (property != null) { if (sender is not TextDocument textDocument) return; property.SetValue(viewModel, textDocument.Text); } }; var initialText = GetInitialText(viewModel, propertyToBind); TextEditorDocument.Text = initialText; AssociatedObject.TextArea.Document = TextEditorDocument; } } } protected override void OnDetaching() { base.OnDetaching(); if (TextEditorDocument != null) { TextEditorDocument.TextChanged -= (sender, e) => { }; TextEditorDocument = null; } } private string? GetInitialText(MyViewModel p_viewModel, string p_propertyToBind) { var property = p_viewModel.GetType().GetProperty(p_propertyToBind); if (property != null) { return property.GetValue(p_viewModel) as string; } return string.Empty; } }
XAML 定义
父UserControl
<UserControl> <TabControl> <TabItem Header="One" > <Grid> <AvaloniaEdit:TextEditor Name="TextEditorTest" behaviors:TextEditorProperties.BindTextTo="SourceText" FontFamily="Cascadia Code,Consolas,Menlo,Monospace"> <AvaloniaInteractive:Interaction.Behaviors> <behaviors:TextEditorDocumentBindingBehavior/> </AvaloniaInteractive:Interaction.Behaviors> </AvaloniaEdit:TextEditor> </Grid> </ScrollViewer> </TabItem> <TabItem Header="Two"> </TabItem> </TabControl> </UserControl>
MyUserControl
<MyUserControl > <MyUserControl.DataContext> <Binding Path="SelectedItem.SelectedChildItem" Converter="{StaticResource SelectedChildItemToMyViewModelConverter}"/> </MyUserControl.DataContext> </MyUserControl>
问题解答
1. 现象原因
Behavior的OnAttached()方法仅在行为附加到控件且控件已加载时触发一次。初始加载时,TabControl的第一个TabItem虽然已创建,但因为DataContext为NULL,行为逻辑直接跳过。- 当父控件的
SelectedItem.SelectedChildItem赋值后,TextEditor的DataContext更新,但此时行为已经完成OnAttached()的执行,不会自动重新触发。 - 切换标签页时,TabItem会经历卸载和重新加载的过程,行为会重新附加到TextEditor,此时
DataContext已经存在,所以OnAttached()能正常执行并加载文本。
2. 是否需要强制触发OnAttached()
不需要强制触发OnAttached(),这不符合Behavior的设计逻辑。OnAttached()用于初始化一次性资源,重复触发可能导致资源泄漏或重复绑定。正确做法是监听DataContext的变化,在变化时重新执行绑定逻辑。
3. 解决方案:监听DataContext变化
修改TextEditorDocumentBindingBehavior,添加DataContext变化监听,在DataContext更新时重新初始化绑定:
public class TextEditorDocumentBindingBehavior : Behavior<TextEditor> { private TextDocument? TextEditorDocument; private IDisposable? _dataContextChangedSubscription; protected override void OnAttached() { base.OnAttached(); if (AssociatedObject is TextEditor textEditor) { // 订阅DataContext变化事件 _dataContextChangedSubscription = textEditor.GetObservable(TextEditor.DataContextProperty) .Subscribe(OnDataContextChanged); // 初始检查当前DataContext OnDataContextChanged(textEditor.DataContext); } } protected override void OnDetaching() { base.OnDetaching(); _dataContextChangedSubscription?.Dispose(); CleanUpExistingDocument(); } private void OnDataContextChanged(object? newDataContext) { // 清理旧的文档资源 CleanUpExistingDocument(); if (newDataContext is MyViewModel viewModel) { var propertyToBind = TextEditorProperties.GetBindTextTo(AssociatedObject); if (string.IsNullOrWhiteSpace(propertyToBind)) { throw new InvalidOperationException("BindTextTo property cannot be null or empty."); } TextEditorDocument = new TextDocument(); TextEditorDocument.TextChanged += (sender, e) => { var property = viewModel.GetType().GetProperty(propertyToBind); if (property != null && sender is TextDocument textDocument) { property.SetValue(viewModel, textDocument.Text); } }; var initialText = GetInitialText(viewModel, propertyToBind); TextEditorDocument.Text = initialText; AssociatedObject.TextArea.Document = TextEditorDocument; } } private void CleanUpExistingDocument() { if (TextEditorDocument != null) { TextEditorDocument.TextChanged -= (sender, e) => { }; TextEditorDocument = null; } } private string? GetInitialText(MyViewModel p_viewModel, string p_propertyToBind) { var property = p_viewModel.GetType().GetProperty(p_propertyToBind); return property?.GetValue(p_viewModel) as string ?? string.Empty; } }
关键修改点:
- 订阅
TextEditor.DataContextProperty的变化,每次DataContext更新时调用OnDataContextChanged方法。 - 在
OnDataContextChanged中先清理旧的TextDocument资源,再根据新的ViewModel重新初始化绑定。 - 初始
OnAttached时立即检查一次DataContext,覆盖初始加载的场景。
内容的提问来源于stack exchange,提问作者JohnB
相关产品推荐
相关产品推荐

