Unable To Retrieve Custom Control Value from View - xamarin.forms

Xamarin Forms Android Autosize Label TextCompat pre android 8 doesn't autosize text
I unfortunately do not have a high enough rep to comment on anyones post.
I was trying some things out and came across the post linked which got me very close to the solution after experimenting with other posts. I am also trying to autosize text within an app, but inside of an MVVM Master Detail project. If I enter values directly in the Droid renderer it works as expected, but that defeats the purpose when I have fonts of all sizes needed.
I have already made sure my return type is correct.
The code behind is initialized prior to the get value.
The fields are public.
There are no other issues by plugging in numeric values instead of bindable properties.
I am not receiving any values from the view. I would assume the view has not been created yet but the code behind has initialized. I am pretty sure I have done everything mostly right but I mostly deal with stock Xamarin so expanding functionality is still pretty new to me. All help is appreciated.
Custom Control (edit: changed default value from default(int) to an integer value to get rid of exception)
/// <summary>Auto scale label font size class.</summary>
public class AutoSizeLabel : Label
{
/// <summary>Minimum font size property.</summary>
public static readonly BindableProperty MinimumFontSizeProperty = BindableProperty.Create(
propertyName: nameof(MinimumFontSize),
returnType: typeof(int),
declaringType: typeof(AutoSizeLabel),
defaultValue: 17);
/// <summary>Maximum font size property.</summary>
public static readonly BindableProperty MaximumFontSizeProperty = BindableProperty.Create(
propertyName: nameof(MaximumFontSize),
returnType: typeof(int),
declaringType: typeof(AutoSizeLabel),
defaultValue: 24);
/// <summary>Gets or sets minimum font size.</summary>
public int MinimumFontSize
{
get
{
return (int)this.GetValue(MinimumFontSizeProperty);
}
set
{
this.SetValue(MinimumFontSizeProperty, value);
}
}
/// <summary>Gets or sets maximum font size.</summary>
public int MaximumFontSize
{
get
{
return (int)this.GetValue(MaximumFontSizeProperty);
}
set
{
this.SetValue(MaximumFontSizeProperty, value);
}
}
}
Droid Renderer
public class AutoSizeLabelRenderer : LabelRenderer
{
protected override bool ManageNativeControlLifetime => false;
protected override void Dispose(bool disposing)
{
Control.RemoveFromParent();
base.Dispose(disposing);
}
private AutoSizeLabel bindingValue = new AutoSizeLabel();
private AppCompatTextView appCompatTextView;
public AutoSizeLabelRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
//v8 and above supported natively, no need for the extra stuff below.
if (DeviceInfo.Version.Major >= 8)
{
Control?.SetAutoSizeTextTypeUniformWithConfiguration(bindingValue.MinimumFontSize, bindingValue.MaximumFontSize, 2, (int)ComplexUnitType.Sp);
return;
}
appCompatTextView = new AppCompatTextView(Context);
appCompatTextView.SetTextColor(Element.TextColor.ToAndroid());
appCompatTextView.SetMaxLines(1);
appCompatTextView.SetBindingContext(autoLabel.BindingContext);SetNativeControl(appCompatTextView);
TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, bindingValue.MinimumFontSize, bindingValue.MaximumFontSize, 2, (int)ComplexUnitType.Sp);
}
}
XAML Call
<renderer:AutoSizeLabel MinimumFontSize="17"
MaximumFontSize="24"
Style="{StaticResource SomeStyle}"
Text="{Binding SomeText}">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SomeCommand}"></TapGestureRecognizer>
</Label.GestureRecognizers>
</renderer:AutoSizeLabel>

This line is unnecessary.
private AutoSizeLabel bindingValue = new AutoSizeLabel();
Instead reference autoLabel. Alternatively I changed the check to
if (e.NewElement == null || Control == null) { return; }
and cast in the following line using
var autoSizeLabel = e.NewElement as AutoSizeLabel;

Related

C#, Xamarin Forms: No Custom TextChangedEvent Raised on initialization

