You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义渲染器子元素更新异常:MenuContent属性变更不触发PropertyChanged

问题描述

我正在开发类似TabPage的自定义控件CSView,它继承自View,包含一组CSMenuItem;每个CSMenuItem继承自BaseMenuItem,拥有继承自ContentView的MenuContent,结构如下:

  • CSView
    • CSMenuItem
      • MenuContent
        • Content
    • CSMenuItem
      • MenuContent
        • Content

现在遇到的问题是:在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不触发的核心原因

  1. 未监听CSMenuItem.Content属性的变更:原有代码只在CreateNativeControl时初始化了一次MenuContent的渲染器,当CSMenuItem.Content被替换时,渲染器没有更新,也没有绑定新MenuContent的事件。
  2. 属性判断错误:MenuContent继承自ContentView,其Content属性属于ContentView的内置BindableProperty,原有代码中判断MenuContent.ContentProperty是错误的,应该用ContentView.ContentProperty。
  3. 事件绑定/解绑逻辑不完整:旧的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 09:30:52