Maui icon indicating copy to clipboard operation
Maui copied to clipboard

[BUG] Expander is not working in ios inside collectionview

Open NetCSDev opened this issue 1 year ago • 16 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Did you read the "Reporting a bug" section on Contributing file?

  • [X] I have read the "Reporting a bug" section on Contributing file: https://github.com/CommunityToolkit/Maui/blob/main/CONTRIBUTING.md#reporting-a-bug

Current Behavior

The expander control in iOS is not functioning as expected when placed inside a collection view. Specifically, only the first item of the collection view is affected when you click on any other item. This issue started occurring after upgrading the toolkit from 5.2.0 to 7.0.1 and .NET from 7.0 to 8.0.

Expected Behavior

When expander is placed inside collectionview, the item which is tapped or clicked must expand or collapsed. In android, it is working fine but has issue on iOS.

Steps To Reproduce

Create a collection view add expanded control inside it Run the application in ios

image

Link to public reproduction project repository

https://github.com/dotnet/maui/issues/20004

Environment

- .NET MAUI CommunityToolkit: 7.0.1
- OS: iOS 14.2.1 (23C71)
- .NET MAUI:.net 8

Anything else?

na

NetCSDev avatar Jan 30 '24 06:01 NetCSDev

Hi @NetCSDev. We have added the "needs reproduction" label to this issue, which indicates that we cannot take further action. This issue will be closed automatically in 5 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

ghost avatar Jan 30 '24 09:01 ghost

I tried to reproduce this bug in MauiCommunityToolkit sample app and yes expander is not working as it supposed to work in IOS. android-expander.webm

https://github.com/CommunityToolkit/Maui/assets/98330987/7e257255-c2d9-47ee-a386-6248d0f61af9

TRybina132 avatar Jan 31 '24 20:01 TRybina132

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 3 days. It will be closed if no further activity occurs within 2 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

ghost avatar Feb 05 '24 00:02 ghost

@TRybina132 Thanks for reproducing the bug.

NetCSDev avatar Feb 05 '24 07:02 NetCSDev

+1. Having to resort to using a ListView with a custom ResizingViewCell.

bradencohen avatar Feb 23 '24 20:02 bradencohen

@bradencohen would you be able to post the code for your ListView w/ custom ResizingViewCell for us to use in the interrim?

RedZone908 avatar Mar 20 '24 21:03 RedZone908

Certainly!

It really isn't pretty, & may be over-done for my specific use-case, so feel free to take what you need.

First, a custom handler is required for iOS ListView:

internal class ListViewHandler : Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer
{
    public event Action ViewCellSizeChangedEvent
    {
        add { _viewCellSizeChangedEvent += value; }
        remove { _viewCellSizeChangedEvent -= value; }
    }
    private Action _viewCellSizeChangedEvent;

    public ListViewHandler()
    {
        ViewCellSizeChangedEvent += ListViewHandler_ViewCellSizeChangedEvent;
    }

    private void ListViewHandler_ViewCellSizeChangedEvent()
    {
        var tv = Control as UITableView;
        if ( tv == null ) return;
        tv.BeginUpdates();
        tv.EndUpdates();
    }

    public void RaiseViewCellSizeChangedEvent()
    {
        _viewCellSizeChangedEvent?.Invoke();
    }
}

Make sure to register in MauiProgram.

Second, I made an ExpanderViewCell:

public class ExpanderViewCell : Microsoft.Maui.Controls.ViewCell
{
    public static event Action ViewCellSizeChangedEvent;

    public static readonly BindableProperty ExpandedViewProperty =
        BindableProperty.Create( nameof( ExpandedView ), typeof( View ), typeof( ExpanderViewCell ) );

    public static readonly BindableProperty HeaderViewProperty =
        BindableProperty.Create( nameof( HeaderView ), typeof( View ), typeof( ExpanderViewCell ) );

    public static readonly BindableProperty IsExpandedProperty =
        BindableProperty.Create( nameof( IsExpanded ), typeof( bool ), typeof( ExpanderViewCell ), false );

    public static readonly BindableProperty DividerViewProperty =
        BindableProperty.Create( nameof( DividerView ), typeof( View ), typeof( ExpanderViewCell ) );

    public View ExpandedView
    {
        get { return ( View ) GetValue( ExpandedViewProperty ); }
        set { SetValue( ExpandedViewProperty, value ); }
    }

    public View HeaderView
    {
        get { return ( View ) GetValue( HeaderViewProperty ); }
        set { SetValue( HeaderViewProperty, value ); }
    }

    public View DividerView
    {
        get { return ( View ) GetValue( DividerViewProperty ); }
        set { SetValue( DividerViewProperty, value ); }
    }

    public bool IsExpanded
    {
        get { return ( bool ) GetValue( IsExpandedProperty ); }
        set { SetValue( IsExpandedProperty, value ); }
    }

    public static readonly BindableProperty UpdateExpandedOnTappedProperty = BindableProperty.CreateAttached(
        propertyName: "UpdateExpandedOnTapped",
        returnType: typeof( bool ),
        declaringType: typeof( ExpanderViewCell ),
        defaultValue: false,
        propertyChanged: OnUpdateExpandedOnTappedChanged );

