GrapeCity / ComponentOne-MAUI-Samples

3 stars 4 forks source link

iOS.Grid - Issue with Cell Customization in FlexGrid - Possible Virtualization Problem #5

Closed am-kh closed 11 months ago

am-kh commented 11 months ago

Hello,

I'm experiencing an issue with cell customization in FlexGrid, and I suspect it might be related to virtualization.

In my example below, I want to use a custom cell in the second column. However, the problem is that this custom cell also appears in other columns (as shown in the screenshot). What's even stranger is that if I scroll quickly, the custom cell seems to propagate throughout the entire grid.

Could you please help me identify the issue and suggest a solution?

image

Thank you in advance for your assistance.

`

using C1.iOS.Grid; using UIKit;

namespace iOSFlexGrid

public class RootViewController : UIViewController
{
    public RootViewController()
    {
        View!.BackgroundColor = UIColor.SystemBackground;
        FlexGrid grid = new FlexGrid();
        grid.CellFactory = new CustomFactory();
        grid.ZoomMode = GridZoomMode.Disabled;
        grid.AutoGenerateColumns = false;
        grid.Columns.Add(new GridColumn
        {
            Header = "Header",
            Binding = "Title",
            IsReadOnly = true,
        });

        grid.Columns.Add(new GridColumn
        {
            Header = "Quantity",
            Binding = "Quantity",
            IsReadOnly = true,
            InputType = UIKeyboardType.DecimalPad,
            Format = "N0",
        });

        grid.Columns.Add(new GridColumn
        {
            Header = "Amount",
            Binding = "Amount",
            Format = "N2"
        });

        grid.Columns.Add(new GridColumn
        {
            Header = "Total amount",
            Binding = "TotalAmount",
            Format = "N2"
        });

        grid.Columns.Add(new GridColumn
        {
            Header = "Total quantity",
            Binding = "TotalQuantity",
            Format = "N0"
        });

        View.Add(grid);
        grid.TranslatesAutoresizingMaskIntoConstraints = false;
        var constraints = new NSLayoutConstraint[]
        {
            grid.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor),
            grid.BottomAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.BottomAnchor),
            grid.LeftAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.LeftAnchor),
            grid.RightAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.RightAnchor)
        };
        NSLayoutConstraint.ActivateConstraints(constraints);

        List<Item> items = new List<Item>();

        for (int i = 0; i < 500; i++)
        {
            items.Add(new Item { Title = $"Line {i}", Quantity = i, Amount = i * 10, TotalAmount = i * 3 * 10, TotalQuantity =0 });
        }
        grid.ItemsSource = items;
    }
}

internal class CustomFactory : GridCellFactory
{

    public override GridCellView CreateCell(GridCellType cellType, GridCellRange range, object cellKind)
    {
        return base.CreateCell(cellType, range, cellKind);
    }

    public override UIView CreateCellContent(GridCellType cellType, GridCellRange range, object cellContentType)
    {
        if (range.Column == 1 && cellType == GridCellType.Cell)
            return new UIView { BackgroundColor = UIColor.Yellow };
        return base.CreateCellContent(cellType, range, cellContentType);
    }
}

public class Item
{
    public string? Title { get; set; }
    public int Quantity { get; set; }
    public int Amount { get; set; }
    public int TotalAmount { get; set; }
    public int TotalQuantity { get; set; }
}

`

enkaradag commented 11 months ago

try adding an else condition for CreateCellContent

    public override UIView CreateCellContent(GridCellType cellType, GridCellRange range, object cellContentType)
    {
        if (range.Column == 1 && cellType == GridCellType.Cell)
            return new UIView { BackgroundColor = UIColor.Yellow };
       else           //add here
            return new UIView {BackgroundColor = UIColor.White};

        return base.CreateCellContent(cellType, range, cellContentType);
    }
am-kh commented 11 months ago

The "Else" won't change anything, and that's not what I want anyway. I want a custom cell only for the first column; the other cells should retain their default behavior. thank you for your help

IrinaPykhova commented 11 months ago

looks like something about recycling. @arivoir please take a look

arivoir commented 11 months ago

Hi @am-kh, This issue is related to cell-recycling, to effectively work with it there are different things to consider. 1) If your intention is just painting the background of the cell, you can use PrepareCell method instead, and leave the content of the cells as they are.

