danny
danny

Reputation: 21

How to convert an old Xamarin.Forms renderer derived from IVisualElementRenderer with a custom controller to a .NET MAUI handler?

I'm having a major issue with converting Xamarin.Forms renderers to .NET MAUI handlers.

Using registration with .AddCompatibilityRenderer is not an option because only a blank page is displayed (it’s well known that compatibility renderers targeting an entire page are not usable because they don’t work). Additionally, my renderer is derived from the IVisualElementRenderer interface and a custom controller that adds specific functionality. This interface allows exporting the class as a renderer and using it in Xamarin.Forms. However, I haven't found an equivalent interface in .NET MAUI. I thought about using IViewHandler, but that seems overly complex because there’s much more to implement compared to XF, and it’s spread around the ViewHandler through multiple levels of inheritance in MAUI. This is a big issue because my application uses quite a few of these renderers, so I need a universal way to convert them.

As an example, here’s the StreamListPageRenderer, which serves to render the page correctly on iOS. It inherits from CancellationTokenViewController and renders StreamListNativePage, which is the native page that should be displayed on iOS — it's a View of this Controller. Additionally, the renderer implements the IVisualElementRenderer interface.

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

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; }
}

To clarify, these renderers worked perfectly in XF without any issues.

Thank you in advance for any helpful suggestions on what to do with this.

Upvotes: -1

Views: 108

Answers (1)

danny
danny

Reputation: 21

Fixed - iOS compatible renderers started working using the new .NET 8.0.403. No need to convert to a handler.

Upvotes: 0

Related Questions