I'm creating an Xamarin.Forms MVVM App (only using Android) which needs certain buttons to be outlined red, whenever their text property holds a specific value. (Purpose: alert the user to press the button and select a value, which will change the Button Text Property and therefore remove the red outline)
To achieve this I've create the following documents:
A custom button CButton that extents the default Button:
public class CButton : Button
{
// this Hides the Default .Text-Property
public string Text
{
get => base.Text;
set
{
base.Text = value;
TextChangedEvent(this, new EventArgs());
}
}
// The Raised Event
protected virtual void TextChangedEvent(object sender, EventArgs e)
{
EventHandler<EventArgs> handler = TextChanged;
handler(sender, e);
}
public event EventHandler<EventArgs> TextChanged;
}
A custom behavior makes use of the raised TextChangedEvent
public class ButtonValBehavior : Behavior<CButton>
{
protected override void OnAttachedTo(CButton bindable)
{
bindable.TextChanged += HandleTextChanged;
base.OnAttachedTo(bindable);
}
void HandleTextChanged(object sender, EventArgs e)
{
string forbidden = "hh:mm|dd.mm.yyyy";
if (forbidden.Contains((sender as CButton).Text.ToLower()))
{
//Do when Button Text = "hh:mm" || "dd.mm.yyyy"
(sender as CButton).BorderColor = Color.Gray;
}
else
{
//Do whenever Button.Text is any other value
(sender as CButton).BorderColor = Color.FromHex("#d10f32");
}
}
protected override void OnDetachingFrom(CButton bindable)
{
bindable.TextChanged -= HandleTextChanged;
base.OnDetachingFrom(bindable);
}
}
The relevant parts of the ViewModel look the following:
public class VM_DIVI : VM_Base
{
public VM_DIVI(O_BasisProtokoll base)
{
Base = base;
}
private O_BasisProtokoll _base = null;
public O_BasisProtokoll Base
{
get => _base;
set
{
_base = value;
OnPropertyChanged();
}
}
Command _datePopCommand;
public Command DatePopCommand
{
get
{
return _datePopCommand ?? (_datePopCommand = new Command(param => ExecuteDatePopCommand(param)));
}
}
void ExecuteDatePopCommand(object param)
{
//launch popup
var p = new PP_DatePicker(param);
PopupNavigation.Instance.PushAsync(p);
}
}
The .xmal looks the following (b is the xmlns of the Namespace):
<b:CButton x:Name="BTN_ED_Datum"
Text="{Binding Base.ED_datum, Mode=TwoWay}"
Grid.Column="1"
Command="{Binding DatePopCommand}"
CommandParameter="{x:Reference BTN_ED_Datum}">
<b:CButton.Behaviors>
<b:ButtonValBehavior/>
</b:CButton.Behaviors>
</b:CButton>
This solution works fine whenever the input is caused by user interaction. However, when a Value is assigned during the initialization of the Page no red outline is created, in fact the TextChangedEvent isn't raised. By using breakpoints I noticed that during initialization the Text Property of CButton is never set, eventhough it actually will be in the view.
Despite fiddling around with my solution I cannot make this work on initialization. I tried to work around this issue by outlining every button by default in their constructor, however this will outline every button red, even when their text value doesn't require them to be.
How can I achieve my initial goal?
Many thanks in advance!
It's been a while but if I recall correctly what I ended up doing was:
Changing the new Text-Property of my custom Button to CText and
Making sure that I have Mode=TwoWay activated for any Element, that doesn't have it enabled by default. (Look up Binding modes on msdn for more)
making CText a bindable property of CButton
My custom button now looks the following:
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace EORG_Anton.Model
{
public class CButton : Button
{
public static readonly BindableProperty CTextProperty =
BindableProperty.Create(nameof(CText),
typeof(string),
typeof(CButton),
default(string),
BindingMode.TwoWay,
propertyChanged: OnTextChanged);
private static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (CButton)bindable;
var value = (string)newValue;
control.CText = value;
}
public string CText
{
get => base.Text;
set
{
base.Text = value;
TextChangedEvent(this, new EventArgs());
}
}
protected virtual void TextChangedEvent(object sender, EventArgs e)
{
EventHandler<EventArgs> handler = TextChanged;
handler(sender, e);
}
public event EventHandler<EventArgs> TextChanged;
}
}

Why does setting BindableProperty.DefaultValue not call OnPropertyChanged?

