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

如何将基于IVisualElementRenderer的Xamarin.Forms渲染器迁移至.NET MAUI Handler?

将Xamarin.Forms渲染器迁移到.NET MAUI Handler的问题

我在将Xamarin.Forms渲染器转换为.NET MAUI Handler时遇到了严重问题:

  • 使用.AddCompatibilityRenderer注册完全不可行,只会显示空白页面(众所周知,针对整个页面的兼容性渲染器无法正常工作)。
  • 原渲染器继承自IVisualElementRenderer接口和自定义控制器以实现特定功能,该接口在Xamarin.Forms中可将类导出为渲染器,但在.NET MAUI中找不到等效接口。
  • 考虑过使用IViewHandler,但相比XF的实现过于复杂,且MAUI中ViewHandler的逻辑通过多层继承分散实现。由于应用中大量使用此类渲染器,急需一种通用的转换方案。

以下是iOS平台下的示例实现:StreamListPageRenderer继承自CancellationTokenViewController,负责渲染原生页面StreamListNativePage(作为控制器的View),同时实现IVisualElementRenderer接口。

StreamListPageRenderer

class StreamListPageRenderer : CancellationTokenViewController<StreamListNativePage>, IVisualElementRenderer
{
    private ViewSources.StreamViewSource tableViewSource;
    private bool disposed;

    public async override void ViewDidLoad()
    {
        base.ViewDidLoad();
        this.ProgressColor = UIColor.Black;
        this.tableViewSource = new ViewSources.StreamViewSource(this.mainView.TableView);
        this.mainView.TableView.Source = this.tableViewSource;
        this.ViewModel.PropertyChanged += this.ViewModel_PropertyChanged;
        if (this.Page != null)
        {
            await this.Page.OnPageLoaded();
        }
    }

    public override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);
        //We need to subscribe to the event in ViewDidLoad, but if disappearing is called, we need to re-subscribe it here.
        //This ensures it won’t be subscribed twice.
        this.ViewModel.PropertyChanged -= this.ViewModel_PropertyChanged;
        this.ViewModel.PropertyChanged += this.ViewModel_PropertyChanged;
        this.tableViewSource.ItemTouch += this.TableViewSource_ItemTouch;
        this.tableViewSource.SetData(this.ViewModel.Items, this.mainView.TableView);
        this.mainView.RefreshControl.ValueChanged += this.RefreshControl_ValueChanged;
    }

    private async void RefreshControl_ValueChanged(object sender, EventArgs e)
    {
        await this.ViewModel.RefreshData(true);
        this.mainView.RefreshControl.EndRefreshing();
    }

    private void TableViewSource_ItemTouch(object sender, ViewModels.ItemViewModels.StreamItemViewModel e)
    {
        this.ViewModel.SelectedItem = e;
    }

    private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(this.ViewModel.IsInBackgroundWorking))
        {
            this.SetBackgroundWorking(this.ViewModel.IsInBackgroundWorking, this.ViewModel.LoadingText);
        }
        else if (e.PropertyName == nameof(this.ViewModel.LoadingText))
        {
            this.SetBackgroundWorking(this.ViewModel.IsInBackgroundWorking, this.ViewModel.LoadingText);
        }
        else if (e.PropertyName == nameof(this.ViewModel.Items))
        {
            this.tableViewSource.SetData(this.ViewModel.Items, this.mainView.TableView);
        }
    }

    public override void ViewDidDisappear(bool animated)
    {
        base.ViewDidDisappear(animated);
        this.UnsubscribeEvents();
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        this.disposed = true;
        this.UnsubscribeEvents();
    }

    protected override UIView RootView
    {
        get
        {
            if (!this.disposed)
                return base.RootView;
            return null;
        }
    }

    private void UnsubscribeEvents()
    {
        this.ViewModel.PropertyChanged -= this.ViewModel_PropertyChanged;
        this.tableViewSource.ItemTouch -= this.TableViewSource_ItemTouch;
        this.mainView.RefreshControl.ValueChanged -= this.RefreshControl_ValueChanged;
    }

    public StreamListPage Page { get; private set; }
    public StreamListViewModel ViewModel => (this.Page.BindingContext as StreamListViewModel);

    #region Elements added for Xamarin.Forms compatibility (implementing IVisualElementRenderer)

    public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
    public VisualElement Element { get; private set; }
    public UIView NativeView => this.View;
    public UIViewController ViewController => this;
    public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) => NativeView.GetSizeRequest(widthConstraint, heightConstraint);
    public void SetElementSize(Size size) => Element.Layout(new Rect(Element.X, Element.Y, size.Width, size.Height));

    public void SetElement(VisualElement element)
    {
        VisualElement oldElement = Element;
        Element = element;
        Page = (this.Element as StreamListPage);
        UpdateTitle();
        this.ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(oldElement, element));
    }

    private void UpdateTitle()
    {
        if (!string.IsNullOrWhiteSpace((this.Element as Page)?.Title))
            NavigationItem.Title = (this.Element as Page).Title;
    }

    #endregion
}

