Monotouch.Dialog DequeueReusableCell duplicating content

JonBullockJonBullock USMember, Beta

Hi,

I am using a custom element that inherits from the StyledMultilineElement (this is the first time I've tried this) my custom element adds some buttons to the cell but not all cells should have the buttons. When the table first loads it is all correct but as soon as the cells scroll off screen and back on they all have the buttons added even if they shouldn't. Im guessing its some refresh issue with reusing cells, I've tried SetNeedsLayout and SetNeedsDisplay with no luck. Can anyone help?

/// <summary>
/// Custom alert element.
/// </summary>
public class CustomAlertElement : StyledMultilineElement
{
    #region Properties

    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="IG.CasinoManagement.CustomAlertElement"/> view
    /// profile hidden.
    /// </summary>
    /// <value><c>true</c> if view profile hidden; otherwise, <c>false</c>.</value>
    public bool ViewProfileHidden
    {
        get
        {
            return _viewProfileButtonHidden;
        }

        set
        {
            _viewProfileButtonHidden = value;
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="IG.CasinoManagement.CustomAlertElement"/> mark read hidden.
    /// </summary>
    /// <value><c>true</c> if mark read hidden; otherwise, <c>false</c>.</value>
    public bool MarkReadHidden
    {
        get
        {
            return _viewMarkReadButtonHidden;
        }

        set
        {
            _viewMarkReadButtonHidden = value;
        }
    }

    /// <summary>
    /// Gets or sets the alert colour.
    /// </summary>
    /// <value>The alert colour.</value>
    public UIColor AlertColour
    {
        get
        {
            return _alertColour;
        }

        set
        {
            _alertColour = value;
        }
    }

    #endregion

    #region Fields

    private UIButton _viewProfileButton;
    private UIButton _markReadButton;
    private bool _viewProfileButtonHidden = true;
    private bool _viewMarkReadButtonHidden = true;
    private UIView _colourView;
    private UIColor _alertColour;

    #endregion

    /// <summary>
    /// Initializes a new instance of the <see cref="IG.CasinoManagement.CustomAlertElement"/> class.
    /// </summary>
    /// <param name="caption">Caption.</param>
    public CustomAlertElement(string caption) : base(caption)
    {

    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IG.CasinoManagement.CustomAlertElement"/> class.
    /// </summary>
    /// <param name="caption">Caption.</param>
    /// <param name="value">Value.</param>
    public CustomAlertElement(string caption, string value) : base(caption, value)
    {

    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IG.CasinoManagement.CustomAlertElement"/> class.
    /// </summary>
    /// <param name="caption">Caption.</param>
    /// <param name="value">Value.</param>
    /// <param name="style">Style.</param>
    public CustomAlertElement(string caption, string value, UITableViewCellStyle style) : base(caption, value, style)
    {

    }

    /// <Docs>The containing table view.</Docs>
    /// <returns></returns>
    /// <summary>
    /// Gets the cell.
    /// </summary>
    /// <param name="tv">Tv.</param>
    public override UITableViewCell GetCell(UITableView tv)
    {
        var key = GetKey ((int) style);
        var cell = tv.DequeueReusableCell (key);
        if (cell == null)
        {
            cell = new UITableViewCell (style, key);
            cell.SelectionStyle = UITableViewCellSelectionStyle.None;
        }

        PrepareCell (cell);

        return cell;
    }

    /// <Docs>To be added.</Docs>
    /// <remarks>To be added.</remarks>
    /// <summary>
    /// Prepares the cell.
    /// </summary>
    /// <param name="cell">Cell.</param>
    protected void PrepareCell (UITableViewCell cell)
    {
        cell.Accessory = Accessory;
        var textLabel = cell.TextLabel;
        textLabel.Text = Caption;
        textLabel.TextAlignment = Alignment;
        textLabel.TextColor = TextColor ?? UIColor.Black;
        textLabel.Font = Font ?? UIFont.BoldSystemFontOfSize (17);
        textLabel.LineBreakMode = LineBreakMode;
        textLabel.Lines = Lines;     

        // The check is needed because the cell might have been recycled.
        if (cell.DetailTextLabel != null)
        {
            cell.DetailTextLabel.Text = Value == null ? "" : Value;
        }

        var imgView = cell.ImageView;

        if (imgView != null) 
        {
            imgView.Image = this.Image;
        }

        if (cell.DetailTextLabel != null)
        {
            cell.DetailTextLabel.Lines = Lines;
            cell.DetailTextLabel.LineBreakMode = LineBreakMode;
            cell.DetailTextLabel.Font = SubtitleFont ?? UIFont.SystemFontOfSize (14);
            cell.DetailTextLabel.TextColor = DetailColor;
        }

        // Create the view profile button 
        _viewProfileButton = new UIButton(new RectangleF(903, 8, 101, 28));
        _viewProfileButton.BackgroundColor = UIColor.FromRGB(88, 201, 93);
        _viewProfileButton.SetTitleColor(UIColor.White, UIControlState.Normal);
        _viewProfileButton.SetTitle(NSBundle.MainBundle.LocalizedString("View Profile", "View Profile"), UIControlState.Normal);
        _viewProfileButton.Font = UIFont.SystemFontOfSize(13.0f);
        _viewProfileButton.Layer.CornerRadius = 5;
        _viewProfileButton.Layer.BorderColor = UIColor.LightGray.CGColor;
        _viewProfileButton.Layer.BorderWidth = 3;
        _viewProfileButton.TouchUpInside += delegate
        {
            if(ViewProfileTapped != null)
            {
                ViewProfileTapped.Invoke(this, new EventArgs());
            }
        };

        // Create the mark as read button
        _markReadButton = new UIButton(new RectangleF(788, 8, 107, 28));
        _markReadButton.BackgroundColor = UIColor.FromRGB(28, 71, 201);
        _markReadButton.SetTitleColor(UIColor.White, UIControlState.Normal);
        _markReadButton.SetTitle(NSBundle.MainBundle.LocalizedString("Acknowledge", "Acknowledge"), UIControlState.Normal);
        _markReadButton.Font = UIFont.SystemFontOfSize(13.0f);
        _markReadButton.Layer.CornerRadius = 5;
        _markReadButton.Layer.BorderColor = UIColor.LightGray.CGColor;
        _markReadButton.Layer.BorderWidth = 3;
        _markReadButton.TouchUpInside += delegate
        {
            if(MarkReadTapped != null)
            {
                MarkReadTapped.Invoke(this, new EventArgs());
            }
        };

        // Create the colour view and add it to the view
        _colourView = new UIView(new RectangleF(4, 4, 40, 40));
        _colourView.BackgroundColor = _alertColour;
        imgView.Image = _colourView.ToImage();

        // If set to be visible show the view profile button
        if (!_viewProfileButtonHidden)
        {
            cell.ContentView.AddSubview(_viewProfileButton);
        }

        // If set to be visible show the mark as read button
        if (!_viewMarkReadButtonHidden)
        {
            cell.ContentView.AddSubview(_markReadButton);
        }

        cell.ContentView.SetNeedsLayout();
        cell.ContentView.SetNeedsDisplay();
        cell.SetNeedsLayout();
        cell.SetNeedsLayout();
    }

    /// <summary>
    /// Occurs when view profile tapped.
    /// </summary>
    public event EventHandler <EventArgs> ViewProfileTapped;

    /// <summary>
    /// Occurs when mark read tapped.
    /// </summary>
    public event EventHandler <EventArgs> MarkReadTapped;        
}

Posts

  • JohnMillerJohnMiller USForum Administrator, Xamarin Team Xamurai
    edited October 2013

    @JonBullock,

    I had a similar issue recently and to solve it I think I subclassed UITableViewCell for my custom cell, and implemented the Dispose method to remove my views and null them.

     customButtonInCell.RemoveFromSuperview();
     customButtonInCell = null;
    

    I think the other way I did this was at the Element level before the PrepareCell() call, I made sure the cell didnt have the customButtonInCell by making sure it was null, etc. Then the PrepareCell method logic would take care if it actually needed the button. I'd be curious to know what the accepted pattern for this is too.

    I also saw @sezumuz post something showing how to implement PrepareForReuse() and PrepareForUse() which seemed like a really good way to handle this sort of thing.

    EDIT: here are the comments I was referring to.

  • PerHungebergMllerPerHungebergMller DKMember ✭✭✭

    @JonBullock

    You need to provide an (application wide) unique cell key, using style as key won't do.
    AFAIK it is very important that custom cells have a 100% unique key for the cells when getting them throu DequeuReusableCell()

    Try something like:

    protected override NSString CellKey { get { return (NSString) "CustomAlertElement"; } }
    You might also need to implement GetHeight for the element:

    public float GetHeight (UITableView tableView, NSIndexPath indexPath) { var cellHeight = CalculateCellHeight(); return cellHeight; }
    Also it is a good practice to calculate relative control positions instead of using absolute positions for x, y, width and height. This way you are better prepared for different resolutions on different iOS devices.

  • SebastienPouliotSebastienPouliot CAXamarin Team Xamurai

    The important part is that when you re-use a cell what you get as an input is a old cell that was already used. That old cell will already contains everything you added to it when it was created (or first initialized).

    So if your code does stuff like:

    cell.ContentView.AddSubview(_viewProfileButton);
    

    on both new and reused cells then you gonna end up with a new button for each re-used cell.

    You need to either:

    • initialize new cells differently than re-used cells (e.g. create the buttons only on new cells); or

    • have smarter cell initialization code that will only add the buttons if they are not already present.

  • JonBullockJonBullock USMember, Beta

    I think I've sorted it.

    Element has a RemoveTag method which I have used in GetCell

        public override UITableViewCell GetCell(UITableView tv)
        {
            var key = CellKey;
            var cell = tv.DequeueReusableCell (key);
            if (cell == null)
            {
                cell = new UITableViewCell(style, key);
                cell.SelectionStyle = UITableViewCellSelectionStyle.None;
            }
            else
            {
                RemoveTag(cell, 1); 
                RemoveTag(cell, 2);
            }
    
            PrepareCell (cell);
    
            return cell;
        }
    

    And I have set the Tag property of each of my buttons.

Sign In or Register to comment.