I'm using a bindable property like this in a class the inherits from Xamarin.Forms.ContentView:
public static readonly BindableProperty OverlayColorProperty = BindableProperty.Create(nameof(OverlayColor), typeof(Color), typeof(MyControl), Color.FromHex("#55000000"));
public Color OverlayColor
{
get => (Color)GetValue(OverlayColorProperty);
set => SetValue(OverlayColorProperty, value);
}
Furthermore I'm listening for changes to update an inner elements background color:
protected override void OnPropertyChanged(string propertyName)
{
base.OnPropertyChanged(propertyName);
switch (propertyName)
{
case nameof(OverlayColor):
GridBackground.BackgroundColor = OverlayColor;
break;
}
}
I just noticed that OnPropertyChanged does not get called with the default value. Just when I update it from another place, XAML or through code.
Is this exspected behavior?
If yes, why? What should I do instead? Define it also in XAML code?

Xamarin Forms - UWP custom renderer: how to add a child to a stacklayout

I'm trying to insert a UWP specific child in the custom renderer of a StackLayout.
However, in the sample code below, Control is always null whereas my StackLayout has Children. Maybe StackPanel is not what StackLayout is rendered into in UWP.
public class MyRenderer : ViewRenderer<StackLayout, StackPanel>
{
private bool _childAdded;
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!_childAdded && Control?.Children != null)
{
_childAdded = true;
Control.Children.Insert(0, new Windows.UI.Xaml.Shapes.Rectangle());
}
base.OnElementPropertyChanged(sender, e);
}
}
Some modification in you are cade because you are calling base.OnElementPropertyChanged(sender,e) after code implementation. Just try to use below code.
public class MyRenderer : ViewRenderer<StackLayout, StackPanel>
{
private bool _childAdded;
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if(Control==null)
return;
if (!_childAdded && Control.Children != null)
{
_childAdded = true;
Control.Children.Insert(0, new Windows.UI.Xaml.Shapes.Rectangle());
}
}
}
The StackLayout (Layout) renderer is ViewRenderer and implemented on UWP by FrameworkElement; Renderer Base Classes and Native Controls.
Theoretical renderer:
public class MyRenderer : ViewRenderer<StackLayout, FrameworkElement>
...
Control is always null whereas my StackLayout has Children. Maybe StackPanel.
Derive from official document,
In Xamarin.Forms, all layout classes derive from the Layout<T> class and constrain the generic type to View and its derived types. But the layout of children element is incorrect.
And the match Native control within UWP platform is LayoutRenderer. So it is not inherit StackPanel directly. You could also custom a customrederer like the follow.
[assembly: ExportRenderer(typeof(StackLayout), typeof(ICustomStackLayoutRenderer))]
namespace CustomStackLayoutRenderer.UWP
{
public class ICustomStackLayoutRenderer : ViewRenderer<StackLayout, StackPanel>
{
private bool _childAdded;
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var stacklayout = new StackPanel();
SetNativeControl(stacklayout);
}
if (e.OldElement != null)
{
}
if (e.NewElement != null)
{
if (!_childAdded && Control.Children != null)
{
_childAdded = true;
Control.Children.Insert(0, new Windows.UI.Xaml.Shapes.Rectangle() { Width = 100, Height = 100, Fill = new SolidColorBrush(Colors.Red) });
}
}
}
}
}
For your requirement, the better way is that create a CustomStackLayout inherit StackLayout in Xamarin.Forms, and re-layout your children element in your LayoutChildren override method. For more detail you could refer Creating a Custom Layout.

Xamarin Forms Custom View Renderer for TextView Height issue

I want to render xamarin forms view control into android textview.
Below customrenderer renders Android TextView on Android device with initial default text.
Now, If I add more text into it, this control would not grow in height but instead becomes scrollable to see the text not visible on screen.
On other way around, if I reduce text content, this control would not reduce in height and occupy the same space and show reduced text content on scree.
Issue : In this case of text change, this label should increase or reduce height on screen for Android. It is working fine on IOS as expected.
BindingText is bindable property to set text for this control.
CopyableLabelRenderer is my custom renderer class for Xamarin Forms
view.
Here is a sample code.
class CopyableLabelRenderer : ViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
try
{
var label = new TextView(MainApplication.Context);
label.SetTextIsSelectable(true);
label.LinksClickable = true;
label.Text = (e.NewElement as CopyableLabel).BindingText; // this line sets text into this label
SetNativeControl(label);
}
catch (Exception ex)
{
var msg = ex.Message;
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("BindingText"))
{
TextView textView = this.Control as TextView;
textView.Text = (Element as CopyableLabel).BindingText;
}
}
}
Here is a base class,
public class CustomLabel : View
{
public static readonly BindableProperty BindingTextProperty = BindableProperty.Create("BindingText", typeof(string),typeof(View),
string.Empty
);
public string BindingText
{
get
{
return (string)GetValue(BindingTextProperty);
}
set
{
SetValue(BindingTextProperty, value);
}
}
}

