Is there a way to update child elements in custom renderer - xamarin.forms

I was creating a custom control it is more like TabPage, where it's derive from View, containing a list of CSMenuItems and foreach menuItem is derived from BaseMenuItem and has menuContent that is derived from ContentView, like this :
• CSView
• CSMenuItem
• MenuContent
• Content
• CSMenuItem
• MenuContent
• Content
My problem is whenever I Change the properties value of MenuContent in the xaml file, the propertyChanged won't fire and MenuContent won't update. I am pretty sure the problem is in my renderers. Is there any way to update the child element in custom renderer?
Here are my codes for controls:
class CSView : View
{
public CSView ()
{
var items = new ObservableCollection <CSMenuItem>();
items.CollectionChanged += OnItemsChanged;
Items = items;
}
public static readonly BindableProperty ItemsProperty = BindableProperty.Create ("Items", typeof (IList <CSMenuItem>), typeof (CSView));
void OnItemsChanged (object sender, NotifyCollectionChangedEventArgs e)
{
foreach (CSMenuItem item in e.NewItems)
item.Parent = this;
// Maybe setting the item parent to this would be good?
// cause whenever new item is add I want its parent to be this.
// Correct me if im wrong.
}
public IList <CSMenuItem> Items
{
get => (IList <CSMenuItem>)GetValue (ItemsProperty);
set => SetValue (ItemsProperty, value);
}
class CSMenuItem : BaseMenuItem
{
public CSMenuItem()
{
}
public static readonly BindableProperty TitleProperty = BindableProperty.Create("Title", typeof(string), typeof(CSMenuItem), default);
public static readonly BindableProperty ContentProperty = BindableProperty.Create("Content", typeof(MenuContent), typeof(CSMenuItem));
public string Title
{
get => (string)GetValue (TitleProperty);
set => SetValue (TitleProperty, value);
}
public MenuContent Content
{
get => (MenuContent)GetValue (ContentProperty);
set => SetValue (ContentProperty, value);
}
}
class MenuContent : ContentView
{
public MenuContent ()
{
}
public static readonly BindableProperty TitleProperty = BindableProperty.Create ("Title", typeof (string), typeof (MenuContent));
public string Title
{
get => (string)GetValue (TitleProperty);
set => SetValue (TitleProperty, value);
}
}
and for renderers :
class CSViewRenderer : ViewRenderer<CSView, FrameLayout>, NavigationBarView.IOnItemSelectedListener
{
...
protected override void OnElementChanged (ElementChangedEventArgs <CSView> e)
{
if (e.OldElement != null)
UnhandleEvents (e.OldElement);
if (e.NewElement != null)
{
if (Control == null)
SetNativeControl (_control);
HandleEvents (e.NewElement)
}
}
void HandleEvents (CSView element)
=> element.PropertyChanged += OnElementPropertyChanged;
void UnhandleEvents (CSView element)
=> element.PropertyChanged -= OnElementPropertyChanged;
}
/// this is where the issue came from
class CSMenuItemRender : AbsItemRenderer
{
...
protected override View CreateNativeControl ()
{
_base = new LinearLayout (Context);
_toolBar = new ToolBar (Context)
_content = Platform.CreateRendererWithContext (Element.Content, Context); // where Element is type of MenuItem
_toolBar.Title = Element.Content.Title;
_base.Orientation = Orientation.Vertical;
_base.AddView (_toolBar, new LinearLayout.LayoutParams (-1, -2);
_base.AddView (_content.View, new LinearLayout.LayoutParams (-1, -2);
ElementRendererUtil.FitElement (Context, _base, _content);
Platform.SetRenderer (Element.Content, _content);
_content.ElementChanged += OnRendererElementChanged;
return _base;
}
void OnElementChanged (ElementChangedEventArgs <CSMenuItem> e)
{
if (e.OldElement != null)
UnhandleEvents (e.OldElement);
if (e.NewElement != null)
{
if (Control == null)
SetNativeControl (this)
HandleEvents (e.NewElement);
}
}
void OnRendererElementChanged (object sender, VisualElementChangedEventArgs e)
{
if (e.OldElement != null)
e.OldElement.PropertyChanged -= OnRendererElementPropertyChanged;
if (e.NewElement != null)
e.NewElement.PropertyChanged += OnRendererElementPropertyChanged;
}
void OnRendererElementPropertyChanged (object sender, PropertyChangedEventArgs e)
{
var menuContent = (MenuContent)sender;
if (e.PropertyName == MenuContent.TitleProperty.PropertyName)
_toolBar.Title = menuContent.Title;
else if (e.PropertyName == MenuContent.ContentProperty.PropertyName)
_content.Tracker.UpdateLayout ();
}
void HandleEvents (CSMenuItem element)
=> element.PropertyChanged += OnElementPropertyChanged;
void UnhandleEvents (CSMenuItem element)
=> element.PropertyChanged -= OnElementPropertyChanged;
}
I was able to change the MenuContent properties like Title when changing the derive type to Element, like this:
class MenuContent : Element
And setting its parent to CSMenuItem, like this :
public MenuContent Content
{
...
set {
if (value.Parent != this)
value.Parent = this;
SetValue (ContentProperty, value);
}
}
but still the Content property of MenuContent won't update when the value is changed. What I want is to update the MenuContent what ever the value changed in xaml file and to know why the propertyChanged won't fire.
Sorry for my bad English, hope you understand. May the Almighty Bless You😇 Stay safe.

Related

custom LabelRenderer shows wrong text style inside list view on scroll on Xamarin iOS

I am working on an App that requires one list view having text labels with NSUnderlineStyle on user deletion.
As per the requirement user have Delete/restore option in detail screen. On delete confirmation the text label should be underline style for that particular cell.
I am using LabelRenderer for NSUnderlineStyle in Xamarin iOS.
But currently ListView displays text Labels with underline style which is not deleted by user on list view scroll. The underline style are swapping from one cell label to another on list view scroll.
Below my sample code.
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (this.Control == null)
{
return;
}
if (this.Element is ExtendedLabel extended)
{
var strikethroughStyle = extended.IsStrikeThrough ? NSUnderlineStyle.Single : NSUnderlineStyle.None;
this.Control.AttributedText = new NSMutableAttributedString(
extended.Text ?? string.Empty,
this.Control.Font,
strikethroughStyle: strikethroughStyle);
}
}
This is the common issue of TableView Cell Resue , tableView will reuse the cell for the visible(inside screen shot) ones , so it would show the previous style .
To solve this we can forcedly set the style each time when the cell is going to display .
Create custom renderer for ListView , and do this in WillDisplay method ,before it we need to override TableView's Delegate.
[assembly: ExportRenderer(typeof(ListView), typeof(MyRenderer))]
namespace FormsApp.iOS
{
public class MyDelegate : UITableViewDelegate
{
List<Model> data;
public MyDelegate(List<Model> _data)
{
data = _data;
}
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
var views = cell.ContentView.Subviews;
foreach (var view in views)
{
if(view is LabelRenderer renderer)
{
UILabel label = renderer.Control;
var strikethroughStyle = data[indexPath.Row].YourProperty?NSUnderlineStyle.Single : NSUnderlineStyle.None;
label.AttributedText = new NSMutableAttributedString(
label.Text ?? string.Empty,
label.Font,
strikethroughStyle: strikethroughStyle);
}
}
}
}
public class MyRenderer : ListViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
{
IEnumerable<Model> data = (IEnumerable<Model>)Element.ItemsSource;
Control.Delegate = new MyDelegate(data.ToList());
}
}
}
}

