Skip to content

Latest commit

 

History

History
236 lines (200 loc) · 15.3 KB

File metadata and controls

236 lines (200 loc) · 15.3 KB

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

First working version of the library. Nothing has been tagged yet, so the repository builds as 0.1.0-dev.N, where N is the number of commits.

The API is not committed to until 1.0.0. It is taking its shape against a real application — an attribute table over the geometries of a loaded layer — and the 0.1.0-dev.* packages are where it is allowed to change.

Added

The control

  • DataGrid, a read-only column-driven table: a header row and virtualized data rows laid out from one solved set of column widths. Named DataGrid and not Grid because Grid is a WinUI panel, and two things by that name in one XAML file is a collision nobody should have to live with.
  • The body is a WinUI ListView. Vertical virtualization, keyboard navigation, the focus, mouse wheel and touch panning, and the whole automation tree below the grid are the platform's rather than this library's. What is written here is what WinUI is missing: shared column widths, a header strip that scrolls with the body, and a column model.
  • ItemsSource, SelectedItem, SelectedIndex, SelectionMode, ShowColumnHeaders, ShowRowSeparators, AllowColumnResizing, HeaderRowHeight, RowHeight, EmptyContent and EmptyContentTemplate, and ScrollIntoView.
  • SelectionChanged, CellTapped and CellDoubleTapped. The two cell events carry the record and not only an index, because an application that acts on the thing a row stands for would otherwise have to keep a copy of the collection to look it up in.

Columns

  • DataGridColumn, DataGridTextColumn and DataGridTemplateColumn. A column is a description rather than a control: it is declared once and the grid builds one cell per realized row from it, so a column costs the same whether the grid is showing ten rows or a hundred thousand.
  • MappingName resolves properties (Name), dotted paths (Geometry.Length) and indexers (Cells[Height]), and chains of the three. The indexer form is the one the library exists for: a grid over the fields of a file has no typed row class to bind to, because the columns are only known once the file is open. A name nothing on the row answers to leaves the cell empty rather than throwing, so a grid can be pointed at a heterogeneous collection.
  • The same syntax is answered twice, on purpose. What a cell draws comes from a WinUI Binding, so that a cell follows a row that raises PropertyChanged; what UI Automation reports comes from this library's own parser, because a peer has to name a whole row - template columns included - and reading that back out of the elements a template happened to build gives nothing worth reading aloud. The tests are over the second, which is the half that can be exercised without a XAML runtime.
  • Width is a GridLength, so Width="40", Width="Auto" and Width="*" go in one property. That replaces the pair grids usually need - a global width mode plus a per-column override - and the case that pair exists for, an icon column pinned at forty pixels with the rest sharing what is left, is two attributes here.
  • MinWidth, MaxWidth, HorizontalCellAlignment, CanResize, HeaderText and Header; Format, TextTrimming and TextAlignment on a text column; CellTemplate on a template column.

Layout

  • The width solver, written before the control that uses it and kept out of it: fixed widths first, automatic ones against the realized rows, and the remainder split by star weight, each column held to its own bounds. A star column that would land outside its bounds is pinned there and taken out of the split, and what it did not use goes back to the others, which is why that step repeats.
  • The answer is solved once per layout pass and handed to the header and to every realized row from the same instance. WinUI 3 has no Grid.SharedSizeGroup, and rows that each worked out their own widths would line up only by accident.
  • Columns that do not fit make the grid scroll sideways rather than being squeezed below their minimums. An unreadable column is worse than one that has to be scrolled to.
  • Auto is measured against the rows that have actually been realized, and against the column's own header as a floor. Rows are what decides it, because reading the whole collection to answer it would stop the grid being virtualized at the moment somebody wrote Width="Auto"; the header is a floor because a column drawn narrower than the name at the top of it is unreadable, and a grid with no rows at all would otherwise leave every automatic column at its minimum.
  • Dragging a column's edge sets its Width to the pixel width it was dropped at, so a dragged column stops being a star column. Anything else would put it back where it was on the next layout pass.
  • The header scrolls sideways with the body and never vertically. It is padded by whatever the vertical scroll bar takes, so that the header and the body have the same distance to travel and the last column does not drift as the body reaches its end.