xamarin.forms entry binding on blur

It appears the default binding trigger for an entry text is the TextChanged event. I want to defer updating the source until the blur event. In WPF there was the UpdateSourceTrigger parameter that could be set to modify the binding trigger, but there isn’t any documentation I’ve found on this in Xamarin.Form.
How can that be achieved in Xaramin.Forms with binding in XAML. For obvious reasons, I don’t want to manually handle it in the code behind.
This question is old but still actual. Especially for fields with decimal point...
First option to use Xamarin Community Toolkit EventToCommandBehavior (or similar implementations, like Prism). Then possible to bind "Unfocused" event to some command:
<Entry.Behaviors>
<xct:EventToCommandBehavior
EventName="Unfocused"
Command="{Binding MyCustomCommand}" />
</Entry.Behaviors>
Other option to add custom Entry component that handle this event, this option fits me better with numeric input with decimal point (float, double, decimal). This a little modified solution from MSDN forum Entry binding Decimal:
public class NumericEntry : Entry
{
#region Bindables
public static readonly BindableProperty NumericValueProperty = BindableProperty.Create(
"NumericValue",
typeof(decimal?),
typeof(NumericEntry),
null,
BindingMode.TwoWay,
coerceValue: (_, value) => (decimal?)value,
propertyChanged: (bindable, _, __) => SetDisplayFormat((NumericEntry)bindable)
);
public static readonly BindableProperty NumericValueFormatProperty = BindableProperty.Create(
"NumericValueFormat",
typeof(string),
typeof(NumericEntry),
"N0",
BindingMode.TwoWay,
propertyChanged: (bindable, _, __) => SetDisplayFormat((NumericEntry)bindable)
);
#endregion Bindables
#region Constructor
public NumericEntry()
{
Keyboard = Keyboard.Numeric;
Focused += OnFocused;
Unfocused += OnUnfocused;
}
#endregion Constructor
#region Events
private void OnFocused(object sender, FocusEventArgs e)
{
SetEditFormat(this);
}
private void OnUnfocused(object sender, FocusEventArgs e)
{
var numberFormant = CultureInfo.CurrentCulture.NumberFormat;
var _text = Text.Replace(".", numberFormant.NumberDecimalSeparator);
if (decimal.TryParse(_text, NumberStyles.Number, CultureInfo.CurrentCulture, out var numericValue))
{
var round = Convert.ToInt32(NumericValueFormat.Substring(1));
NumericValue = Math.Round(numericValue, round);
}
else
{
NumericValue = null;
}
SetDisplayFormat(this);
}
#endregion Events
#region Properties
public decimal? NumericValue
{
get => (decimal?)GetValue(NumericValueProperty);
set => SetValue(NumericValueProperty, value);
}
public string NumericValueFormat
{
get => (string)GetValue(NumericValueFormatProperty) ?? "N0";
set
{
var _value = string.IsNullOrWhiteSpace(value) ? "N0" : value;
SetValue(NumericValueFormatProperty, _value);
}
}
#endregion Properties
#region Methods
private static void SetDisplayFormat(NumericEntry textBox)
{
if (textBox.NumericValue.HasValue)
{
textBox.Text = textBox.NumericValue.Value.ToString(textBox.NumericValueFormat, CultureInfo.DefaultThreadCurrentCulture);
}
else
{
textBox.Text = string.Empty;
}
}
private static void SetEditFormat(NumericEntry textBox)
{
if (textBox.NumericValue.HasValue)
{
var numberFormant = CultureInfo.CurrentCulture.NumberFormat;
textBox.Text = textBox.NumericValue.Value.ToString(textBox.NumericValueFormat, CultureInfo.CurrentCulture).Replace(numberFormant.NumberGroupSeparator, string.Empty);
}
else
{
textBox.Text = string.Empty;
}
}
#endregion Methods
}
And use it like this:
// import our component
xmlns:ex="clr-namespace:BoganPos.Extensions"
//...
<ex:NumericEntry NumericValue="{Binding DecimalValue}" NumericValueFormat="F2" Placeholder="Placeholder"/>

Resources