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

MAUI迁移:自定义TableView FooterText实现及Renderer相关疑问

迁移Xamarin Forms自定义TableView至MAUI的问题

我们正在将Xamarin Forms(XF)应用迁移至MAUI,由于XF与MAUI原生均不支持为TableSection或Table添加Footer,因此需要迁移自定义TableView控件及渲染器。此前认为MAUI已移除Renderer,但发现其仍存在于Handlers.Compatibility命名空间中。

现有XF自定义Renderer代码如下:

public class BreatheTableViewRenderer : TableViewRenderer
{
    /// <summary>
    /// Raises the <see cref="E:ElementChanged" /> event.
    /// </summary>
    /// <param name="e">The <see cref="ElementChangedEventArgs{TableView}"/> instance containing the event data.</param>
    protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
    {
        base.OnElementChanged(e);
        if (this.Control == null)
        {
            return;
        }

        var tableView = this.Control as UITableView;
        var formsTableView = this.Element as TableView;
        tableView.WeakDelegate = new BreatheTableViewModelRenderer(formsTableView);
    }

    private class BreatheTableViewModelRenderer : TableViewModelRenderer
    {
        private BreatheTableView view;

        public BreatheTableViewModelRenderer(TableView model)
            : base(model)
        {
            this.view = model as BreatheTableView;
        }

        protected BreatheTableView BreatheTableView { get => this.view; private set => this.view = value; }

        public override UIView GetViewForFooter(UITableView tableView, nint section)
        {
            // Do we have a TableView Footer and if so is this is the last section
            if (this.BreatheTableView.FooterText != null && section == tableView.NumberOfSections() - 1)
            {
                // ... if it is use that last section to display the footer
                return this.GetViewForTableFooter(this.BreatheTableView.FooterText);
            }

            TableSection tblSection = this.View.Root[(int)section];

            string sectionFooterText = BreatheTableView.GetSectionFooterText(tblSection);

            if (string.IsNullOrEmpty(sectionFooterText) == false)
            {
                return this.GetViewForTableFooter(sectionFooterText);
            }

            return null;
        }

        private UIView GetViewForTableFooter(string text)
        {
            var footerView = new UITextView()
            {
                Text = text,
                Font = UIFont.PreferredFootnote,
                TextContainerInset = new UIEdgeInsets(5, 10, 5, 10),
                TextColor = UIColor.LightGray,
                BackgroundColor = UIColor.Clear,
                Editable = false,
            };

            if (this.BreatheTableView.FooterAlignment == TextAlignment.Start)
            {
                footerView.TextAlignment = UITextAlignment.Left;
            }
            else if (this.BreatheTableView.FooterAlignment == TextAlignment.Center)
            {
                footerView.TextAlignment = UITextAlignment.Center;
            }
            else if (this.BreatheTableView.FooterAlignment == TextAlignment.End)
            {
                footerView.TextAlignment = UITextAlignment.Right;
            }

            return footerView;
        }
    }
}

针对你提出的两个疑问,解答如下:


1. 如何自定义实现GetViewForFooter?

在MAUI中有两种可行方案,分别适配迁移过渡和原生扩展需求:

方案一:使用兼容性Renderer(复用XF逻辑)

MAUI保留了XF风格的兼容性Renderer,可直接基于原有代码改造,核心逻辑无需大幅修改:

  1. 引用兼容性命名空间Microsoft.Maui.Controls.Handlers.Compatibility及iOS专属命名空间Microsoft.Maui.Controls.Handlers.Compatibility.iOS
  2. 自定义Renderer继承TableViewRenderer,在OnElementChanged中替换原生UITableView的WeakDelegate
  3. 复用原有BreatheTableViewModelRenderer的逻辑,仅调整命名空间引用

改造后核心代码:

using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Handlers.Compatibility.iOS;
using UIKit;