Height Button in iOS (Xamarin Forms) dont resize when text label wrap word (I want dynamic size)

I have a ListView in Xamarin.Forms of this way :
this.listView = new ListView();
this.listView.HasUnevenRows = true;
var dataTemplate = new DataTemplate(() =>
{
return new ViewCell { View = new CustomButtonTemplate()};
});
this.listView.ItemTemplate = dataTemplate;
CustomButtonTemplate.xaml
<local:CustomButton
Margin="6"
Padding="0"
HeightRequest="-1"
WidthRequest="-1"
Style="{StaticResource Title_LabelStyle}"
Text="{Binding DisplayText}" />
I also got one button renderer but dont work (without HeightRequest,WidthRequest,Padding dont work either):
[assembly: ExportRenderer(typeof(CustomButton), typeof(CustomButtonMultilineRenderer))]
namespace SGUK.ClassAction.IOS.Renderers
{
public class CustomButtonMultilineRenderer : ButtonRenderer
{
public CustomButtonMultilineRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
{
base.OnElementChanged(e);
if (this.Control != null)
{
this.Control.TitleLabel.LineBreakMode = UILineBreakMode.WordWrap;
this.Control.TitleEdgeInsets = new UIEdgeInsets(0, 10, 0, 10);
this.Control.TitleLabel.TextAlignment = UITextAlignment.Center;
this.Control.HorizontalAlignment = UIControlContentHorizontalAlignment.Center;
}
}
}
}
(with MaterialButtonRenderer dont work either)
The auto height with HasUnevenRows=true works fine on iOS if not using a custom renderer. If using a custom renderer, then it is up to the renderer to set the height of the cell, you have to calculate your own row height in the GetHeightForRow method in the custom renderer.
[assembly: ExportRenderer(typeof(ListView), typeof(MyLVRenderer))]
namespace App79.iOS
{
public class MyLVRenderer : ListViewRenderer
{
//UITableViewSource originalSource;
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
UITableViewSource originalSource = (UIKit.UITableViewSource)Control.Source;
Control.Source = new MyLVSource(originalSource, e.NewElement);
}
}
public class MyLVSource : UITableViewSource
{
UITableViewSource originalSource;
ListView myListView;
public MyLVSource(UITableViewSource origSource, ListView myListV)
{
originalSource = origSource;
myListView = myListV;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return originalSource.RowsInSection(tableview, section);
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
return originalSource.GetCell(tableView, indexPath);
}
public override nfloat GetHeightForFooter(UITableView tableView, nint section)
{
return originalSource.GetHeightForFooter(tableView, section);
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
nfloat origHeight = originalSource.GetHeightForRow(tableView, indexPath);
// calculate your own row height here
ObservableCollection<Employee> employees = myListView.ItemsSource as ObservableCollection<Employee>;
string displayName = employees[indexPath.Row].DisplayName;
nfloat height = MeasureTextSize(displayName,UIScreen.MainScreen.Bounds.Size.Width-50,UIFont.SystemFontSize,null);
return height;
}
public nfloat MeasureTextSize(string text, double width, double fontSize, string fontName = null)
{
var nsText = new NSString(text);
var boundSize = new SizeF((float)width, float.MaxValue);
var options = NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesLineFragmentOrigin;
if (fontName == null)
{
fontName = "HelveticaNeue";
}
var attributes = new UIStringAttributes
{
Font = UIFont.FromName(fontName, (float)fontSize)
};
var sizeF = nsText.GetBoundingRect(boundSize, options, attributes, null).Size;
//return new Xamarin.Forms.Size((double)sizeF.Width, (double)sizeF.Height);
return sizeF.Height + 5;
}
}
}
Here is the result:
I uploaded a sample here and you can check.