    public static void OnUpdateExpandedOnTappedChanged( BindableObject bindable, object oldValue, object newValue )
    {
        // Once the view is attached to the visual tree, we can add the gesture recognizer
        if ( ( bindable is not View view ) )
        {
            return;
        }

        view.Effects.Add( new OnAttachedListenerEffect
        {
            OnAttachedToWindow = () =>
            {
                var closestExpanderViewCell = view.ClosestAncestor<ExpanderViewCell>();
                if ( closestExpanderViewCell is not null )
                {
                    if ( newValue is bool updateExpandedOnTapped )
                    {
                        if ( updateExpandedOnTapped )
                        {
                            view.GestureRecognizers.Add( new TapGestureRecognizer
                            {
                                Command = new Command( () => closestExpanderViewCell.IsExpanded = !closestExpanderViewCell.IsExpanded )
                            } );
                        }
                    }
                }
            }
        }
        );
    }

    public static void SetUpdateExpandedOnTapped( BindableObject bindable, bool value ) => bindable.SetValue( UpdateExpandedOnTappedProperty, value );
   
    public static bool GetUpdateExpandedOnTapped( BindableObject bindable ) => ( bool ) bindable.GetValue( UpdateExpandedOnTappedProperty );

    private void UpdateExpandedOnTapped( object sender, EventArgs e )
    {
        IsExpanded = !IsExpanded;
    }

    protected override void OnParentSet()
    {
        base.OnParentSet();

        if ( Parent != null )
        {
            ConfigureContent();
        }
    }

    protected override void OnPropertyChanged( [CallerMemberName] string propertyName = null )
    {
        base.OnPropertyChanged( propertyName );

        if ( propertyName == nameof( IsExpanded ) )
        {
            OnExpandedChanged();
        }
    }
    private void ConfigureContent()
    {
        var expanderGrid = new Grid
        {
            RowDefinitions =
            {
                new RowDefinition( GridLength.Auto ),
                new RowDefinition( GridLength.Auto ),
                new RowDefinition( GridLength.Auto ),
            }
        };

        if ( HeaderView is not null )
        {
            expanderGrid.Add( HeaderView, 0, 0 );
            HeaderView.GestureRecognizers.Add( new TapGestureRecognizer
            {
                Command = new Command( () => IsExpanded = !IsExpanded )
            } );
        }

        if ( ExpandedView is not null )
        {
            expanderGrid.Add( ExpandedView, 0, 1 );
            ExpandedView.SetBinding( View.IsVisibleProperty, new Binding( nameof( IsExpanded ), source: this ) );
        }

        if ( DividerView is not null )
        {
            expanderGrid.Add( DividerView, 0, 2 );
        }

        View = expanderGrid;
    }

    private void OnExpandedChanged()
    {
        if ( Parent is not ListView listView )
        {
            return;
        }

#if IOS
        if( listView.Handler is Handlers.ListViewHandler listViewHandler )
        {
            listViewHandler.RaiseViewCellSizeChangedEvent();
        }
#endif
    }
}

You probably won't need all of this class. I added an attached property to the view cell (UpdateExpandedOnTapped) in cases where I need to specify the exact view that triggers expanding/not. That property an OnAttachedListener effect that we've found useful to trigger an event when a view gets attached to the window.

Then usage is like:

<DataTemplate x:Key="PersonSearchResultItemNoDivider">
    <local:ExpanderViewCell>
        <local:ExpanderViewCell.ExpandedView>
            
        </local:ExpanderViewCell.ExpandedView>
        <local:ExpanderViewCell.HeaderView>

        </local:ExpanderViewCell.HeaderView>
    </Rock:ExpanderViewCell>
</DataTemplate>

Then I just set HasUnevenRows to true on the ListView.

bradencohen avatar Mar 20 '24 23:03 bradencohen

related https://github.com/dotnet/maui/issues/21141

PureWeen avatar May 07 '24 14:05 PureWeen

@bradencohen do you mind showing the code for OnAttachedListener?

Baraiboapex avatar Jun 18 '24 16:06 Baraiboapex

Is it just me that is finding it astonishing that we are repeatedly getting issues like this? FIVE MONTHS after being reported, its not fixed? Are we back to 'not fixing issues in 8 , we're developing .net 9 now' ? It feels like all I'm doing while trying to develop, is get updates to fix bugs, then later find new bugs in the new releases which I now have to find different solutions for, instead of working on the actual app. I guess it's my fault for not understanding enough so that I can just take bradens solution.... I'll try another layout...

DeFeBe avatar Jun 21 '24 13:06 DeFeBe

@DeFeBe given that we all do this for free and in our spare time I don't believe there shouldn't be any expectation on when things get fixed.

This is a community toolkit which means it's built by the community, for the community. We openly accept PRs. If something is urgent then feel free to submit a fix.

bijington avatar Jun 21 '24 13:06 bijington

It may be a "community toolkit", but microsoft direct people to it for functionality. Maui is being pushed as 'make your projects in it', but it relies on people giving up their free time. This just seems utterly astonishing and is of course aimed at microsoft management. Do we bin Maui and start again in something else or spend thousands on other companies sdk's (which aren't exactly bug free) ? As for a PR, this is in a PR, its a 6 months open bug which I found whilst trying to address non functionality in the ios version. It would be better if it was removed rather than be broken.

DeFeBe avatar Jul 02 '24 09:07 DeFeBe

Still having this Issue, any updates?

OudomMunint avatar Aug 20 '24 00:08 OudomMunint

We’ll see if new CollectionView handler fixes the issue.

VladislavAntonyuk avatar Aug 20 '24 04:08 VladislavAntonyuk