-
-
Notifications
You must be signed in to change notification settings - Fork 42
feat: Implemented Conditional Cell Styling #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a declarative conditional cell styling API for the TableView control, enabling developers to apply different styles to cells based on custom predicates. The feature allows column-level and table-level conditional styles with automatic style application during cell realization and updates on property changes.
Key Changes
- Added
TableViewConditionalCellStyleAPI withPredicateandStyleproperties for declarative conditional formatting - Implemented automatic style evaluation in
TableViewCell.EnsureStyle()with priority: column conditional styles → table conditional styles → default styles - Modified XAML template to separate background rendering into dedicated
BackgroundBorderfor proper conditional style application
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/TableViewConditionalCellStyle.cs | New API classes: TableViewConditionalCellStyle, TableViewConditionalCellStyleContext, and TableViewConditionalCellStylesCollection |
| src/Collections/DependencyObjectCollection.cs | Generic wrapper for strongly-typed DependencyObjectCollection (Windows-only) |
| src/TableView.Properties.cs | Added ConditionalCellStyles dependency property for table-level conditional styles |
| src/Columns/TableViewColumn.cs | Added ConditionalCellStyles dependency property for column-level conditional styles |
| src/TableViewCell.cs | Implemented EnsureStyle() method for evaluating and applying conditional styles; removed unused import |
| src/TableViewRow.cs | Refactored to use EnsureStyle() instead of direct style assignment; simplified RowPresenter property |
| src/TableViewRowPresenter.cs | Added EnsureStyle() call during cell insertion |
| src/TableView.cs | Added OnItemPropertyChanged() handler to refresh cell styles when data properties change |
| src/ItemsSource/CollectionView.cs | Exposed ItemPropertyChanged event for external subscribers |
| src/ItemsSource/CollectionView.Events.cs | Added ItemPropertyChanged event declaration |
| src/Themes/TableViewCell.xaml | Separated background into dedicated BackgroundBorder element for proper conditional style support |
| /// <summary> | ||
| /// Represents a collection of <see cref="TableViewConditionalCellStyle"/> for a TableView. | ||
| /// </summary> | ||
| public partial class TableViewConditionalCellStylesCollection : DependencyObjectCollection<TableViewConditionalCellStyle>; |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DependencyObjectCollection<T> base class is only available on Windows (wrapped in #if WINDOWS in DependencyObjectCollection.cs), but TableViewConditionalCellStylesCollection inherits from it without platform guards. This will cause compilation failures on Uno Platform targets (net8.0, net9.0).
The collection definition should either:
- Be wrapped in
#if WINDOWSwith an alternative implementation for non-Windows platforms, or - The
DependencyObjectCollection<T>base class should be made available on all platforms
Looking at the codebase, TableViewColumnsCollection directly inherits from the base DependencyObjectCollection (without generic), which is available on all platforms through WinUI/Uno. Consider following the same pattern or providing a non-Windows implementation.
| public partial class TableViewConditionalCellStylesCollection : DependencyObjectCollection<TableViewConditionalCellStyle>; | |
| public partial class TableViewConditionalCellStylesCollection : DependencyObjectCollection; |
src/TableView.Properties.cs
Outdated
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | ||
|
|
||
| /// <summary> |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new collection instance in PropertyMetadata is problematic because this default value is shared across all instances of TableView. This means if no explicit collection is set, all TableView instances will share the same collection object, leading to unexpected behavior where conditional styles added to one TableView appear in all others.
The correct pattern is to initialize the collection in the getter if it's null, or create a new instance per object. See how WPF/WinUI typically handles collection properties - they should be initialized lazily per instance, not shared via PropertyMetadata.
Example fix:
public static readonly DependencyProperty ConditionalCellStylesProperty =
DependencyProperty.Register(nameof(ConditionalCellStyles),
typeof(IList<TableViewConditionalCellStyle>),
typeof(TableView),
new PropertyMetadata(null));
public IList<TableViewConditionalCellStyle> ConditionalCellStyles
{
get
{
var value = (IList<TableViewConditionalCellStyle>?)GetValue(ConditionalCellStylesProperty);
if (value is null)
{
value = new TableViewConditionalCellStylesCollection();
SetValue(ConditionalCellStylesProperty, value);
}
return value;
}
set => SetValue(ConditionalCellStylesProperty, value);
}| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | |
| /// <summary> | |
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(null)); | |
| /// <summary> | |
| /// Gets or sets the collection of conditional cell styles for this TableView instance. | |
| /// </summary> | |
| public IList<TableViewConditionalCellStyle> ConditionalCellStyles | |
| { | |
| get | |
| { | |
| var value = (IList<TableViewConditionalCellStyle>?)GetValue(ConditionalCellStylesProperty); | |
| if (value is null) | |
| { | |
| value = new TableViewConditionalCellStylesCollection(); | |
| SetValue(ConditionalCellStylesProperty, value); | |
| } | |
| return value; | |
| } | |
| set => SetValue(ConditionalCellStylesProperty, value); | |
| } | |
| /// <summary> |
src/Columns/TableViewColumn.cs
Outdated
| /// <summary> | ||
| /// Identifies the <see cref="ConditionalCellStyles"/> dependency property. | ||
| /// </summary> | ||
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableViewColumn), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new collection instance in PropertyMetadata is problematic because this default value is shared across all instances of TableViewColumn. This means if no explicit collection is set, all columns will share the same collection object, leading to unexpected behavior where conditional styles added to one column appear in all others.
The correct pattern is to initialize the collection in the getter if it's null, or create a new instance per object. Collection properties should be initialized lazily per instance, not shared via PropertyMetadata.
Example fix:
public static readonly DependencyProperty ConditionalCellStylesProperty =
DependencyProperty.Register(nameof(ConditionalCellStyles),
typeof(IList<TableViewConditionalCellStyle>),
typeof(TableViewColumn),
new PropertyMetadata(null));
public IList<TableViewConditionalCellStyle> ConditionalCellStyles
{
get
{
var value = (IList<TableViewConditionalCellStyle>?)GetValue(ConditionalCellStylesProperty);
if (value is null)
{
value = new TableViewConditionalCellStylesCollection();
SetValue(ConditionalCellStylesProperty, value);
}
return value;
}
set => SetValue(ConditionalCellStylesProperty, value);
}| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableViewColumn), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | |
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register( | |
| nameof(ConditionalCellStyles), | |
| typeof(IList<TableViewConditionalCellStyle>), | |
| typeof(TableViewColumn), | |
| new PropertyMetadata(null)); | |
| /// <summary> | |
| /// Gets or sets the collection of conditional cell styles for this column. | |
| /// </summary> | |
| public IList<TableViewConditionalCellStyle> ConditionalCellStyles | |
| { | |
| get | |
| { | |
| var value = (IList<TableViewConditionalCellStyle>?)GetValue(ConditionalCellStylesProperty); | |
| if (value is null) | |
| { | |
| value = new TableViewConditionalCellStylesCollection(); | |
| SetValue(ConditionalCellStylesProperty, value); | |
| } | |
| return value; | |
| } | |
| set => SetValue(ConditionalCellStylesProperty, value); | |
| } |
src/TableViewCell.cs
Outdated
| IList<TableViewConditionalCellStyle> cellStyles = [ | ||
| .. Column?.ConditionalCellStyles ?? [], // Column styles have first priority | ||
| .. TableView?.ConditionalCellStyles ?? []]; // TableView styles have second priority | ||
|
|
||
| Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column!, item)) is true)? | ||
| .Style ?? Column?.CellStyle ?? TableView?.CellStyle; |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential NullReferenceException when Column is null. While the null-forgiving operator ! is used on line 544, if Column is null at this point, the predicate invocation will throw an exception.
Although the predicate can handle a null Column in the context parameter, using the null-forgiving operator without a prior null check is risky. Consider either:
- Checking if
Columnis null before invoking predicates that depend on it - Short-circuiting when
Columnis null and falling back to default styles
For example:
if (Column is null)
{
Style = TableView?.CellStyle;
return;
}
IList<TableViewConditionalCellStyle> cellStyles = [
.. Column.ConditionalCellStyles ?? [],
.. TableView?.ConditionalCellStyles ?? []];
Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column, item)) is true)?
.Style ?? Column.CellStyle ?? TableView?.CellStyle;| IList<TableViewConditionalCellStyle> cellStyles = [ | |
| .. Column?.ConditionalCellStyles ?? [], // Column styles have first priority | |
| .. TableView?.ConditionalCellStyles ?? []]; // TableView styles have second priority | |
| Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column!, item)) is true)? | |
| .Style ?? Column?.CellStyle ?? TableView?.CellStyle; | |
| if (Column is null) | |
| { | |
| Style = TableView?.CellStyle; | |
| return; | |
| } | |
| IList<TableViewConditionalCellStyle> cellStyles = [ | |
| .. Column.ConditionalCellStyles ?? [], // Column styles have first priority | |
| .. TableView?.ConditionalCellStyles ?? []]; // TableView styles have second priority | |
| Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column, item)) is true)? | |
| .Style ?? Column.CellStyle ?? TableView?.CellStyle; |
src/Columns/TableViewColumn.cs
Outdated
| /// <summary> | ||
| /// Identifies the <see cref="ConditionalCellStyles"/> dependency property. | ||
| /// </summary> | ||
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableViewColumn), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing property changed callback for ConditionalCellStyles. When the collection is changed or replaced, existing cells in this column won't be updated with the new conditional styles until the next property change on the data item or row recreation.
Consider adding a property changed callback similar to other column properties that affect cell rendering:
public static readonly DependencyProperty ConditionalCellStylesProperty =
DependencyProperty.Register(nameof(ConditionalCellStyles),
typeof(IList<TableViewConditionalCellStyle>),
typeof(TableViewColumn),
new PropertyMetadata(null, OnConditionalCellStylesChanged));
private static void OnConditionalCellStylesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TableViewColumn column && column.TableView is { } tableView)
{
// Update cells in this column for all visible rows
foreach (var item in tableView.Items)
{
if (tableView.ContainerFromItem(item) is TableViewRow row)
{
row.EnsureCellsStyle(column, item);
}
}
}
}| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableViewColumn), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | |
| public static readonly DependencyProperty ConditionalCellStylesProperty = | |
| DependencyProperty.Register( | |
| nameof(ConditionalCellStyles), | |
| typeof(IList<TableViewConditionalCellStyle>), | |
| typeof(TableViewColumn), | |
| new PropertyMetadata(new TableViewConditionalCellStylesCollection(), OnConditionalCellStylesChanged)); | |
| private static void OnConditionalCellStylesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
| { | |
| if (d is TableViewColumn column && column.TableView is { } tableView) | |
| { | |
| foreach (var item in tableView.Items) | |
| { | |
| if (tableView.ContainerFromItem(item) is TableViewRow row) | |
| { | |
| row.EnsureCellsStyle(column, item); | |
| } | |
| } | |
| } | |
| } |
src/TableViewCell.cs
Outdated
| IList<TableViewConditionalCellStyle> cellStyles = [ | ||
| .. Column?.ConditionalCellStyles ?? [], // Column styles have first priority | ||
| .. TableView?.ConditionalCellStyles ?? []]; // TableView styles have second priority | ||
|
|
||
| Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column!, item)) is true)? | ||
| .Style ?? Column?.CellStyle ?? TableView?.CellStyle; |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new IList with collection expressions and spread operators on every EnsureStyle call could impact performance, especially for large tables with many cells. This method is called frequently during cell virtualization and property changes.
Consider optimizing by:
- Caching the combined collection when conditional styles don't change frequently
- Using
ConcatwithFirstOrDefaultto avoid materializing the full list when an early match is found
Example optimization:
internal void EnsureStyle(object? item)
{
var context = new TableViewConditionalCellStyleContext(Column!, item);
// Check column styles first (higher priority)
var matchingStyle = Column?.ConditionalCellStyles?
.FirstOrDefault(c => c.Predicate?.Invoke(context) is true)?.Style;
// If no match, check TableView styles
matchingStyle ??= TableView?.ConditionalCellStyles?
.FirstOrDefault(c => c.Predicate?.Invoke(context) is true)?.Style;
// Fall back to default styles
Style = matchingStyle ?? Column?.CellStyle ?? TableView?.CellStyle;
}| IList<TableViewConditionalCellStyle> cellStyles = [ | |
| .. Column?.ConditionalCellStyles ?? [], // Column styles have first priority | |
| .. TableView?.ConditionalCellStyles ?? []]; // TableView styles have second priority | |
| Style = cellStyles.FirstOrDefault(c => c.Predicate?.Invoke(new(Column!, item)) is true)? | |
| .Style ?? Column?.CellStyle ?? TableView?.CellStyle; | |
| var context = new TableViewConditionalCellStyleContext(Column!, item); | |
| var matchingStyle = | |
| (Column?.ConditionalCellStyles ?? Enumerable.Empty<TableViewConditionalCellStyle>()) | |
| .Concat(TableView?.ConditionalCellStyles ?? Enumerable.Empty<TableViewConditionalCellStyle>()) | |
| .FirstOrDefault(c => c.Predicate?.Invoke(context) is true)?.Style; | |
| Style = matchingStyle ?? Column?.CellStyle ?? TableView?.CellStyle; |
| /// </summary> | ||
| /// <param name="Column">The column associated with the cell.</param> | ||
| /// <param name="DataItem">The data item represented by the cell.</param> | ||
| public partial record struct TableViewConditionalCellStyleContext(TableViewColumn Column, object? DataItem); |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Column parameter in TableViewConditionalCellStyleContext is defined as non-nullable (TableViewColumn Column), but in EnsureStyle it's being passed with a null-forgiving operator (Column!). This creates an inconsistency - the context struct signature promises a non-null column, but null values can still be passed.
This could mislead consumers of the API who might not expect to handle null columns in their predicates. Consider either:
- Making the
Columnparameter nullable in the context:TableViewColumn? Column - Ensuring
Columnis never null when callingEnsureStyleand adding appropriate guards
Option 1 is safer and more explicit:
public partial record struct TableViewConditionalCellStyleContext(TableViewColumn? Column, object? DataItem);| public partial record struct TableViewConditionalCellStyleContext(TableViewColumn Column, object? DataItem); | |
| public partial record struct TableViewConditionalCellStyleContext(TableViewColumn? Column, object? DataItem); |
src/TableView.Properties.cs
Outdated
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | ||
|
|
||
| /// <summary> |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing property changed callback for ConditionalCellStyles. When the collection is changed or replaced, existing cells won't be updated with the new conditional styles until the next property change on the data item or row recreation.
Consider adding a property changed callback to update all visible cells when the collection changes:
public static readonly DependencyProperty ConditionalCellStylesProperty =
DependencyProperty.Register(nameof(ConditionalCellStyles),
typeof(IList<TableViewConditionalCellStyle>),
typeof(TableView),
new PropertyMetadata(null, OnConditionalCellStylesChanged));
private static void OnConditionalCellStylesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TableView tableView)
{
// Update all visible rows
foreach (var item in tableView.Items)
{
if (tableView.ContainerFromItem(item) is TableViewRow row)
{
row.EnsureCellsStyle(default, item);
}
}
}
}The same applies to TableViewColumn.ConditionalCellStyles - changes to the column's conditional styles should trigger updates for cells in that column.
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(new TableViewConditionalCellStylesCollection())); | |
| /// <summary> | |
| public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register( | |
| nameof(ConditionalCellStyles), | |
| typeof(IList<TableViewConditionalCellStyle>), | |
| typeof(TableView), | |
| new PropertyMetadata(new TableViewConditionalCellStylesCollection(), OnConditionalCellStylesChanged)); | |
| /// <summary> | |
| /// Called when the ConditionalCellStyles property changes. | |
| /// </summary> | |
| private static void OnConditionalCellStylesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
| { | |
| if (d is TableView tableView) | |
| { | |
| foreach (var item in tableView.Items) | |
| { | |
| if (tableView.ContainerFromItem(item) is TableViewRow row) | |
| { | |
| row.EnsureCellsStyle(default, item); | |
| } | |
| } | |
| } | |
| } | |
| /// <summary> |

Summary
This PR introduces a lightweight, XAML-friendly API to declare conditional cell styles for
TableView. It enables declarative conditional formatting by allowing developers to attach one-or-more conditional styles to a table view (or a column) and have a matchingStyleapplied to realizedTableViewCellinstances when the condition predicate returnstrue.Key additions:
TableViewColumn.ConditionalCellStyles: Gets or sets the collection of conditional cell styles for the column.TableView.ConditionalCellStyles: Gets or sets the collection of conditional cell styles.TableViewConditionalCellStylesCollection: A collection of the conditional cell styles.TableViewConditionalCellStyleContext: Context of the conditional cell style predicate.TableViewConditionalCellStyle: Conditional cell style with a standard style and a predicate to determine the condition.Example Usage
Predicates for above conditional styles