public class BreatheTableViewRenderer : TableViewRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
    {
        base.OnElementChanged(e);
        if (Control == null) return;

        var tableView = Control as UITableView;
        var formsTableView = Element as BreatheTableView;
        tableView.WeakDelegate = new BreatheTableViewModelRenderer(formsTableView);
    }

    private class BreatheTableViewModelRenderer : TableViewModelRenderer
    {
        private readonly BreatheTableView _view;

        public BreatheTableViewModelRenderer(TableView model) : base(model)
        {
            _view = model as BreatheTableView;
        }

        protected BreatheTableView BreatheTableView => _view;

        public override UIView GetViewForFooter(UITableView tableView, nint section)
        {
            // 完全复用原有Footer逻辑
            if (BreatheTableView.FooterText != null && section == tableView.NumberOfSections() - 1)
            {
                return GetViewForTableFooter(BreatheTableView.FooterText);
            }

            TableSection tblSection = View.Root[(int)section];
            string sectionFooterText = BreatheTableView.GetSectionFooterText(tblSection);

            if (!string.IsNullOrEmpty(sectionFooterText))
            {
                return GetViewForTableFooter(sectionFooterText);
            }

            return null;
        }

        private UIView GetViewForTableFooter(string text)
        {
            var footerView = new UITextView()
            {
                Text = text,
                Font = UIFont.PreferredFootnote,
                TextContainerInset = new UIEdgeInsets(5, 10, 5, 10),
                TextColor = UIColor.LightGray,
                BackgroundColor = UIColor.Clear,
                Editable = false,
            };

            switch (BreatheTableView.FooterAlignment)
            {
                case TextAlignment.Start:
                    footerView.TextAlignment = UITextAlignment.Left;
                    break;
                case TextAlignment.Center:
                    footerView.TextAlignment = UITextAlignment.Center;
                    break;
                case TextAlignment.End:
                    footerView.TextAlignment = UITextAlignment.Right;
                    break;
            }

            return footerView;
        }
    }
}

在MauiProgram.cs中注册兼容性Renderer:

builder.ConfigureMauiHandlers(handlers =>
{
#if IOS
    handlers.AddCompatibilityRenderer(typeof(BreatheTableView), typeof(BreatheTableViewRenderer));
#endif
});

方案二:使用MAUI原生Handler(推荐)

MAUI推荐使用Handler替代传统Renderer,更轻量灵活:

  1. 自定义Handler继承TableViewHandler
  2. 在ConnectHandler方法中为原生UITableView设置自定义Delegate,实现GetViewForFooter逻辑

示例代码:

using Microsoft.Maui.Handlers;
using UIKit;

public class BreatheTableViewHandler : TableViewHandler
{
    protected override void ConnectHandler(UITableView platformView)
    {
        base.ConnectHandler(platformView);
        platformView.WeakDelegate = new BreatheTableViewDelegate(this);
    }

    private class BreatheTableViewDelegate : UITableViewDelegate
    {
        private readonly BreatheTableViewHandler _handler;

        public BreatheTableViewDelegate(BreatheTableViewHandler handler)
        {
            _handler = handler;
        }

        public override UIView GetViewForFooter(UITableView tableView, nint section)
        {
            var breatheTableView = _handler.VirtualView as BreatheTableView;
            if (breatheTableView == null) return null;

            if (breatheTableView.FooterText != null && section == tableView.NumberOfSections() - 1)
            {
                return GetViewForTableFooter(breatheTableView.FooterText, breatheTableView.FooterAlignment);
            }

            TableSection tblSection = breatheTableView.Root[(int)section];
            string sectionFooterText = BreatheTableView.GetSectionFooterText(tblSection);

            if (!string.IsNullOrEmpty(sectionFooterText))
            {
                return GetViewForTableFooter(sectionFooterText, breatheTableView.FooterAlignment);
            }

            return null;
        }

        private UIView GetViewForTableFooter(string text, TextAlignment alignment)
        {
            var footerView = new UITextView()
            {
                Text = text,
                Font = UIFont.PreferredFootnote,
                TextContainerInset = new UIEdgeInsets(5, 10, 5, 10),
                TextColor = UIColor.LightGray,
                BackgroundColor = UIColor.Clear,
                Editable = false,
            };

            switch (alignment)
            {
                case TextAlignment.Start:
                    footerView.TextAlignment = UITextAlignment.Left;
                    break;
                case TextAlignment.Center:
                    footerView.TextAlignment = UITextAlignment.Center;
                    break;
                case TextAlignment.End:
                    footerView.TextAlignment = UITextAlignment.Right;
                    break;
            }

            return footerView;
        }
    }
}

在MauiProgram.cs中注册Handler:

builder.ConfigureMauiHandlers(handlers =>
{
#if IOS
    handlers.AddHandler(typeof(BreatheTableView), typeof(BreatheTableViewHandler));
#endif
});

2. MAUI中的Renderer是否已被移除?

MAUI并未完全移除Renderer,而是提供了两种扩展机制:

  • 兼容性Renderer:保留在Microsoft.Maui.Controls.Handlers.Compatibility命名空间下,专门用于兼容XF现有Renderer代码,降低迁移成本。
  • 原生Handler:MAUI主推的新扩展方式,相比传统Renderer更轻量化、模块化,是MAUI原生扩展的标准方案。

内容的提问来源于stack exchange,提问作者Pat Long - Munkii Yebee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:28:11