我在将 Xamarin.Forms 渲染器转换为 .NET MAUI 处理程序时遇到重大问题。
使用
.AddCompatibilityRenderer
注册不是一个选项,因为仅显示 空白页面(众所周知,针对整个页面的兼容性渲染器不可用,因为它们不起作用)。此外,我的渲染器源自 IVisualElementRenderer
接口和添加特定功能的 自定义控制器。 此接口允许将类导出为渲染器并在 Xamarin.Forms 中使用它。但是,我在 .NET MAUI 中没有找到等效的接口。我考虑过使用 IViewHandler
,但这似乎过于复杂,因为与 XF 相比,需要实现的东西要多得多,而且它通过 MAUI 中的多个继承级别分布在 ViewHandler 周围。这是一个大问题,因为我的应用程序使用了相当多的渲染器,所以我需要一种通用方法来转换它们。
作为一个示例,这是
StreamListPageRenderer
,它用于在 iOS 上正确渲染页面。它继承自 CancellationTokenViewController
并呈现 StreamListNativePage
,这是 iOS 上应该显示的本机页面 - 它是此控制器的视图。此外,渲染器还实现了 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; }
}
澄清一下,这些渲染器在 XF 中运行完美,没有任何问题。
提前感谢您提供有关如何处理此问题的任何有用建议。