CancellationTokenViewController

该控制器进一步继承自BackgroundWorkingViewController,负责正确显示加载布局和加载指示器。

public class CancellationTokenViewController<T> : BackgroundWorking.BackgroundWorkingViewController<T> where T : UIView
{
    private CancellationTokenSource cancelTokenSource;
    private Task<bool> loadingTask;
    private bool loadingTaskWasCanceled;

    public async override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);
        await this.CheckAndRefreshCanceledOperations();
    }

    public override void ViewDidDisappear(bool animated)
    {
        base.ViewDidDisappear(animated);
        this.Cancel();
    }

    protected async Task<bool> LoadData()
    {
        this.loadingTaskWasCanceled = false;
        this.loadingTask = this.LoadData();
        return await loadingTask;
    }

    protected virtual Task<bool> LoadDataInternal() => Task.FromResult<bool>(true);


    /// <summary>
    /// Allows cancellation of the operation
    /// </summary>
    protected CancellationToken CancelToken
    {
        get
        {
            if (this.cancelTokenSource == null)
                this.InitCancelTokenSource();
            return this.cancelTokenSource.Token;
        }
    }

    /// <summary>
    /// Cancels the operation
    /// </summary>
    private void Cancel()
    {
        ErrorLog.Instance.StartAddRecord(ErrorTypes.MESSAGE, $"this.cancelTokenSource != null = {this.cancelTokenSource != null}", "CancellationViewModel.Cancel() called");
        if (this.cancelTokenSource != null && !this.cancelTokenSource.IsCancellationRequested && this.cancelTokenSource.Token.CanBeCanceled)
        {
            if (loadingTask != null && !loadingTask.IsCompleted)
                loadingTaskWasCanceled = true;

            this.cancelTokenSource.Cancel();
            this.cancelTokenSource = null;
        }
    }

    /// <summary>
    /// Checks if the previous loading operation was canceled, and if so, restarts it
    /// </summary>
    /// <returns></returns>
    private async Task CheckAndRefreshCanceledOperations()
    {
        ErrorLog.Instance.StartAddRecord(ErrorTypes.MESSAGE, $"loadingTaskWasCanceled-{loadingTaskWasCanceled}", "CheckAndRefreshCanceledOperations() called");
        if (loadingTaskWasCanceled)
        {
            this.ResetCancelationTokenSource();
            await this.LoadData();
        }
    }

    /// <summary>
    /// Initializes a new Token
    /// </summary>
    private void InitCancelTokenSource()
    {
        ErrorLog.Instance.StartAddRecord(ErrorTypes.MESSAGE, "CancellationViewModel.InitCancelTokenSource() called");
        this.cancelTokenSource = new CancellationTokenSource();
    }

    /// <summary>
    /// Resets the token so the operation can no longer be stopped
    /// </summary>
    private void ResetCancelationTokenSource()
    {
        this.cancelTokenSource = null;
    }
}

StreamListNativePage

class StreamListNativePage : UIView
{
    public StreamListNativePage()
    {
        this.TableView = this.CreateTableView();
    }

    private TableView CreateTableView()
    {
        var tableView = new TableView();
        this.RefreshControl = new UIRefreshControl();
        tableView.RefreshControl = this.RefreshControl;
        tableView.SeparatorStyle = UITableViewCellSeparatorStyle.None;
        tableView.AutoRowHeight = true;
        tableView.EmptyText = "StreamList_EmptyText".LocalizeResx();
        tableView.EmptyLabelsColor = ((Color)App.Current.Resources["PrimaryBackgroundTextColor"]).ToUIColor();
        this.AddSubview(tableView);
        tableView.LeadingAnchor.ConstraintEqualTo(this.LeadingAnchor, 0).Active = true;
        tableView.TopAnchor.ConstraintEqualTo(this.TopAnchor, 0).Active = true;
        tableView.TrailingAnchor.ConstraintEqualTo(this.TrailingAnchor, 0).Active = true;
        tableView.BottomAnchor.ConstraintEqualTo(this.BottomAnchor, 0).Active = true;
        return tableView;
    }

    public TableView TableView { get; }
    public UIRefreshControl RefreshControl { get; private set; }
}

注:这些渲染器在Xamarin.Forms环境下运行完全正常。

求各位提供可行的解决方案,感谢!

内容的提问来源于stack exchange,提问作者danny

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 10:29:55