I am wanting to create a page layout where I display multiple TableView
s along with other views such as a Label
in between each.
In doing this I need the ability to:
- Make the
TableView
not default to a vertically expanded layout option (the default which does not seem changeable) so that they can be stacked in a StackLayout
vertically.
- Hide the corresponding
UITableView
section header/footers so that there isn't excess vertical spacing between.
I have achieved this with the following CustomTableView
control:
public class CustomTableView : TableView
{
public bool NoHeader { get; set; }
public bool NoFooter { get; set; }
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
if(!VerticalOptions.Expands)
{
// Call OnSizeRequest that is overwritten in custom renderer
var baseOnSizeRequest = GetVisualElementOnSizeRequest();
return baseOnSizeRequest(widthConstraint, heightConstraint);
}
return base.OnSizeRequest(widthConstraint, heightConstraint);
}
public Func<double, double, SizeRequest> GetVisualElementOnSizeRequest()
{
var handle = typeof(VisualElement).GetMethod(
"OnSizeRequest",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new Type[] { typeof(double), typeof(double) },
null)?.MethodHandle;
var pointer = handle.Value.GetFunctionPointer();
return (Func<double, double, SizeRequest>)Activator.CreateInstance(
typeof(Func<double, double, SizeRequest>), this, pointer);
}
}
and the custom iOS renderer for this which follows the advise from this SO answer for hiding UITableView
headers/footers:
public class CustomTableViewRenderer : TableViewRenderer
{
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
{
Control.LayoutIfNeeded();
var size = new Size(Control.ContentSize.Width, Control.ContentSize.Height);
return new SizeRequest(size);
}
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if(e.NewElement is CustomTableView view)
{
Control.ScrollEnabled = false;
SetSource();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var view = (CustomTableView)Element;
if(e.PropertyName == TableView.HasUnevenRowsProperty.PropertyName)
{
SetSource();
}
}
private void SetSource()
{
var view = (CustomTableView)Element;
if(view.NoFooter || view.NoHeader)
{
Control.Source = new CustomTableViewModelRenderer(view);
}
else
{
Control.Source = Element.HasUnevenRows ? new UnEvenTableViewModelRenderer(Element) :
new TableViewModelRenderer(Element);
}
}
public class CustomTableViewModelRenderer : UnEvenTableViewModelRenderer
{
private readonly CustomTableView _view;
public CustomTableViewModelRenderer(CustomTableView model)
: base(model)
{
_view = model;
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
if(_view.HasUnevenRows)
{
return UITableView.AutomaticDimension;
}
return base.GetHeightForRow(tableView, indexPath);
}
public override nfloat GetHeightForHeader(UITableView tableView, nint section)
{
if(_view.NoHeader)
{
return 0.00001f;
}
return base.GetHeightForHeader(tableView, section);
}
public override UIView GetViewForHeader(UITableView tableView, nint section)
{
if(_view.NoHeader)
{
return new UIView(new CGRect(0, 0, 0, 0));
}
return base.GetViewForHeader(tableView, section);
}
public override nfloat GetHeightForFooter(UITableView tableView, nint section)
{
if(_view.NoFooter)
{
return 0.00001f;
}
return 10f;
}
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
if(_view.NoFooter)
{
return new UIView(new CGRect(0, 0, 0, 0));
}
return base.GetViewForFooter(tableView, section);
}
}
}
I then create a simple Xamarin.Forms
ContentPage
to implement this as such:
public class TablePage : ContentPage
{
public TablePage()
{
Table1 = new PageTableView
{
BackgroundColor = Color.Cyan,
Root = new TableRoot
{
new TableSection("Table 1")
{
new LabelEntryTableCell("Label A"),
new LabelEntryTableCell("Label B")
}
}
};
var label1 = new Label
{
Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Vivamus accumsan lacus orci. Nulla in enim erat.",
Margin = new Thickness(15, 5, 15, 0),
BackgroundColor = Color.Yellow
};
Table2 = new PageTableView
{
BackgroundColor = Color.Red,
Root = new TableRoot
{
new TableSection("Table 2")
{
new LabelEntryTableCell("Label C"),
new LabelEntryTableCell("Label D")
}
}
};
var label2 = new Label
{
Text = "Duis vulputate mattis elit. " +
"Donec vulputate lorem vitae elit posuere, quis consequat ligula imperdiet.",
Margin = new Thickness(15, 5, 15, 0),
BackgroundColor = Color.Green
};
StackLayout = new StackLayout
{
Children = { Table1, label1, Table2, label2 },
Spacing = 0
};
BackgroundColor = Color.Gray;
Title = "Custom Table View";
Content = new ScrollView
{
Content = StackLayout
};
}
public PageTableView Table1 { get; set; }
public PageTableView Table2 { get; set; }
public StackLayout StackLayout { get; set; }
}
public class PageTableView : CustomTableView
{
public PageTableView()
{
Intent = TableIntent.Settings;
HasUnevenRows = true;
RowHeight = -1;
VerticalOptions = LayoutOptions.Start;
NoFooter = true;
}
}
public class LabelEntryTableCell : ViewCell
{
public LabelEntryTableCell(string labelText)
{
var label = new Label { Text = labelText };
var entry = new Entry();
View = new StackLayout
{
Children = { label, entry },
BackgroundColor = Color.White,
Padding = 10
};
}
}
This all seems to work as expected, however, when the TablePage
first displays much of the CustomTableView
content is chopped off. I can then rotate the device to landscape and the problem corrects itself, showing my layout as I intend. Then rotating then device back to portrait still shows the correct layout with no more information from the CustomTableView
being chopped off anymore in that orientation either. This happens consistently every time. See a demo of this here:
I can also fix the problem by adding this to the TablePage
:
protected override void OnAppearing()
{
base.OnAppearing();
// This will also fix the problem, but you see a flash of the old layout
// when the page first displays.
var l = new Label();
StackLayout.Children.Add(l);
StackLayout.Children.Remove(l);
}
How do I correct this problem? My custom control/renderer/page works as intended whenever I do the rotation/OnAppearing hack to correct it and I am not sure why. It seems like I need to call some re-draw method on the UITableView
.
Sample Project
If you want to see the full source/project for this isolated test scenario you can find it on GitHub here.