using System; using System.Collections; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests; public sealed class TableViewTests : ScopedTestBase { [Fact] public void Container_For_Each_Item_Is_TableViewRow() { using var app = Start(); var target = CreateTarget(new[] { "Foo", "Bar", "Baz" }); Prepare(target); var containers = target.GetRealizedContainers().ToList(); Assert.Equal(3, containers.Count); Assert.All(containers, c => Assert.IsType(c)); } [Fact] public void Row_Has_One_Cell_Per_Column() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn()); target.Columns.Add(new TableViewColumn()); target.Columns.Add(new TableViewColumn()); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cells = GetCellsPresenter(row).Children; Assert.Equal(3, cells.Count); Assert.All(cells, c => Assert.IsType(c)); // Cells must also be part of the row's logical tree. var logicalChildren = row.GetLogicalChildren().ToArray(); Assert.Equal(cells, logicalChildren); } [Fact] public void Column_TableView_Is_Set_When_Added_And_Unset_When_Removed() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; target.Columns.Add(column); Prepare(target); Assert.Same(target, column.TableView); target.Columns.Remove(column); Assert.Null(column.TableView); } [Fact] public void Column_LogicalParent_Is_Set_When_Added_And_Unset_When_Removed() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); var column = new TableViewColumn(); target.Columns.Add(column); Prepare(target); Assert.Same(target, column.GetLogicalParent()); target.Columns.Remove(column); Assert.Null(column.GetLogicalParent()); } [Fact] public void Column_Receives_Styles_From_TableView() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); var style = new Style(x => x.OfType().Class("right-align")) { Setters = { new Setter(TableViewColumn.HorizontalContentAlignmentProperty, HorizontalAlignment.Right) } }; target.Styles.Add(style); var column = new TableViewColumn { Classes = { "right-align" } }; target.Columns.Add(column); Prepare(target); Assert.Equal(HorizontalAlignment.Right, column.HorizontalContentAlignment); } [Fact] public void Column_Cannot_Belong_To_Two_TableViews() { using var app = Start(); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var first = CreateTarget(new[] { "Foo" }); first.Columns.Add(column); Prepare(first); Assert.Same(first, column.TableView); var second = CreateTarget(new[] { "Bar" }); second.Columns.Add(column); Assert.Throws(() => Prepare(second)); } [Fact] public void Adding_Column_Adds_Cell_To_Realized_Rows_And_Headers() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cellsPresenter = GetCellsPresenter(row); Assert.Equal(2, cellsPresenter.Children.Count); var headersPresenter = GetColumnHeadersPresenter(target); Assert.Equal(2, headersPresenter.Children.Count); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Assert.Equal(3, cellsPresenter.Children.Count); Assert.Equal(3, headersPresenter.Children.Count); // The newly added cell must also be part of the row's logical tree. var logicalChildren = row.GetLogicalChildren().ToArray(); Assert.Equal(cellsPresenter.Children, logicalChildren); } [Theory] [InlineData(0)] [InlineData(1)] public void Adding_Hidden_Column_Updates_Cells_And_Headers(int columnIndex) { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Styles.Add(new Style(x => x.OfType().Class("hidden")) { Setters = { new Setter(TableViewColumn.IsVisibleProperty, false) }, }); target.Columns.Add(new TableViewColumn()); Prepare(target); var column = new TableViewColumn { Classes = { "hidden" } }; target.Columns.Insert(columnIndex, column); Layout(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); Assert.False(GetCellsPresenter(row).Children[columnIndex].IsVisible); Assert.False(GetColumnHeadersPresenter(target).Children[columnIndex].IsVisible); Assert.True(GetCellsPresenter(row).Children[1 - columnIndex].IsVisible); Assert.True(GetColumnHeadersPresenter(target).Children[1 - columnIndex].IsVisible); } [Fact] public void Removing_Column_Removes_Cell_From_Realized_Rows_And_Headers() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cellsPresenter = GetCellsPresenter(row); Assert.Equal(3, cellsPresenter.Children.Count); var headersPresenter = GetColumnHeadersPresenter(target); Assert.Equal(3, headersPresenter.Children.Count); var removedCell = (TableViewCell)cellsPresenter.Children[2]; target.Columns.RemoveAt(2); Assert.Equal(2, cellsPresenter.Children.Count); Assert.Equal(2, headersPresenter.Children.Count); // The removed cell must also be detached from the row's logical tree, while the remaining cells stay in it. var logicalChildren = row.GetLogicalChildren().ToArray(); Assert.DoesNotContain(removedCell, logicalChildren); Assert.Equal(cellsPresenter.Children, logicalChildren); } [Fact] public void Changing_Column_Width_Does_Not_Recreate_Cells() { using var app = Start(); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(column); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target, width: 200); var presenter = GetCellsPresenter((TableViewRow)target.GetRealizedContainers().Single()); var cellsBefore = presenter.Children.ToArray(); column.Width = new GridLength(2, GridUnitType.Star); Layout(target); var cellsAfter = presenter.Children.ToArray(); Assert.Equal(cellsBefore, cellsAfter); } [Fact] public void Changing_Column_Width_Invalidates_Row_Presenter_Measure() { using var app = Start(); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(column); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target, width: 200); var presenter = GetCellsPresenter((TableViewRow)target.GetRealizedContainers().Single()); Assert.True(presenter.IsMeasureValid); column.Width = new GridLength(2, GridUnitType.Star); Assert.False(presenter.IsMeasureValid); } [Fact] public void Changing_Column_Width_Updates_ActualWidth_After_Layout() { using var app = Start(); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(column); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target, width: 200); Assert.Equal(100, target.Columns[0].ActualWidth); Assert.Equal(100, target.Columns[1].ActualWidth); column.Width = new GridLength(3, GridUnitType.Star); Layout(target); Assert.Equal(150, target.Columns[0].ActualWidth); Assert.Equal(50, target.Columns[1].ActualWidth); } [Fact] public void Hiding_Column_Closes_Gap_Without_Recreating_Controls() { using var app = Start(); var column = new TableViewColumn { Width = new GridLength(80) }; var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(column); target.Columns.Add(new TableViewColumn()); Prepare(target, width: 200); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cells = GetCellsPresenter(row); var headers = GetColumnHeadersPresenter(target); var originalCells = cells.Children.ToArray(); var originalHeaders = headers.Children.ToArray(); column.IsVisible = false; Layout(target); Assert.False(cells.Children[0].IsVisible); Assert.False(headers.Children[0].IsVisible); Assert.Equal(0, cells.Children[1].Bounds.X); Assert.Equal(0, headers.Children[1].Bounds.X); Assert.Equal(200, target.Columns[1].ActualWidth); Assert.Equal(0, column.ActualWidth); Assert.Equal(new GridLength(80), column.Width); column.IsVisible = true; Layout(target); Assert.True(cells.Children[0].IsVisible); Assert.True(headers.Children[0].IsVisible); Assert.Equal(80, cells.Children[1].Bounds.X); Assert.Equal(80, headers.Children[1].Bounds.X); Assert.Equal(originalCells, cells.Children); Assert.Equal(originalHeaders, headers.Children); } [Fact] public void Hidden_Columns_Do_Not_Contribute_To_Row_Size() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); var column = new TableViewColumn { IsVisible = false }; target.Columns.Add(column); target.Columns.Add(new TableViewColumn()); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cells = GetCellsPresenter(row); cells.Children[0].Height = 80; cells.Children[1].Height = 20; Layout(target); Assert.False(cells.Children[0].IsVisible); Assert.False(GetColumnHeadersPresenter(target).Children[0].IsVisible); Assert.Equal(20, cells.DesiredSize.Height); column.IsVisible = true; Layout(target); Assert.Equal(80, cells.DesiredSize.Height); column.IsVisible = false; target.Columns[1].IsVisible = false; Layout(target); Assert.Equal(default, cells.DesiredSize); column.IsVisible = true; Layout(target); Assert.Equal(80, cells.DesiredSize.Height); } [Fact] public void Replacing_Columns_Collection_Updates_Realized_Rows_And_Headers() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cellsPresenter = GetCellsPresenter(row); Assert.Equal(2, cellsPresenter.Children.Count); var headersPresenter = GetColumnHeadersPresenter(target); Assert.Equal(2, headersPresenter.Children.Count); var oldCells = cellsPresenter.Children.ToArray(); target.Columns = [ new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }, new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }, new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) } ]; Assert.Equal(3, cellsPresenter.Children.Count); Assert.Equal(3, headersPresenter.Children.Count); // The new cells must be in the row's logical tree, and the old ones gone. var logicalChildren = row.GetLogicalChildren().ToArray(); Assert.Equal(cellsPresenter.Children, logicalChildren); Assert.All(oldCells, cell => Assert.DoesNotContain(cell, logicalChildren)); } [Fact] public void Cell_Content_Defaults_To_Row_Item_When_No_Template_Or_Binding() { using var app = Start(); var item = new Person("Alice"); var target = CreateTarget(new[] { item }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var firstCell = (TableViewCell)GetCellsPresenter(row).Children[0]; Assert.Same(item, firstCell.Content); } [Fact] public void Cell_Uses_Column_Binding() { using var app = Start(); var target = CreateTarget(new[] { new Person("Alice"), new Person("Bob") }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star), Binding = new ReflectionBinding(nameof(Person.Name)) }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var rows = target.GetRealizedContainers().Cast().ToArray(); var firstCell = (TableViewCell)GetCellsPresenter(rows[0]).Children[0]; var secondCell = (TableViewCell)GetCellsPresenter(rows[1]).Children[0]; Assert.Equal("Alice", firstCell.Content); Assert.Equal("Bob", secondCell.Content); } [Fact] public void Hidden_Column_Preserves_Width_And_Visibility_Bindings() { using var app = Start(); var model = new Border { IsVisible = false, Tag = new GridLength(80) }; var target = CreateTarget(new[] { "Foo" }); target.DataContext = model; var column = new TableViewColumn(); column.Bind(TableViewColumn.WidthProperty, new ReflectionBinding(nameof(Border.Tag))); column.Bind(TableViewColumn.IsVisibleProperty, new ReflectionBinding(nameof(Border.IsVisible))); target.Columns.Add(column); target.Columns.Add(new TableViewColumn()); Prepare(target, width: 200); Assert.False(column.IsVisible); Assert.Equal(200, target.Columns[1].ActualWidth); model.Tag = new GridLength(120); model.IsVisible = true; Layout(target); Assert.True(column.IsVisible); Assert.Equal(120, column.ActualWidth); Assert.Equal(80, target.Columns[1].ActualWidth); model.IsVisible = false; Layout(target); Assert.False(column.IsVisible); Assert.Equal(200, target.Columns[1].ActualWidth); } [Fact] public void Cell_Uses_Column_CellTemplate() { using var app = Start(); var template = new FuncDataTemplate((_, _) => new TextBlock()); var item = new Person("Alice"); var target = CreateTarget(new[] { item }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star), CellTemplate = template }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var firstCell = (TableViewCell)GetCellsPresenter(row).Children[0]; Assert.Same(template, firstCell.ContentTemplate); Assert.Same(item, firstCell.Content); } [Fact] public void Cell_Uses_Column_CellTheme() { using var app = Start(); var cellTheme = new ControlTheme(typeof(TableViewCell)); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star), CellTheme = cellTheme }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var firstCell = (TableViewCell)GetCellsPresenter(row).Children[0]; var secondCell = (TableViewCell)GetCellsPresenter(row).Children[1]; Assert.Same(cellTheme, firstCell.Theme); Assert.Null(secondCell.Theme); } [Fact] public void Cell_Column_Property_Is_Set_To_Owning_Column() { using var app = Start(); var column0 = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var column1 = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }; var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(column0); target.Columns.Add(column1); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var firstCell = (TableViewCell)GetCellsPresenter(row).Children[0]; var secondCell = (TableViewCell)GetCellsPresenter(row).Children[1]; Assert.Same(column0, firstCell.Column); Assert.Same(column1, secondCell.Column); } [Fact] public void Cell_Uses_Column_HorizontalContentAlignment() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star), HorizontalContentAlignment = HorizontalAlignment.Right }); target.Columns.Add(new TableViewColumn { Width = new GridLength(1, GridUnitType.Star) }); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var firstCell = (TableViewCell)GetCellsPresenter(row).Children[0]; var secondCell = (TableViewCell)GetCellsPresenter(row).Children[1]; Assert.Equal(HorizontalAlignment.Right, firstCell.HorizontalContentAlignment); Assert.Equal(HorizontalAlignment.Left, secondCell.HorizontalContentAlignment); } [Fact] public void Changing_Column_Properties_Updates_Existing_Headers_And_Cells() { using var app = Start(); var cellThemeA = new ControlTheme(typeof(TableViewCell)); var cellThemeB = new ControlTheme(typeof(TableViewCell)); var headerThemeA = new ControlTheme(typeof(TableViewColumnHeader)); var headerThemeB = new ControlTheme(typeof(TableViewColumnHeader)); var headerTemplateA = new FuncDataTemplate((_, _) => new TextBlock()); var headerTemplateB = new FuncDataTemplate((_, _) => new TextBlock()); var item = new Person("Alice", "Ally"); var column = new TableViewColumn { Width = new GridLength(1, GridUnitType.Star), CellTheme = cellThemeA, HorizontalContentAlignment = HorizontalAlignment.Left, Binding = new ReflectionBinding(nameof(Person.Name)), Header = "H1", HeaderTheme = headerThemeA, HeaderTemplate = headerTemplateA }; var target = CreateTarget(new[] { item }); target.Columns.Add(column); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var cell = (TableViewCell)GetCellsPresenter(row).Children[0]; var header = (TableViewColumnHeader)GetColumnHeadersPresenter(target).Children[0]; Assert.Same(cellThemeA, cell.Theme); Assert.Equal(HorizontalAlignment.Left, cell.HorizontalContentAlignment); Assert.Null(cell.ContentTemplate); Assert.Equal("Alice", cell.Content); Assert.Same(headerThemeA, header.Theme); Assert.Equal(HorizontalAlignment.Left, header.HorizontalContentAlignment); Assert.Same(headerTemplateA, header.ContentTemplate); Assert.Equal("H1", header.Content); // Mutating the column after the cell and header were built should update both. // Switch the Binding first to confirm it's reflected in the cell content. column.CellTheme = cellThemeB; column.HorizontalContentAlignment = HorizontalAlignment.Right; column.Binding = new ReflectionBinding(nameof(Person.Nickname)); column.Header = "H2"; column.HeaderTheme = headerThemeB; column.HeaderTemplate = headerTemplateB; Assert.Same(cellThemeB, cell.Theme); Assert.Equal(HorizontalAlignment.Right, cell.HorizontalContentAlignment); Assert.Null(cell.ContentTemplate); Assert.Equal("Ally", cell.Content); Assert.Same(headerThemeB, header.Theme); Assert.Equal(HorizontalAlignment.Right, header.HorizontalContentAlignment); Assert.Same(headerTemplateB, header.ContentTemplate); Assert.Equal("H2", header.Content); // CellTemplate takes priority over Binding: the row item flows through the template. var cellTemplateB = new FuncDataTemplate((_, _) => new TextBlock()); column.CellTemplate = cellTemplateB; Assert.Same(cellTemplateB, cell.ContentTemplate); Assert.Same(item, cell.Content); } [Theory] [InlineData(true)] [InlineData(false)] public void Re_Templating_Row_Detaches_Old_Cells_And_Rebuilds_New_Cells(bool isVisible) { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.Columns.Add(new TableViewColumn { IsVisible = isVisible }); target.Columns.Add(new TableViewColumn()); Prepare(target); var row = (TableViewRow)target.GetRealizedContainers().Single(); var oldCells = GetCellsPresenter(row).Children.ToArray(); Assert.Equal(2, oldCells.Length); Assert.Equal(oldCells, row.GetLogicalChildren()); row.Template = RowTemplate(); row.ApplyTemplate(); var newCells = GetCellsPresenter(row).Children.ToArray(); Assert.Equal(2, newCells.Length); Assert.Equal(isVisible, newCells[0].IsVisible); Assert.True(newCells[1].IsVisible); Assert.All(oldCells, cell => Assert.Null(cell.Parent)); Assert.Equal(newCells, row.GetLogicalChildren()); } [Fact] public void CanUserResizeColumns_Defaults_To_True() { Assert.True(new TableView().CanUserResizeColumns); } [Theory] [InlineData(true, null, true)] // inherits the table's value [InlineData(true, true, true)] // column opts in [InlineData(true, false, false)] // column opts out [InlineData(false, null, false)] // inherits the table's value [InlineData(false, true, true)] // column opts in [InlineData(false, false, false)] // column opts out public void Column_CanUserEffectivelyResize_Combines_TableView_And_Column_Settings( bool canResizeColumns, bool? canResize, bool expected) { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.CanUserResizeColumns = canResizeColumns; var column = new TableViewColumn { CanUserResize = canResize }; target.Columns.Add(column); Prepare(target); Assert.Equal(expected, column.CanUserEffectivelyResize); } [Fact] public void Column_CanUserEffectivelyResize_Updates_When_TableView_CanResizeColumns_Changes() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); var column = new TableViewColumn(); target.Columns.Add(column); Prepare(target); Assert.True(column.CanUserEffectivelyResize); target.CanUserResizeColumns = false; Assert.False(column.CanUserEffectivelyResize); target.CanUserResizeColumns = true; Assert.True(column.CanUserEffectivelyResize); } [Fact] public void Column_CanUserEffectivelyResize_Updates_When_Column_CanResize_Changes() { using var app = Start(); var target = CreateTarget(new[] { "Foo" }); target.CanUserResizeColumns = false; var column = new TableViewColumn(); target.Columns.Add(column); Prepare(target); Assert.False(column.CanUserEffectivelyResize); column.CanUserResize = true; Assert.True(column.CanUserEffectivelyResize); column.CanUserResize = null; Assert.False(column.CanUserEffectivelyResize); } private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); private static TableView CreateTarget(IEnumerable items) => new() { Template = TableViewTemplate(), ItemContainerTheme = TableViewRowTheme(), ItemsSource = items }; private static TableViewCellsPresenter GetCellsPresenter(TableViewRow row) { var presenter = row.GetVisualDescendants() .OfType() .FirstOrDefault(); Assert.NotNull(presenter); return presenter; } private static TableViewColumnHeadersPresenter GetColumnHeadersPresenter(TableView target) { var presenter = target.GetVisualDescendants() .OfType() .FirstOrDefault(); Assert.NotNull(presenter); return presenter; } private static void Prepare(TableView target, double width = 300, double height = 200) { target.Width = width; target.Height = height; var root = new TestRoot(target); root.LayoutManager.ExecuteInitialLayoutPass(); } private static void Layout(Control control) => control.GetLayoutManager()?.ExecuteLayoutPass(); private static FuncControlTemplate TableViewTemplate() => new FuncControlTemplate((parent, scope) => new DockPanel { Children = { new TableViewColumnHeadersPresenter { [DockPanel.DockProperty] = Dock.Top }, new ScrollViewer { Name = "PART_ScrollViewer", Template = ScrollViewerTemplate(), Content = new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding() }.RegisterInNameScope(scope) }.RegisterInNameScope(scope) } }); private static FuncControlTemplate ScrollViewerTemplate() => new FuncControlTemplate((_, scope) => new Panel { Children = { new ScrollContentPresenter { Name = "PART_ContentPresenter" }.RegisterInNameScope(scope) } }); private static ControlTheme TableViewRowTheme() => new(typeof(TableViewRow)) { Setters = { new Setter(TemplatedControl.TemplateProperty, RowTemplate()) } }; private static FuncControlTemplate RowTemplate() => new((_, scope) => new TableViewCellsPresenter { Name = "PART_CellsPresenter" }.RegisterInNameScope(scope)); private class Person { public Person(string name, string? nickname = null) { Name = name; Nickname = nickname; } public string Name { get; } public string? Nickname { get; } } }