Hi,
I'm experiencing a strange issue with my custom Label renderer. My custom renderer keeps crashing due to a NullReferenceException while disposing its instance. I'm gonna attach my custom renderer at the bottom of this text. I'll try to reproduce the issue in brief:
Case 1:
- Create a ContentView and set my custom Label as its content.
- Add the ContentView created above into a generic Layout (e.g. StackLayout).
- Once the custom Label is instantiated, remove the whole ContentView from the generic Layout.
- The custom Label renderer WON'T crash.
Case 2:
- Create a ViewCell and set my custom Label as its content.
- Create an ObservableCollection containing various instances of String type.
- Set the ViewCell as ItemTemplate of a generic ListView.
- Set the above ObservableCollection as ItemsSource.
- Finally remove an item from the above ListView.
- The custom Label renderer WILL crash.
I don't know how to solve this issue. Is there something related directly to the ListView? Why my custom Label renderer crashes only when it gets disposed from a ListView? My custom Label renderer does not override the Dispose() method of the LabelRenderer class. It's a very strange behaviour to me. Waiting for your help, thanks in advance.
CustomLabel
using MyProject.Helpers;
using Xamarin.Forms;
namespace MyProject.Renders
{
public class CustomLabel : Label
{
#region LineSpacing
public static readonly BindableProperty LineSpacingProperty = BindableProperty.Create(nameof(LineSpacing), typeof(float), typeof(CustomLabel), 1.1f);
/// <summary>
/// Gets or sets the extra spacing between lines of text, as a multiplier.
/// </summary>
public float LineSpacing
{
get => (float)GetValue(LineSpacingProperty);
set => SetValue(LineSpacingProperty, value);
}
#endregion
#region HasShadow
public static readonly BindableProperty HasShadowProperty = BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(CustomLabel), false);
/// <summary>
/// Gets or sets whether the text should have a shadow.
/// </summary>
public bool HasShadow
{
get => (bool)GetValue(HasShadowProperty);
set => SetValue(HasShadowProperty, value);
}
#endregion
#region ShadowColor
public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(CustomLabel), Color.Black);
/// <summary>
/// Gets or sets the shadow color of the text.
/// </summary>
public Color ShadowColor
{
get => (Color)GetValue(ShadowColorProperty);
set => SetValue(ShadowColorProperty, value);
}
#endregion
#region ShadowRadius
public static readonly BindableProperty ShadowRadiusProperty = BindableProperty.Create(nameof(ShadowRadius), typeof(int), typeof(CustomLabel), 5);
/// <summary>
/// Gets or sets the shadow radius of the text.
/// </summary>
public int ShadowRadius
{
get => (int)GetValue(ShadowRadiusProperty);
set => SetValue(ShadowRadiusProperty, value);
}
#endregion
#region ShadowDistanceX
public static readonly BindableProperty ShadowDistanceXProperty = BindableProperty.Create(nameof(ShadowDistanceX), typeof(int), typeof(CustomLabel), 5);
/// <summary>
/// Gets or sets the horizontal offset of the text shadow.
/// </summary>
public int ShadowDistanceX
{
get => (int)GetValue(ShadowDistanceXProperty);
set => SetValue(ShadowDistanceXProperty, value);
}
#endregion
#region ShadowDistanceY
public static readonly BindableProperty ShadowDistanceYProperty = BindableProperty.Create(nameof(ShadowDistanceY), typeof(int), typeof(CustomLabel), 5);
/// <summary>
/// Gets or sets the vertical offset of the text shadow.
/// </summary>
public int ShadowDistanceY
{
get => (int)GetValue(ShadowDistanceYProperty);
set => SetValue(ShadowDistanceYProperty, value);
}
#endregion
#region Html
public static readonly BindableProperty HtmlProperty = BindableProperty.Create(nameof(Html), typeof(bool), typeof(CustomLabel), false);
/// <summary>
/// Gets or sets whether the text should be rendered as HTML text.
/// </summary>
public bool Html
{
get => (bool)GetValue(HtmlProperty);
set => SetValue(HtmlProperty, value);
}
#endregion
#region FontName
public static readonly BindableProperty FontNameProperty = BindableProperty.Create(nameof(FontName), typeof(string), typeof(CustomLabel), Utils.GetGeneralLightFont());
/// <summary>
/// Gets or sets the font name of the text. Does not require the font ".ttf" extension.
/// </summary>
public string FontName
{
get => (string)GetValue(FontNameProperty);
set => SetValue(FontNameProperty, value);
}
#endregion
#region FontSizeMultiplier
public static readonly BindableProperty FontSizeMultiplierProperty = BindableProperty.Create(nameof(FontSizeMultiplier), typeof(float), typeof(CustomLabel), 1.0f);
/// <summary>
/// Gets or sets the extra custom font size, as a multiplier.
/// </summary>
public float FontSizeMultiplier
{
get => (float)GetValue(FontSizeMultiplierProperty);
set => SetValue(FontSizeMultiplierProperty, value);
}
#endregion
#region Padding
public static readonly BindableProperty PaddingProperty = BindableProperty.Create(nameof(Padding), typeof(Thickness), typeof(CustomLabel), new Thickness(0));
/// <summary>
/// Gets or sets the inner padding.
/// </summary>
public Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
#endregion
#region IsHeader
public static readonly BindableProperty IsHeaderProperty = BindableProperty.Create(nameof(IsHeader), typeof(bool), typeof(CustomLabel), false);
/// <summary>
/// Gets or sets whether the text should be rendered as an header text with marquee ellipsize.
/// </summary>
public bool IsHeader
{
get => (bool)GetValue(IsHeaderProperty);
set => SetValue(IsHeaderProperty, value);
}
#endregion
#region MaxLines
public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(nameof(MaxLines), typeof(int), typeof(CustomLabel), 0);
/// <summary>
/// Gets or sets the number of lines that CustomLabel should be tall.
/// </summary>
public int MaxLines
{
get => (int)GetValue(MaxLinesProperty);
set => SetValue(MaxLinesProperty, value);
}
#endregion
#region BreakMode
public enum BreakMode { Start, End, None };
public static readonly BindableProperty EllipsizeProperty = BindableProperty.Create(nameof(Ellipsize), typeof(BreakMode), typeof(CustomLabel), BreakMode.None);
/// <summary>
/// Gets or sets the ellipsize type.
/// </summary>
public BreakMode Ellipsize
{
get => (BreakMode)GetValue(EllipsizeProperty);
set => SetValue(EllipsizeProperty, value);
}
#endregion
}
}
[Android] CustomLabelRenderer
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Graphics;
using System.ComponentModel;
using Android.Util;
using Android.OS;
using Android.Text;
using static Android.Text.TextUtils;
using MyProject.Renders;
using MyProject.Droid.Renders;
using MyProject.Droid.Helpers;
using Android.Content;
[assembly: ExportRenderer(typeof(CustomLabel), typeof(CustomLabelRenderer))]
namespace MyProject.Droid.Renders
{
class CustomLabelRenderer : LabelRenderer
{
CustomLabel @CustomLabel;
int MarqueeRepeatLimit = -1; // INFINITE
public CustomLabelRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.OldElement == null && e.NewElement != null)
{
@CustomLabel = (CustomLabel)e.NewElement;
Initialize();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.FontNameProperty.PropertyName))
UpdateFont();
if (IsRendererAvailable() &&
e.PropertyName.Equals(Label.FontSizeProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.FontSizeMultiplierProperty.PropertyName))
UpdateFontSize();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.LineSpacingProperty.PropertyName))
UpdateLineSpacing();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.MaxLinesProperty.PropertyName))
UpdateMaxLines();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.EllipsizeProperty.PropertyName))
UpdateEllipsize();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.IsHeaderProperty.PropertyName))
UpdateHeader();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.HasShadowProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.ShadowDistanceXProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.ShadowDistanceYProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.ShadowColorProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.ShadowRadiusProperty.PropertyName))
UpdateShadow();
if (IsRendererAvailable() &&
e.PropertyName.Equals(Label.TextProperty.PropertyName) ||
e.PropertyName.Equals(CustomLabel.HtmlProperty.PropertyName))
UpdateHtml();
if (IsRendererAvailable() &&
e.PropertyName.Equals(CustomLabel.PaddingProperty.PropertyName))
UpdatePadding();
}
void Initialize()
{
UpdateFont();
UpdateFontSize();
UpdateLineSpacing();
UpdateMaxLines();
UpdateEllipsize();
UpdateHeader();
UpdateShadow();
UpdateHtml();
UpdatePadding();
}
void UpdateFont()
{
Control.Typeface = Typeface.CreateFromAsset(
MainActivity.MainContext.ApplicationContext.Assets,
$"{@CustomLabel.FontName}.ttf"
);
}
void UpdateFontSize()
{
Control.SetTextSize(
ComplexUnitType.Sp,
(float)@CustomLabel.FontSize * @CustomLabel.FontSizeMultiplier
);
}
void UpdateLineSpacing()
{
Control.SetLineSpacing(2.5f, @CustomLabel.LineSpacing);
}
void UpdateMaxLines()
{
if (@CustomLabel.MaxLines > 0)
Control.SetMaxLines(@CustomLabel.MaxLines);
}
void UpdateEllipsize()
{
if (@CustomLabel.Ellipsize != CustomLabel.BreakMode.None)
Control.Ellipsize = @CustomLabel.Ellipsize == CustomLabel.BreakMode.Start ?
TruncateAt.Start : TruncateAt.End;
}
void UpdateHeader()
{
if (@CustomLabel.IsHeader)
{
Control.SetMaxLines(1);
Control.Ellipsize = TruncateAt.Marquee;
Control.SetMarqueeRepeatLimit(MarqueeRepeatLimit);
Control.HorizontalFadingEdgeEnabled = true;
Control.SetHorizontallyScrolling(true);
Control.Focusable = true;
Control.FocusableInTouchMode = true;
Control.Selected = true;
}
}
void UpdateShadow()
{
if (@CustomLabel.HasShadow)
Control.SetShadowLayer(
@CustomLabel.ShadowRadius,
@CustomLabel.ShadowDistanceX,
@CustomLabel.ShadowDistanceY,
@CustomLabel.ShadowColor.ToAndroid()
);
}
void UpdateHtml()
{
if (@CustomLabel.Html && !string.IsNullOrEmpty(@CustomLabel.Text))
{
ISpanned textFromHtml = default(ISpanned);
#pragma warning disable CS0618
if (Utils.IsAndroidVersionSatisfied(BuildVersionCodes.N))
textFromHtml = Html.FromHtml(@CustomLabel.Text, FromHtmlOptions.ModeLegacy);
else textFromHtml = Html.FromHtml(@CustomLabel.Text);
#pragma warning restore CS0618
Control.Text = textFromHtml.ToString().Trim();
}
}
void UpdatePadding()
{
Control.SetPadding(
(int)@CustomLabel.Padding.Left,
(int)@CustomLabel.Padding.Top,
(int)@CustomLabel.Padding.Right,
(int)@CustomLabel.Padding.Bottom
);
}
bool IsRendererAvailable() => Control != null && Element != null && @CustomLabel != null;
}
}