如何将基于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
相关产品推荐
相关产品推荐