internal class CustomFactory : GridCellFactory
{
    public override void PrepareCell(GridCellType cellType, GridCellRange range, GridCellView cell, UIEdgeInsets internalBorders)
    {
        base.PrepareCell(cellType, range, cell, internalBorders);
        if (range.Column == 1 && cellType == GridCellType.Cell)
            cell.BackgroundColor = UIColor.Yellow;
    }
}

2) If you need to customize the UILabel inside the cell, everything you set in BindCellContent method you have to undo it in UnbindCellContent

internal class CustomFactory : GridCellFactory
{
    public override void BindCellContent(GridCellType cellType, GridCellRange range, UIView cellContent)
    {
        if (range.Column == 1 && cellType == GridCellType.Cell && cellContent is UILabel label)
        {
            label.TextColor = UIColor.Yellow;
        }
        base.BindCellContent(cellType, range, cellContent);
    }

    public override void UnbindCellContent(GridCellType cellType, GridCellRange range, UIView cellContent)
    {
        if (range.Column == 1 && cellType == GridCellType.Cell && cellContent is UILabel label)
        {
            label.TextColor = UIColor.Black;
        }
        base.UnbindCellContent(cellType, range, cellContent);
    }
}

3) If you need to customize the cell so the content is something different from a UILabel you need to override the methods GetCellContentType and CreateCellContent. In the method GetCellContentType, you will specify what kind of content the cell will have. The returned object will be the key that will determine the bucket where the recycled objects are saved. When a new cell is going to be laid out, it will firstly call GetCellContentType, if there is a recycled cell, that cell will be reused, if there is none the method CreateCellConetent will be called to create a new one.

internal class CustomCellContent : UIView
{
    public CustomCellContent()
    {
        BackgroundColor = UIColor.Yellow;
    }
}

internal class CustomFactory : GridCellFactory
{
    public override object GetCellContentType(GridCellType cellType, GridCellRange range)
    {
        if (range.Column == 1 && cellType == GridCellType.Cell)
            return typeof(CustomCellContent);
        return base.GetCellContentType(cellType, range);
    }

    public override UIView CreateCellContent(GridCellType cellType, GridCellRange range, object cellContentType)
    {
        if (cellContentType is Type type && type == typeof(CustomCellContent))
            return new CustomCellContent();
        return base.CreateCellContent(cellType, range, cellContentType);
    }
}

Then you will also need to override BindCellContent and UnbindCellContent if you need to bind the cell content to the data.

What do you think it is more appropriate to your scenario?

am-kh commented 11 months ago

Hi @arivoir Thank you for your response. The third scenario is the one I'm interested in, and your answer aligns perfectly with what I want to achieve. Thank you very much.

I have a question: how can I disable grid bounces? Also, how can I tell it not to scroll both vertically and horizontally at the same time?

I appreciate your assistance. This component is fantastic and performs well.

Screenshot 2023-10-02 at 15 27 59 Screenshot 2023-10-02 at 15 29 09

arivoir commented 11 months ago

So far there is no api for disabling bounces, but FlexGrid uses native UIScrollView inside. As a workaround you can walk the visual tree, grab the scroll-view and set the properties there.

var scrollView = grid.GetChildren().OfType<UIScrollView>().FirstOrDefault();
scrollView.Bounces = false;
scrollView.BouncesZoom = false;
public static class UIViewEx
{
    /// <summary>
    /// Finds all the children of a given <see cref="UIView"/>.
    /// </summary>
    internal static IEnumerable<UIView> GetChildren(this UIView view, bool includeSelf = true, bool recursive = true)
    {
        if (view == null) yield break;
        if (includeSelf) yield return view;

        UIView current = view;
        foreach (var child in view.Subviews)
        {
            yield return child;
            if (recursive)
            {
                foreach (var item in child.GetChildren(false, true))
                {
                    yield return item;
                }
            }
        }
    }
}

Notice the internal ScrollView is created after FlexGrid is added to Window.

On the other hand the DirectionalLockEnabled of UIScrollView is set to true by default inside FlexGrid. This doesn't prohibit from performing diagonal gestures, but it locks the direction when the gesture is close to horizontal or vertical. I wonder if there is any way to prohibit diagonal gestures to be performed.