xamarin / Xamarin.Forms

Xamarin.Forms is no longer supported. Migrate your apps to .NET MAUI.
https://aka.ms/xamarin-upgrade
Other
5.62k stars 1.87k forks source link

[Bug, iOS] Control event registration in custom `ListViewRenderer` child class throws an exception #5876

Open gtbuchanan opened 5 years ago

gtbuchanan commented 5 years ago

Description

Control event registration in custom ListViewRenderer child class throws an InvalidOperationException. In my particular case, I'm trying to listen on UITableView.Scrolled because there is no equivalent Xamarin Forms property/event I can use to track scroll position.

The ListViewRenderer class itself sets Control.Source, which is what I believe is causing the problem. There may be a workaround for this I just don't know about. I'd like to avoid implementing my own ListViewRenderer from scratch.

Related to #2619

Related Xamarin Forum Post

Steps to Reproduce

  1. Create custom renderer inheriting from ListViewRenderer
  2. Register event delegate in OnElementChanged
  3. Add ListView to page
  4. Launch app and exception is thrown

Expected Behavior

Event registration works unless I manually override UITableView.Delegate.

Actual Behavior

System.InvalidOperationException: Event registration is overwriting existing delegate. Either just use events or your own delegate: Xamarin.Forms.Platform.iOS.ListViewRenderer+ListViewDataSource UIKit.UIScrollView+_UIScrollViewDelegate

Basic Information

Reproduction Link

ListViewDelegateIssue.zip

gtbuchanan commented 5 years ago

I came up with a workaround using a proxy. Not really an ideal solution, in my opinion. Nonetheless, it seems to work without error.

internal sealed class CustomListViewRenderer : ListViewRenderer
{
    private new CustomListView Element => (CustomListView)base.Element;

    protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
    {
        base.OnElementChanged(e);
        if (Element == null || Control == null) return;
        Control.Source = new ListViewSource(Control.Source, Element);
    }

    private sealed class ListViewSource : UITableViewSourceProxy
    {
        private CustomListView ListView { get; }

        public ListViewSource(UITableViewSource inner, CustomListView listView) : base(inner) => ListView = listView;

        public override void Scrolled(UIScrollView scrollView)
        {
            base.Scrolled(scrollView);
            var percentage = (scrollView.ContentOffset.Y + scrollView.Bounds.Height) / scrollView.ContentSize.Height;
            ListView.ScrollYPercentage = Math.Min(Math.Round(percentage, 3), 1);
        }
    }
}

internal abstract class UITableViewSourceProxy : UITableViewSource
{
    private UITableViewSource Inner { get; }

    protected UITableViewSourceProxy(UITableViewSource inner) => Inner = inner;

    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) => Inner.GetCell(tableView, indexPath);

    public override nint RowsInSection(UITableView tableview, nint section) => Inner.RowsInSection(tableview, section);

    public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate) => Inner.DraggingEnded(scrollView, willDecelerate);

    public override void DraggingStarted(UIScrollView scrollView) => Inner.DraggingStarted(scrollView);

    public override nfloat GetHeightForHeader(UITableView tableView, nint section) => Inner.GetHeightForHeader(tableView, section);

    public override UIView GetViewForHeader(UITableView tableView, nint section) => Inner.GetViewForHeader(tableView, section);

    public override void HeaderViewDisplayingEnded(UITableView tableView, UIView headerView, nint section) =>
        Inner.HeaderViewDisplayingEnded(tableView, headerView, section);

    public override nint NumberOfSections(UITableView tableView) => Inner.NumberOfSections(tableView);

    public override void RowDeselected(UITableView tableView, NSIndexPath indexPath) => Inner.RowDeselected(tableView, indexPath);

    public override void RowSelected(UITableView tableView, NSIndexPath indexPath) => Inner.RowSelected(tableView, indexPath);

    public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) =>
        Inner.WillDisplay(tableView, cell, indexPath);

    public override void Scrolled(UIScrollView scrollView) => Inner.Scrolled(scrollView);

    public override string[] SectionIndexTitles(UITableView tableView) => Inner.SectionIndexTitles(tableView);

    protected override void Dispose(bool disposing) => Inner.Dispose();
}
samhouts commented 5 years ago

@hartez Did we ever decide to add the Scrolling event to #3172? @gtbuchanan Would a Scrolling event that reports scroll position meet your needs? Thanks!

gtbuchanan commented 5 years ago

@samhouts Yes, as long as it also reported the total scroll/content height as well. This is currently my only use of a custom ListViewRenderer.

jingliancui commented 5 years ago

Met the same error on ios platform while I want override some method like on android platform

JTOne123 commented 5 years ago

Another one workaround

[assembly: ExportRenderer(typeof(MyListView), typeof(MyListViewRender))]
namespace MyApp.Mobile.iOS.CustomRenderers
{
    public class MyListViewRender : ListViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
        {
            base.OnElementChanged(e);

            var scalableListView = Element as MyListView;

            if (Control == null || scalableListView == null)
            {
                return;
            }

            var tvDelegate = new TableViewDelegate();

            Control.Delegate = tvDelegate;

            tvDelegate.OnDraggingStarted += (s, ev) =>
            {
                scalableListView.InvokeScrollEventDraggingStarted();
            };
        }
    }

    public class TableViewDelegate : UITableViewDelegate
    {
        public event EventHandler OnDraggingStarted;

        public override void DraggingStarted(UIScrollView scrollView)
        {
            OnDraggingStarted?.Invoke(scrollView, null);
        }
    }
}
gtbuchanan commented 4 years ago

I've confirmed this is no longer an issue in Xamarin.Forms 4.6.0.847. You seem to be able to use a custom Delegate again. There is also CollectionView now which has the Scrolled event that could possibly meet my needs without a native renderer.

gtbuchanan commented 4 years ago

Turns out I spoke too soon (and forgot what the original issue was). While you can set Delegate without error, doing so appears to break item selection. Attempting to use the Scrolled event on UIScrollView still results in the same error from the OP. Unfortunately, my workaround using UITableViewSourceProxy now results in a Foundation.You_Should_Not_Call_base_In_This_Method exception. I added try...catch statements to swallow this exception since it doesn't matter in my use case and that seemed to help. This will work for now while I figure out CollectionView rendering issues I'm having on UWP.