Columns that change while the grid is on screen

  • Mutating Columns relays the header and rebuilds the cells of every realized row, including the ones scrolled out of sight. A container in the recycling pool still holding the previous column set is the classic bug of a grid whose columns change, and it shows only for the rows nobody happened to be looking at when the change arrived.
  • The decision is made by comparing what a container was built from against what one would be built from now: the same column instances, in the same order, each at the same revision. The revision is not redundant - a column that stays the same object and is given a different MappingName or CellTemplate leaves cells drawing the right thing for a definition nobody uses any more, and the collection has not changed at all.

Selection

  • SelectionMode, and switching it to None clears what was selected - a grid in None with a row still drawn as selected is a grid nobody can deselect. SelectedItem is settable from code, tolerates null, and clears rather than throwing when it is given something the collection does not hold.
  • Replacing the rows clears the selection, whether or not an equal item happens to be in the new set. A panel driven by the grid's selection has to be told that the thing it was showing has gone; one that quietly kept an item belonging to the previous table would go on showing something no row on screen stands for.
  • A row that only moved keeps the selection and the index follows it, without the event being raised
    • nothing downstream needs telling, but the next arrow key would jump.

Nothing to show

  • EmptyContent is drawn when there are no rows and when there are no columns. The second is the one that arrives by accident - a file carrying geometry and no fields at all - and rows that are there and invisible read as a control that has crashed rather than as a table with nothing in it.

Automation

  • Rows are DataItem peers with SelectionItemPattern, because they are ListViewItems and this library did not take that away. A row's name is built from its own values through the column mappings and formatted with DataGridRowAutomationNameFormat.
  • A Button inside a CellTemplate answers to InvokePattern. It is an ordinary WinUI button in the live tree: nothing wraps it, nothing intercepts its input, and nothing makes it a custom element with no pattern.
  • Column headers are HeaderItem peers named after their columns, and the resize handle is named from DataGridColumnResizeName.

Presentation

  • Every brush, metric, text style and string is a key in Themes/DataGridResources.xaml, merged into Application.Resources at the bottom of the collection the first time a grid is created - so it acts as a set of defaults and anything the application declares wins. Nothing has to be added to App.xaml to make the grid work, only to change it.
  • Not one default colour is a literal. Every brush aliases a WinUI system brush, so light, dark, high contrast and the user's accent colour are followed with nothing to set up. The five row states alias the ListViewItem brushes, which is what makes a row selected here look like a selected item in every other list in the application.
  • A cell inherits its colour from the row rather than declaring one, which is how the text of a selected row follows DataGridRowSelectedForegroundBrush without a single cell knowing that selection exists.
  • Right to left: the columns flip with FlowDirection, with nothing to configure. The panels arrange in logical coordinates and WinUI mirrors them.

Sorting

  • Pressing a column header orders the rows by it: ascending, then descending, then back to the order the collection gives them. Three states rather than two, because the order a collection is in is often the order that means something - the sequence things were measured in - and a grid that could only be sorted one way or the other would have thrown that away for good.
  • DataGrid.SortColumn, SortDirection, Sort and AllowUserSorting, and DataGridColumn.CanSort. The properties are ordinary ones, so an application can put the sort in front of the user, save it with the rest of its settings, or set it without a header ever being pressed.
  • A column sorts by its value and not by its text. A column of numbers drawn with a format string orders as numbers and a column of dates orders as dates, which is the whole difference between a grid and a list of strings. What is compared is what the MappingName resolves to, so a column with no mapping name cannot be sorted whatever CanSort says.
  • The comparison is written for values that do not agree with each other, which is the normal case for a grid over the fields of a file: a row with no value sorts before every row that has one in both directions, because a blank is not a small number but the absence of one; numbers of different types are compared as numbers, since a dictionary of boxed values gives an int in one row and a double in the next, and as text 10 comes before 9; strings are compared the way the user's language compares them; and values with nothing in common are compared by their text rather than throwing, because somebody has just pressed a column header and a crash is not an answer.
  • Each row's value is resolved once and sorted alongside it, rather than being read again on each comparison. Sorting a hundred thousand rows makes about a million and a half of them.
  • A sorted column wears an arrow, and says which way it is sorted to a screen reader as well - DataGridSortedAscendingName and DataGridSortedDescendingName - because an arrow says it only to somebody who can see it. The header answers to InvokePattern when it is sortable.

