自定义渲染器子元素更新异常:MenuContent属性变更不触发PropertyChanged
问题描述
我正在开发类似TabPage的自定义控件CSView,它继承自View,包含一组CSMenuItem;每个CSMenuItem继承自BaseMenuItem,拥有继承自ContentView的MenuContent,结构如下:
- CSView
- CSMenuItem
- MenuContent
- Content
- MenuContent
- CSMenuItem
- MenuContent
- Content
- MenuContent
- CSMenuItem
现在遇到的问题是:在XAML文件中修改MenuContent的属性值时,PropertyChanged事件不会触发,MenuContent也无法更新。我确定问题出在自定义渲染器中,需要解决如何在自定义渲染器中更新子元素,并找出事件不触发的原因。
控件代码
class CSView : View { public CSView() { var items = new ObservableCollection<CSMenuItem>(); items.CollectionChanged += OnItemsChanged; Items = items; } public static readonly BindableProperty ItemsProperty = BindableProperty.Create("Items", typeof(IList<CSMenuItem>), typeof(CSView)); void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { foreach (CSMenuItem item in e.NewItems) item.Parent = this; // 设置Item的父级为当前CSView,新增Item时自动绑定 } public IList<CSMenuItem> Items { get => (IList<CSMenuItem>)GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } } class CSMenuItem : BaseMenuItem { public CSMenuItem() { } public static readonly BindableProperty TitleProperty = BindableProperty.Create("Title", typeof(string), typeof(CSMenuItem), default); public static readonly BindableProperty ContentProperty = BindableProperty.Create("Content", typeof(MenuContent), typeof(CSMenuItem)); public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } public MenuContent Content { get => (MenuContent)GetValue(ContentProperty); set => SetValue(ContentProperty, value); } } class MenuContent : ContentView { public MenuContent() { } public static readonly BindableProperty TitleProperty = BindableProperty.Create("Title", typeof(string), typeof(MenuContent)); public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } }
渲染器代码
class CSViewRenderer : ViewRenderer<CSView, FrameLayout>, NavigationBarView.IOnItemSelectedListener { ... protected override void OnElementChanged(ElementChangedEventArgs<CSView> e) { if (e.OldElement != null) UnhandleEvents(e.OldElement); if (e.NewElement != null) { if (Control == null) SetNativeControl(_control); HandleEvents(e.NewElement); } } void HandleEvents(CSView element) => element.PropertyChanged += OnElementPropertyChanged; void UnhandleEvents(CSView element) => element.PropertyChanged -= OnElementPropertyChanged; } // 问题出在这里 class CSMenuItemRender : AbsItemRenderer { ... protected override View CreateNativeControl() { _base = new LinearLayout(Context); _toolBar = new ToolBar(Context); _content = Platform.CreateRendererWithContext(Element.Content, Context); // Element是CSMenuItem类型 _toolBar.Title = Element.Content.Title; _base.Orientation = Orientation.Vertical; _base.AddView(_toolBar, new LinearLayout.LayoutParams(-1, -2)); _base.AddView(_content.View, new LinearLayout.LayoutParams(-1, -2)); ElementRendererUtil.FitElement(Context, _base, _content); Platform.SetRenderer(Element.Content, _content); _content.ElementChanged += OnRendererElementChanged; return _base; } void OnElementChanged(ElementChangedEventArgs<CSMenuItem> e) { if (e.OldElement != null) UnhandleEvents(e.OldElement); if (e.NewElement != null) { if (Control == null) SetNativeControl(this); HandleEvents(e.NewElement); } } void OnRendererElementChanged(object sender, VisualElementChangedEventArgs e) { if (e.OldElement != null) e.OldElement.PropertyChanged -= OnRendererElementPropertyChanged; if (e.NewElement != null) e.NewElement.PropertyChanged += OnRendererElementPropertyChanged; } void OnRendererElementPropertyChanged(object sender, PropertyChangedEventArgs e) { var menuContent = (MenuContent)sender; if (e.PropertyName == MenuContent.TitleProperty.PropertyName) _toolBar.Title = menuContent.Title; else if (e.PropertyName == MenuContent.ContentProperty.PropertyName) _content.Tracker.UpdateLayout(); } void HandleEvents(CSMenuItem element) => element.PropertyChanged += OnElementPropertyChanged; void UnhandleEvents(CSMenuItem element) => element.PropertyChanged -= OnElementPropertyChanged; }
我曾尝试将MenuContent改为继承自Element并设置父级为CSMenuItem,此时Title属性可以变更,但MenuContent的Content属性仍无法更新。需要实现XAML中MenuContent任意属性变更时都能更新,并找出PropertyChanged不触发的原因。
解决方案
一、PropertyChanged不触发的核心原因
- 未监听
CSMenuItem.Content属性的变更:原有代码只在CreateNativeControl时初始化了一次MenuContent的渲染器,当CSMenuItem.Content被替换时,渲染器没有更新,也没有绑定新MenuContent的事件。 - 属性判断错误:
MenuContent继承自ContentView,其Content属性属于ContentView的内置BindableProperty,原有代码中判断MenuContent.ContentProperty是错误的,应该用ContentView.ContentProperty。 - 事件绑定/解绑逻辑不完整:旧的
MenuContent元素没有正确解绑事件,新元素的事件绑定时机不全,导致事件丢失。
二、具体修复步骤
1. 完善CSMenuItem.ContentProperty的回调
在CSMenuItem中修改ContentProperty,添加属性变更回调,确保MenuContent的父子关系正确,并通知渲染器更新:
public static readonly BindableProperty ContentProperty = BindableProperty.Create( nameof(Content), typeof(MenuContent), typeof(CSMenuItem), propertyChanged: OnContentChanged); private static void OnContentChanged(BindableObject bindable, object oldValue, object newValue) { var item = (CSMenuItem)bindable; // 清理旧Content的父级 if (oldValue is MenuContent oldContent) oldContent.Parent = null; // 设置新Content的父级 if (newValue is MenuContent newContent) newContent.Parent = item; }
2. 在CSMenuItemRenderer中监听CSMenuItem的属性变化
添加OnElementPropertyChanged方法的实现,处理Content属性变更时的渲染器更新:
void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { var item = (CSMenuItem)sender; if (e.PropertyName == CSMenuItem.ContentProperty.PropertyName) { // 移除旧的Content渲染器 if (_content != null) { _content.ElementChanged -= OnRendererElementChanged; if (_content.Element != null) _content.Element.PropertyChanged -= OnRendererElementPropertyChanged; _base.RemoveView(_content.View); _content.Dispose(); } // 创建并绑定新的Content渲染器 if (item.Content != null) { _content = Platform.CreateRendererWithContext(item.Content, Context); _toolBar.Title = item.Content.Title; _base.AddView(_content.View, new LinearLayout.LayoutParams(-1, -2)); ElementRendererUtil.FitElement(Context, _base, _content); Platform.SetRenderer(item.Content, _content); _content.ElementChanged += OnRendererElementChanged; if (_content.Element != null) _content.Element.PropertyChanged += OnRendererElementPropertyChanged; } } }
3. 修正OnRendererElementPropertyChanged中的属性判断
处理MenuContent的Title属性和继承自ContentView的Content属性:
void OnRendererElementPropertyChanged(object sender, PropertyChangedEventArgs e) { var menuContent = (MenuContent)sender; if (e.PropertyName == MenuContent.TitleProperty.PropertyName) { _toolBar.Title = menuContent.Title; } else if (e.PropertyName == ContentView.ContentProperty.PropertyName) { // 更新布局并刷新原生视图 _content.Tracker.UpdateLayout(); _content.View.Invalidate(); } }
4. 确保MenuContent的BindableProperty正确触发事件
修改MenuContent.TitleProperty,添加默认值和属性变更回调,确保事件正常触发:
public static readonly BindableProperty TitleProperty = BindableProperty.Create( nameof(Title), typeof(string), typeof(MenuContent), defaultValue: string.Empty, propertyChanged: (bindable, oldValue, newValue) => { var content = (MenuContent)bindable; content.OnPropertyChanged(nameof(Title)); });
内容的提问来源于stack exchange,提问作者CSharp Noob
相关产品推荐
相关产品推荐