Add bottom line in android custom renderer code?

I know the default DatePicker already has a bottom line, but I'm trying to add a bottom line to the DatePicker in the custom renderer code (for some purpose).
I can set a full border of my GradientDrawable object by myGradientDrawable.SetStroke(3, myColor); but I don't know how to add only the bottom line so anyone can help me please?
Try this:
public class CustomPickerRenderer : PickerRenderer
{
public CustomPickerRenderer(Context context) : base(context)
{
}
private AlertDialog alert;
private CustomPicker element;
private int selectedIndex;
public LayerDrawable AddPickerStyles(string imagePath)
{
ColorDrawable borderColorDrawable = new ColorDrawable(Xamarin.Forms.Color.FromHex("#43addf").ToAndroid());
ColorDrawable backgroundColorDrawable = new ColorDrawable(Xamarin.Forms.Color.FromHex("#7e1b80").ToAndroid());
Drawable[] drawables = new Drawable[]
{
borderColorDrawable, backgroundColorDrawable
};
LayerDrawable layerDrawable = new LayerDrawable(drawables);
layerDrawable.SetLayerInset(1, 0, 0, 0, 5);
return layerDrawable;
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
element = (CustomPicker)this.Element;
if (Control != null && this.Element != null)
{
Control.Background = AddPickerStyles(element.Image);
}
}
}

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 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