Filtering

  • DataGrid.Filter, a predicate, with RefreshFilter and VisibleRowCount. A predicate and not a search box, because only the application knows what searching its rows means: which columns count, whether case matters, whether a number is matched by its digits or by its value.
  • The filter runs before the sort, and both are one view over the application's collection. While neither is on there is no view at all and the grid hands the collection straight to the list underneath - which is not an optimization but the thing that keeps a collection growing one row at a time from being rebuilt on every arrival.
  • A row arriving while a sort is on is put in its place with a binary search rather than causing a rebuild, because a rebuild is a reset and a reset throws away the scroll position and every realized row for the sake of one arrival.
  • A row the filter takes away stops being selected, and the grid says so.

Frozen columns

  • DataGrid.FrozenColumnCount. The first columns stay at the leading edge while the rest scroll under them, with a line marking the boundary. It is what keeps the column saying which row this is on screen in a table wide enough to have to be scrolled at all.
  • Three things happen together, in the same code for the header and for the rows: the frozen children are arranged at the scroll offset, they are drawn over the ones passing under them, and those are clipped where the frozen band begins. The clip is not decoration - a cell has no background of its own, because it inherits the row's, so without it the text of a scrolling column is legible straight through a frozen one.
  • More columns than there are is not an error; the count is held to what exists.

Selecting more than one row

  • DataGridSelectionMode.Multiple and DataGrid.SelectedItems. A press replaces the selection, Ctrl adds a row and Shift extends the run - which is how a table behaves rather than how a list of check boxes does.
  • SelectedItem is still the first of them, so a panel that follows a single row goes on working when the mode is opened up; and narrowing the mode back to Single keeps that first row rather than clearing, because asking for one row means one row and not none.
  • DataGridSelectionChangedEventArgs.SelectedItems.

Copying

  • Ctrl+C and DataGrid.CopySelectionToClipboard, with CanUserCopy and CopyIncludesColumnHeaders. A grid that is read-only exists to be looked at, and looking at data ends in wanting it in a spreadsheet.
  • Tab between columns and a newline between rows, which a spreadsheet pastes as a table without being asked anything. One line per selected row in the order they are drawn rather than the order they were picked, and every column, including the ones scrolled out of sight - what was copied is a row and not a screenful.
  • A value holding a tab, a line break or a quotation mark is quoted the way a spreadsheet expects, or one cell with a line break in it would arrive as two rows and everything after it would be a column out.
  • A column with no mapping name - a column of buttons - copies as an empty cell rather than as the word "Button".

Fixed

  • The gallery's theming page drew light-theme text onto the window's dark surface when it was switched to light. Nothing was wrong with the grid - ActualTheme was Light all the way down - but DataGridBackgroundBrush is Transparent on purpose, so a page carrying a theme of its own has to paint a background of its own too. Documented in docs/theming.md, where it now has the section Ribbon has for the same trap.
  • VisibleRowCount answered zero before the grid had been given a template, when there is no list view to ask yet, so a status line built in a page's constructor was told there were no rows at all.

Notes

  • SelectedItem has to be bound with Mode=TwoWay written out. WinUI has no way for a dependency property to declare that it binds two-way by default, unlike WPF, so this one cannot ask on the consumer's behalf.
  • A ContentPresenter declared inside a control template binds its own Content to the templated parent's when nothing sets it, and it counts a child declared inline as nothing: the row's cells were quietly replaced by the row item's ToString() and the grid drew a table of blanks. The row template uses a ContentControl for that job instead.