I have this XAML. What I would like to do is to put a 1px line at the top and bottom of the grid with an iOS renderer. Can someone tell me is there a special way to put a border line just at the top and bottom of a grid using a renderer?
<Grid x:Name="phraseGrid" BackgroundColor="Transparent"
Margin="0,55,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="10*" />
<RowDefinition Height="6*" />
<RowDefinition Height="80*" />
<RowDefinition Height="13*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="prGrid" Grid.Row="0" Grid.Column="0"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="#EEEEEE">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="25*" />
<ColumnDefinition Width="25*" />
<ColumnDefinition Width="50*" />
</Grid.ColumnDefinitions>
<Label x:Name="cards" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="0" />
<Label x:Name="points" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="1" />
<Label x:Name="timer" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="2" />
</Grid>
From maintainability and complexity point of view, I would recommend that you create a couple of bindable properties and use it to render borders.
There are three options available to implement this:
1. Platform-renderer: Extend Grid
with properties and draw borders at platform level.
2. Forms control: Use Padding
and BackgroundColor
to give appearance of a border.
3. Platform-effect: Create a PlatformEffect
to render border (in this case we define attached bindable properties), and attach to any visual-element.
You can extend Grid
to create a custom control and implement its corresponding renderer. This code sample illustrates how to implement this using custom control approach.
Custom control implementation:
public class ExtendedGrid : Grid
{
/// <summary>
/// The border color property.
/// </summary>
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
"BorderColor", typeof(Color), typeof(ExtendedGrid),
defaultValue: Color.Black);
/// <summary>
/// Gets or sets the color of the border.
/// </summary>
/// <value>The color of the border.</value>
public Color BorderColor
{
get { return (Color)GetValue(BorderColorProperty); }
set { SetValue(BorderColorProperty, value); }
}
/// <summary>
/// The border width property.
/// </summary>
public static readonly BindableProperty BorderWidthProperty =
BindableProperty.Create(
"BorderWidth", typeof(Thickness), typeof(ExtendedGrid),
defaultValue: new Thickness(1));
/// <summary>
/// Gets or sets the width of the border.
/// </summary>
/// <value>The width of the border.</value>
public Thickness BorderWidth
{
get { return (Thickness)GetValue(BorderWidthProperty); }
set { SetValue(BorderWidthProperty, value); }
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if(nameof(Padding).Equals(propertyName) || nameof(BorderWidth).Equals(propertyName))
{
double minLeft, minRight, minTop, minBottom;
// ensure padding is always greater than borderwidth - we will have overlapping issue with client-area
minLeft = Math.Max(Padding.Left, BorderWidth.Left);
minRight = Math.Max(Padding.Right, BorderWidth.Right);
minTop = Math.Max(Padding.Top, BorderWidth.Top);
minBottom = Math.Max(Padding.Bottom, BorderWidth.Bottom);
var minPadding = new Thickness(minLeft, minTop, minRight, minBottom);
if (!minPadding.Equals(Padding)) //add this check to ensure we don't end up in a recursive loop
Padding = minPadding;
}
}
}
And, renderer can be implemented as:
[assembly: ExportRenderer(typeof(ExtendedGrid), typeof(ExtendedGridRenderer))]
namespace AppNamespace.iOS
{
public class ExtendedGridRenderer : VisualElementRenderer<ExtendedGrid>
{
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
//redraw border if any of these properties changed
if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == VisualElement.HeightProperty.PropertyName ||
e.PropertyName == ExtendedGrid.BorderWidthProperty.PropertyName ||
e.PropertyName == ExtendedGrid.BorderColorProperty.PropertyName)
SetNeedsDisplay();
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
var box = Element;
if (box == null)
return;
RemoveBorderLayers(); //remove previous layers - this can further be optimized.
CGColor lineColor = box.BorderColor.ToCGColor();
nfloat leftBorderWidth = new nfloat(box.BorderWidth.Left);
nfloat rightBorderWidth = new nfloat(box.BorderWidth.Right);
nfloat topBorderWidth = new nfloat(box.BorderWidth.Top);
nfloat bottomBorderWidth = new nfloat(box.BorderWidth.Bottom);
if(box.BorderWidth.Left > 0)
{
var leftBorderLayer = new BorderCALayer();
leftBorderLayer.BackgroundColor = lineColor;
leftBorderLayer.Frame = new CGRect(0, 0, leftBorderWidth, box.Height);
InsertBorderLayer(leftBorderLayer);
}
if (box.BorderWidth.Right > 0)
{
var rightBorderLayer = new BorderCALayer();
rightBorderLayer.BackgroundColor = lineColor;
rightBorderLayer.Frame = new CGRect(box.Width - box.BorderWidth.Right, 0, rightBorderWidth, box.Height);
InsertBorderLayer(rightBorderLayer);
}
if (box.BorderWidth.Top > 0)
{
var topBorderLayer = new BorderCALayer();
topBorderLayer.BackgroundColor = lineColor;
topBorderLayer.Frame = new CGRect(0, 0, box.Width, topBorderWidth);
InsertBorderLayer(topBorderLayer);
}
if (box.BorderWidth.Bottom > 0)
{
var bottomBorderLayer = new BorderCALayer();
bottomBorderLayer.BackgroundColor = lineColor;
bottomBorderLayer.Frame = new CGRect(0, box.Height - box.BorderWidth.Bottom, box.Width, bottomBorderWidth);
InsertBorderLayer(bottomBorderLayer);
}
}
void RemoveBorderLayers()
{
if (NativeView.Layer.Sublayers?.Length > 0)
{
var layers = NativeView.Layer.Sublayers.OfType<BorderCALayer>();
foreach(var layer in layers)
layer.RemoveFromSuperLayer();
}
}
void InsertBorderLayer(BorderCALayer layer)
{
var index = (NativeView.Layer.Sublayers?.Length > 0) ? NativeView.Layer.Sublayers.Length - 1 : 0;
//This is needed to get every background redrawn if the color changes on runtime
NativeView.Layer.InsertSublayer(layer, index);
}
}
public class BorderCALayer : CoreAnimation.CALayer { } //just create a type for easier replacement
}
Sample usage and output:
<Grid Margin="20">
<Grid x:Name="phraseGrid" BackgroundColor="Transparent"
Margin="0,55,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Grid.RowDefinitions>
<RowDefinition Height="10*" />
<RowDefinition Height="6*" />
<RowDefinition Height="80*" />
<RowDefinition Height="13*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:ExtendedGrid x:Name="prGrid1" Grid.Row="0" Grid.Column="0"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="#EEEEEE"
BorderColor="Gray"
BorderWidth="0,2,0,2">
<Label Text="only top and bottom set" Grid.Row="0" Grid.Column="0" />
</local:ExtendedGrid>
<local:ExtendedGrid x:Name="prGrid2" Grid.Row="1" Grid.Column="0"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Gray"
BorderColor="Blue"
BorderWidth="2">
<Label Text="all border set" Grid.Row="0" Grid.Column="0" />
</local:ExtendedGrid>
<local:ExtendedGrid x:Name="prGrid3" Grid.Row="2" Grid.Column="0"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Silver"
BorderColor="Red"
BorderWidth="0,2,0,2">
<Label Text="no horizontal borders" Grid.Row="0" Grid.Column="0" />
</local:ExtendedGrid>
</Grid>
</Grid>
If you don't want to get into hassle of implementing renderers for each platform - you can also create a custom control BorderView
as wrapper for rendering border at forms level itself (using a simple Padding
, and BackgroundColor
hack) and it should work on all platforms. The disadvantage is that it introduces an extra wrapper view for adding border, and child view can't have a transparent background.
BorderView implementation:
public class BorderView : ContentView
{
/// <summary>
/// The border color property.
/// </summary>
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(
"BorderColor", typeof(Color), typeof(BorderView),
defaultValue: Color.Black);
/// <summary>
/// Gets or sets the color of the border.
/// </summary>
/// <value>The color of the border.</value>
public Color BorderColor
{
get { return (Color)GetValue(BorderColorProperty); }
set { SetValue(BorderColorProperty, value); }
}
/// <summary>
/// The border width property.
/// </summary>
public static readonly BindableProperty BorderWidthProperty =
BindableProperty.Create(
"BorderWidth", typeof(Thickness), typeof(BorderView),
defaultValue: new Thickness(1));
/// <summary>
/// Gets or sets the width of the border.
/// </summary>
/// <value>The width of the border.</value>
public Thickness BorderWidth
{
get { return (Thickness)GetValue(BorderWidthProperty); }
set { SetValue(BorderWidthProperty, value); }
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (nameof(BorderColor).Equals(propertyName))
{
BackgroundColor = BorderColor;
}
if (nameof(BorderWidth).Equals(propertyName))
{
Padding = BorderWidth;
}
}
}
And sample usage (output is same as above image):
<local:BorderView Grid.Row="0" Grid.Column="0" BorderColor="Gray" BorderWidth="0,2,0,2">
<Grid x:Name="prGrid1"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="#EEEEEE">
<Label Text="only top and bottom set" Grid.Row="0" Grid.Column="0" />
</Grid>
</local:BorderView>
<local:BorderView Grid.Row="1" Grid.Column="0" BorderColor="Blue" BorderWidth="2">
<Grid x:Name="prGrid2"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Gray">
<Label Text="all border set" Grid.Row="0" Grid.Column="0" />
</Grid>
</local:BorderView>
<local:BorderView Grid.Row="2" Grid.Column="0" BorderColor="Red" BorderWidth="0,2,0,2">
<Grid x:Name="prGrid3"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Silver">
<Label Text="no horizontal borders" Grid.Row="0" Grid.Column="0" />
</Grid>
</local:BorderView>
</Grid>
</Grid>
Another option is to create a custom PlatformEffect
and a couple of attached bindable properties to implement border for any visual control.
Attached properties and effect (portable/shared code):
public class VisualElementBorderEffect : RoutingEffect
{
public VisualElementBorderEffect() : base("MyCompany.VisualElementBorderEffect")
{
}
}
public static class BorderEffect
{
public static readonly BindableProperty HasBorderProperty =
BindableProperty.CreateAttached("HasBorder", typeof(bool), typeof(BorderEffect), false, propertyChanged: OnHasBorderChanged);
public static readonly BindableProperty ColorProperty =
BindableProperty.CreateAttached("Color", typeof(Color), typeof(BorderEffect), Color.Default);
public static readonly BindableProperty WidthProperty =
BindableProperty.CreateAttached("Width", typeof(Thickness), typeof(BorderEffect), new Thickness(0));
public static bool GetHasBorder(BindableObject view)
{
return (bool)view.GetValue(HasBorderProperty);
}
public static void SetHasBorder(BindableObject view, bool value)
{
view.SetValue(HasBorderProperty, value);
}
public static Color GetColor(BindableObject view)
{
return (Color)view.GetValue(ColorProperty);
}
public static void SetColor(BindableObject view, Color value)
{
view.SetValue(ColorProperty, value);
}
public static Thickness GetWidth(BindableObject view)
{
return (Thickness)view.GetValue(WidthProperty);
}
public static void SetWidth(BindableObject view, Thickness value)
{
view.SetValue(WidthProperty, value);
}
static void OnHasBorderChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as View;
if (view == null)
{
return;
}
bool hasBorder = (bool)newValue;
if (hasBorder)
{
view.Effects.Add(new VisualElementBorderEffect());
}
else
{
var toRemove = view.Effects.FirstOrDefault(e => e is VisualElementBorderEffect);
if (toRemove != null)
{
view.Effects.Remove(toRemove);
}
}
}
}
Platform effect for iOS:
[assembly: ResolutionGroupName("MyCompany")]
[assembly: ExportEffect(typeof(VisualElementBorderEffect), "VisualElementBorderEffect")]
namespace AppNamespace.iOS
{
public class BorderCALayer : CoreAnimation.CALayer { } //just create a type for easier replacement
public class VisualElementBorderEffect : PlatformEffect
{
protected override void OnAttached()
{
//no need to do anything here - we wait for size update to draw border
}
protected override void OnDetached()
{
RemoveBorderLayers();
}
void UpdateBorderLayers()
{
var box = Element as View;
if (box == null)
return;
RemoveBorderLayers(); //remove previous layers - this can further be optimized.
CGColor lineColor = BorderEffect.GetColor(Element).ToCGColor();
var borderWidth = BorderEffect.GetWidth(Element);
nfloat leftBorderWidth = new nfloat(borderWidth.Left);
nfloat rightBorderWidth = new nfloat(borderWidth.Right);
nfloat topBorderWidth = new nfloat(borderWidth.Top);
nfloat bottomBorderWidth = new nfloat(borderWidth.Bottom);
if (borderWidth.Left > 0)
{
var leftBorderLayer = new BorderCALayer();
leftBorderLayer.BackgroundColor = lineColor;
leftBorderLayer.Frame = new CGRect(0, 0, leftBorderWidth, box.Height);
InsertBorderLayer(leftBorderLayer);
}
if (borderWidth.Right > 0)
{
var rightBorderLayer = new BorderCALayer();
rightBorderLayer.BackgroundColor = lineColor;
rightBorderLayer.Frame = new CGRect(box.Width - borderWidth.Right, 0, rightBorderWidth, box.Height);
InsertBorderLayer(rightBorderLayer);
}
if (borderWidth.Top > 0)
{
var topBorderLayer = new BorderCALayer();
topBorderLayer.BackgroundColor = lineColor;
topBorderLayer.Frame = new CGRect(0, 0, box.Width, topBorderWidth);
InsertBorderLayer(topBorderLayer);
}
if (borderWidth.Bottom > 0)
{
var bottomBorderLayer = new BorderCALayer();
bottomBorderLayer.BackgroundColor = lineColor;
bottomBorderLayer.Frame = new CGRect(0, box.Height - borderWidth.Bottom, box.Width, bottomBorderWidth);
InsertBorderLayer(bottomBorderLayer);
}
}
void RemoveBorderLayers()
{
if ((Control ?? Container).Layer.Sublayers?.Length > 0)
{
var layers = (Control ?? Container).Layer.Sublayers.OfType<BorderCALayer>();
foreach (var layer in layers)
layer.RemoveFromSuperLayer();
}
}
void InsertBorderLayer(BorderCALayer layer)
{
var native = (Control ?? Container);
var index = (native.Layer.Sublayers?.Length > 0) ? native.Layer.Sublayers.Length - 1 : 0;
//This is needed to get every background redrawn if the color changes on runtime
native.Layer.InsertSublayer(layer, index);
}
protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(e);
//redraw border if any of these properties changed
if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == VisualElement.HeightProperty.PropertyName)
{
if(IsAttached && (Control != null || Container != null))
{
RemoveBorderLayers();
UpdateBorderLayers();
(Control ?? Container).SetNeedsDisplay();
}
}
}
}
}
And sample code and output:
<StackLayout Margin="20">
<Grid x:Name="phraseGrid" BackgroundColor="Transparent"
Margin="0,55,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="10*" />
<RowDefinition Height="6*" />
<RowDefinition Height="80*" />
<RowDefinition Height="13*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid x:Name="prGrid1" Grid.Row="0" Grid.Column="0"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="#EEEEEE"
local:BorderEffect.HasBorder="true"
local:BorderEffect.Color="Gray"
local:BorderEffect.Width="0,2,0,2">
<Label Text="grid with only top and bottom border set" Grid.Row="0" Grid.Column="0" />
</Grid>
<Grid x:Name="prGrid2" Grid.Row="1" Grid.Column="0"
Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Gray"
local:BorderEffect.HasBorder="true"
local:BorderEffect.Color="Blue"
local:BorderEffect.Width="2">
<Label Text="grid with all border set" Grid.Row="0" Grid.Column="0" />
</Grid>
<Grid x:Name="prGrid3" Grid.Row="2" Grid.Column="0"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
BackgroundColor="Silver"
local:BorderEffect.HasBorder="true"
local:BorderEffect.Color="Red"
local:BorderEffect.Width="0,2,0,2">
<Label Text="grid with no horizontal borders" Grid.Row="0" Grid.Column="0" />
<Label local:BorderEffect.HasBorder="true"
local:BorderEffect.Color="Maroon"
local:BorderEffect.Width="0,2,0,2"
Text="label with maroon border"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
</Grid>
</StackLayout>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With