Overview
In this article, I will show you how to implement an attached property for Xamarin.Forms.TableView to add support for bindable sections. It’s very useful when you want to keep your logic clean in ViewModel and change sections visibility dynamically.
This functionality is used in my mobile application Escape Rooms Notebook. On the visit details screen, I needed to hide empty cells and empty sections. For this purpose, I had to refresh TableView content when a user updates details. Below you can learn how it’s done.
In this sample I will apply MVVM pattern which is widely used in Xamarin.Forms applications.
Case study
1 2 3 4 5 6 7 8 9 |
public class Contact { public string FirstName { get; set; } public string LastName { get; set; } public string Company { get; set; } public int? YearOfBirth { get; set; } public int? Height { get; set; } public string Notes { get; set; } } |
Let’s assume that we have a Contact object and we want to display sections and rows only for non-empty properties. There is also a button to change contact, therefore a TableView has to rebuild sections after each change.
Case requirements:
- TableView should be able to display four sections:
- Basic Information (First Name, Last Name, Company)
- Personal Details (Year of Birth, Height)
- Notes
- Actions (“Next Contact” button)
- “Next Contact” button reloads TableView and changes sections dynamically
- If a contact property doesn’t have value, row is not displayed
- Section is displayed only if at least one property has value
1. Prepare models [Model]
We need classes to represent sections and rows. You can define your own row types, but in this example I will use these:
- TextValueRow – will represent a cell with title and value on the right side
- EditorRow – will represent a cell with multiline entry
- ButtonRow – will represent a cell with Button
Let’s define them:
1 2 3 4 5 |
// common interface for all row types // you can add some methods here if you need public interface ISectionRow { } |
1 2 3 4 5 |
public class TextValueRow : ISectionRow { public string Title { get; set; } public string Value { get; set; } } |
1 2 3 4 |
public class EditorRow : ISectionRow { public string Text { get; set; } } |
1 2 3 4 5 |
public class ButtonRow : ISectionRow { public string Title { get; set; } public Action OnClickAction { get; set; } } |
1 2 3 4 5 6 |
public class Section { public string Header { get; set; } public IList<isectionrow> Rows { get; } = new List<isectionrow>(); } </isectionrow></isectionrow> |
2. Create a view with TableView [View]
For now we will create a ContentPage with empty TableView:
1 2 3 4 5 |
<!--?xml version="1.0" encoding="utf-8"?--> <contentpage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="DynamicTableView.Views.ContactDetailsView"> <tableview intent="Form" backgroundcolor="#eee" hasunevenrows="true"> </tableview></contentpage> |
3. Build sections [ViewModel]
Defined models don’t use any class from Xamarin framework, therefore we can build TableView content even in regular .NET PCL. To make it simple, I will implement it in ViewModel. It will contain just a few sample contacts and one method to build and set sections list for non-empty properties.
Sections property will define how our TableView should look like.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
public class ContactDetailsViewModel : BindableObject { private List<contact> contacts; public ObservableCollection<section> Sections { get; private set; } public ContactDetailsViewModel() { this.SetUpContacts(); this.SetContact(0); } private void SetUpContacts() { this.contacts = new List<contact> { new Contact { FirstName = "Jack", LastName = "Smith", Company = "Google", Height = 183, Notes = "First contact note\nSecond line", YearOfBirth = 1985 }, new Contact { FirstName = "Jack", LastName = "Smith", }, new Contact { FirstName = "Jack", LastName = "Smith", Company = "Google", Height = 183, YearOfBirth = 1985 }, new Contact { Company = "Google" } }; } private void SetContact(int index) { var contact = this.contacts[index]; var basicSection = new Section { Header = "Basic Information" }; var personalSection = new Section { Header = "Personal Details" }; var notesSection = new Section { Header = "Notes" }; var buttonSection = new Section { Header = "Actions" }; // basic information this.AddTextRowIfNotEmpty(basicSection.Rows, "First Name", contact.FirstName); this.AddTextRowIfNotEmpty(basicSection.Rows, "Last Name", contact.LastName); this.AddTextRowIfNotEmpty(basicSection.Rows, "Company", contact.Company); // personal details this.AddTextRowIfNotEmpty(personalSection.Rows, "Year of Birth", contact.YearOfBirth?.ToString()); this.AddTextRowIfNotEmpty(personalSection.Rows, "Height", contact.Height?.ToString()); // notes if (!string.IsNullOrEmpty(contact.Notes)) { notesSection.Rows.Add(new EditorRow { Text = contact.Notes }); } // actions buttonSection.Rows.Add(new ButtonRow { Title = "Next Contact", OnClickAction = () => this.SetContact((index + 1) % this.contacts.Count) }); var sections = new List<section> { basicSection, personalSection, notesSection, buttonSection }; var nonEmptySections = sections.Where(x => x.Rows.Any()); this.Sections = new ObservableCollection<section>(nonEmptySections); // notify view, that sections have been changed this.OnPropertyChanged(nameof(this.Sections)); } private void AddTextRowIfNotEmpty(IList<isectionrow> rows, string title, string text) { if (!string.IsNullOrEmpty(text)) { rows.Add(new TextValueRow { Title = title, Value = text }); } } } </isectionrow></section></section></contact></section></contact> |
4. Create SectionsFactory
So far we’ve got a definition of our TableView in Sections property from ViewModel, but now we need to tell our application how to translate it into Cells, which TableView can display.
In order to keep our architecture clean and separate views and Xamarin framework from logic I extracted creating cells to another class. Here you can use passed properties in defined models to adjust each row and return custom cells.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
public static class SectionsFactory { public static TableSection CreateSection(Section section) { var tableSection = new TableSection(section.Header); foreach (var row in section.Rows) { if (row is TextValueRow) { tableSection.Add(GetTextValueRow(row as TextValueRow)); } else if (row is ButtonRow) { tableSection.Add(GetButtonRow(row as ButtonRow)); } else if (row is EditorRow) { tableSection.Add(GetEditorRow(row as EditorRow)); } } return tableSection; } private static Cell GetTextValueRow(TextValueRow row) { var grid = new Grid { BackgroundColor = Color.White, Padding = new Thickness(15, 0), HeightRequest = 45 }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3, GridUnitType.Star) }); grid.Children.Add(new Label { Text = row.Title, VerticalTextAlignment = TextAlignment.Center }, 0, 0); grid.Children.Add(new Label { Text = row.Value, TextColor = Color.DarkGray, VerticalTextAlignment = TextAlignment.Center }, 1, 0); return new ViewCell { View = grid }; } private static Cell GetButtonRow(ButtonRow row) { var button = new Button { Text = row.Title, BackgroundColor = Color.White, HeightRequest = 45 }; button.Clicked += (sender, e) => row.OnClickAction?.Invoke(); return new ViewCell { View = button }; } private static Cell GetEditorRow(EditorRow row) { return new ViewCell { Height = 140, View = new Grid { BackgroundColor = Color.White, Children = { new Editor { Text = row.Text, Margin = new Thickness(10, 0, 0, 0), IsEnabled = false } } } }; } } |
5. Create attached property
Finally, we implement the attached property, which will be used in XAML to bind sections from ViewModel to our TableView. In this method, I will use SectionsFactory.CreateSection to convert model into TableSection, which is supported by TableView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
namespace DynamicTableView.Extensions { public static class DynamicTableSections { public static readonly BindableProperty SectionsProperty = BindableProperty.CreateAttached("SectionsProperty", typeof(IList<section>), typeof(DynamicTableSections), null, BindingMode.OneWay, propertyChanged: SectionsChanged); private static void SectionsChanged(BindableObject source, object oldVal, object newVal) { // when sections change we need to rebuild our TableView content var tableView = (TableView)source; var newSections = (IList<section>)newVal; tableView.Root.Clear(); if (newSections == null) { return; } foreach (var section in newSections) { tableView.Root.Add(SectionsFactory.CreateSection(section)); } } } } </section></section> |
6. Bind all to ViewModel
Now we can use implemented attached property to bind Sections property from ViewModel to our View:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:ext="clr-namespace:DynamicTableView.Extensions" x:Class="DynamicTableView.Views.ContactDetailsView"> <TableView ext:DynamicTableSections.Sections="{Binding Sections}" Intent="Form" BackgroundColor="#eee" HasUnevenRows="true"/> </ContentPage> |
1 2 3 4 5 6 7 8 |
public partial class ContactDetailsView : ContentPage { public ContactDetailsView() { InitializeComponent(); this.BindingContext = new ContactDetailsViewModel(); } } |
Final result
This solution works both on iOS and Android. Below you can see how sections change dynamically after clicking “Next Contact” button.