Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions src/ColumnFilterHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,25 @@ public virtual IList<TableViewFilterItem> GetFilterItems(TableViewColumn column,
column.TableView.FilterDescriptions.Where(
x => x is not ColumnFilterDescription columnFilter || columnFilter.Column != column));

var filterValues = new SortedSet<object?>();

foreach (var item in collectionView)
{
var value = column.GetCellContent(item);
filterValues.Add(IsBlank(value) ? null : value);
}

return [.. filterValues.Select(value =>
{
value ??= TableViewLocalizedStrings.BlankFilterValue;
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
(column.IsFiltered && SelectedValues[column].Contains(value));

return string.IsNullOrEmpty(searchText)
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
? new TableViewFilterItem(isSelected, value)
: null;

}).OfType<TableViewFilterItem>()];
return [.. collectionView.Select(column.GetCellContent)
.Select(x => IsBlank(x) ? null : x)
.GroupBy(x => x)
.OrderBy(x => x.Key)
.Select(x =>
{
var value = x.Key;
value ??= TableViewLocalizedStrings.BlankFilterValue;
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
(column.IsFiltered && SelectedValues[column].Contains(value));

return string.IsNullOrEmpty(searchText)
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == true' can be simplified to 'A'.

Suggested change
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase)

Copilot uses AI. Check for mistakes.
? new TableViewFilterItem(isSelected, value, x.Count())
: null;

})
.OfType<TableViewFilterItem>()
.OrderByDescending(x => _tableView.ShowFilterItemsCount ? x.Count : 0)];
Comment on lines +33 to +51
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance concern: The LINQ chain now has two separate sorting operations - .OrderBy(x => x.Key) on line 36 followed by .OrderByDescending(x => _tableView.ShowFilterItemsCount ? x.Count : 0) on line 51. When ShowFilterItemsCount is false, the second OrderBy still executes but sorts by 0 (no-op sort). Consider optimizing by applying conditional ordering:

var items = collectionView.Select(column.GetCellContent)
                         .Select(x => IsBlank(x) ? null : x)
                         .GroupBy(x => x)
                         .Select(x => { /* create filter item */ })
                         .OfType<TableViewFilterItem>();
return [.. (_tableView.ShowFilterItemsCount 
    ? items.OrderByDescending(x => x.Count) 
    : items.OrderBy(x => x.Value))];

This avoids the unnecessary second sort and maintains the original alphabetical ordering when count display is disabled.

Suggested change
return [.. collectionView.Select(column.GetCellContent)
.Select(x => IsBlank(x) ? null : x)
.GroupBy(x => x)
.OrderBy(x => x.Key)
.Select(x =>
{
var value = x.Key;
value ??= TableViewLocalizedStrings.BlankFilterValue;
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
(column.IsFiltered && SelectedValues[column].Contains(value));
return string.IsNullOrEmpty(searchText)
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
? new TableViewFilterItem(isSelected, value, x.Count())
: null;
})
.OfType<TableViewFilterItem>()
.OrderByDescending(x => _tableView.ShowFilterItemsCount ? x.Count : 0)];
var items = collectionView.Select(column.GetCellContent)
.Select(x => IsBlank(x) ? null : x)
.GroupBy(x => x)
.Select(x =>
{
var value = x.Key;
value ??= TableViewLocalizedStrings.BlankFilterValue;
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
(column.IsFiltered && SelectedValues[column].Contains(value));
return string.IsNullOrEmpty(searchText)
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
? new TableViewFilterItem(isSelected, value, x.Count())
: null;
})
.OfType<TableViewFilterItem>();
return [.. (_tableView.ShowFilterItemsCount
? items.OrderByDescending(x => x.Count)
: items.OrderBy(x => x.Value))];

Copilot uses AI. Check for mistakes.
}

return [];
Expand Down
14 changes: 14 additions & 0 deletions src/TableView.Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ public partial class TableView
/// </summary>
public static readonly DependencyProperty ConditionalCellStylesProperty = DependencyProperty.Register(nameof(ConditionalCellStyles), typeof(IList<TableViewConditionalCellStyle>), typeof(TableView), new PropertyMetadata(default));

/// <summary>
/// Identifies the <see cref="ShowFilterItemsCount"/> dependency property.
/// </summary>
public static readonly DependencyProperty ShowFilterItemsCountProperty = DependencyProperty.Register(nameof(ShowFilterItemsCount), typeof(bool), typeof(TableView), new PropertyMetadata(false));
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing: there are two spaces after the = sign instead of one. For consistency with other DependencyProperty declarations in this file (lines 22-261), use a single space:

public static readonly DependencyProperty ShowFilterItemsCountProperty = DependencyProperty.Register(...)
Suggested change
public static readonly DependencyProperty ShowFilterItemsCountProperty = DependencyProperty.Register(nameof(ShowFilterItemsCount), typeof(bool), typeof(TableView), new PropertyMetadata(false));
public static readonly DependencyProperty ShowFilterItemsCountProperty = DependencyProperty.Register(nameof(ShowFilterItemsCount), typeof(bool), typeof(TableView), new PropertyMetadata(false));

Copilot uses AI. Check for mistakes.

/// <summary>
/// Gets or sets a value indicating whether opening the column filter over header right-click is enabled.
/// </summary>
Expand Down Expand Up @@ -316,6 +321,15 @@ public IList<TableViewConditionalCellStyle> ConditionalCellStyles
set => SetValue(ConditionalCellStylesProperty, value);
}

/// <summary>
/// Gets or sets a value that indicates whether the TableView displays items count next to each filter item in filter flyout.
/// </summary>
public bool ShowFilterItemsCount
{
get => (bool)GetValue(ShowFilterItemsCountProperty);
set => SetValue(ShowFilterItemsCountProperty, value);
}

/// <summary>
/// Gets or sets the selection start cell slot.
/// </summary>
Expand Down
11 changes: 9 additions & 2 deletions src/TableViewColumnHeader.FilterItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace WinUI.TableView;
/// <summary>
/// Represents a filter item used in the options flyout of a TableViewColumnHeader.
/// </summary>
public partial class TableViewFilterItem : INotifyPropertyChanged
public partial class TableViewFilterItem : INotifyPropertyChanged
{
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
Expand All @@ -17,10 +17,12 @@ public partial class TableViewFilterItem : INotifyPropertyChanged
/// </summary>
/// <param name="isSelected">Indicates whether the filter item is selected.</param>
/// <param name="value">The value of the filter item.</param>
public TableViewFilterItem(bool isSelected, object value)
/// <param name="count">The count of occurrences for the filter item.</param>
public TableViewFilterItem(bool isSelected, object value, int count = 1)
{
IsSelected = isSelected;
Value = value;
Count = count;
}

/// <summary>
Expand All @@ -40,4 +42,9 @@ public bool IsSelected
/// Gets the value of the filter item.
/// </summary>
public object Value { get; }

/// <summary>
/// Gets or sets the count of occurrences for the filter item.
/// </summary>
public int Count { get; set; }
}
6 changes: 3 additions & 3 deletions src/TableViewColumnHeader.OptionsFlyoutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public partial class TableViewColumnHeader
/// <summary>
/// ViewModel for the options flyout in the TableViewColumnHeader.
/// </summary>
private partial class OptionsFlyoutViewModel : INotifyPropertyChanged
internal partial class OptionsFlyoutViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private IList<TableViewFilterItem> _filterItems = [];
Expand Down Expand Up @@ -114,7 +114,7 @@ public IList<TableViewFilterItem> FilterItems

DetachPropertyChangedHandlers();
_filterItems = value;
AttachPropertyChangedHandlers();
AttachPropertyChangedHandlers();
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace detected after the semicolon. Remove the extra spaces at the end of the line to maintain consistent code formatting.

Suggested change
AttachPropertyChangedHandlers();
AttachPropertyChangedHandlers();

Copilot uses AI. Check for mistakes.
SetSelectAllCheckBoxState();
OnPropertyChanged();
}
Expand All @@ -128,7 +128,7 @@ public IList<TableViewFilterItem> FilterItems
/// <summary>
/// Sets the state of the select all checkbox.
/// </summary>
private void SetSelectAllCheckBoxState()
internal void SetSelectAllCheckBoxState()
{
if (ColumnHeader._selectAllCheckBox is null || !_canSetState)
{
Expand Down
36 changes: 25 additions & 11 deletions src/TableViewColumnHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public partial class TableViewColumnHeader : ContentControl
private double _reorderStartingPosition;
private bool _reorderStarted;
private RenderTargetBitmap? _dragVisuals;
private ListView? _filterItemsList;

/// <summary>
/// Initializes a new instance of the TableViewColumnHeader class.
Expand Down Expand Up @@ -228,24 +229,33 @@ protected override void OnApplyTemplate()
_optionsButton.Tapped += OnOptionsButtonTaped;
_optionsButton.DataContext = _optionsFlyoutViewModel = new OptionsFlyoutViewModel(_tableView, this);

var menuItem = _optionsFlyout.Items.FirstOrDefault(x => x.Name == "ItemsCheckFlyoutItem");
menuItem?.ApplyTemplate();
if (_optionsFlyout.Items.FirstOrDefault(x => x.Name == "ItemsCheckFlyoutItem") is { } menuItem)
{
menuItem.Loaded += OnItemsCheckFlyoutItemLoaded;
}

SetFilterButtonVisibility();
EnsureGridLines();
}

/// <summary>
/// Handles the Loaded event for the items check flyout item.
/// </summary>
private void OnItemsCheckFlyoutItemLoaded(object sender, RoutedEventArgs e)
{
if (sender is not MenuFlyoutItem menuItem) return;

menuItem.Loaded -= OnItemsCheckFlyoutItemLoaded;

if (menuItem?.FindDescendant<CheckBox>(x => x.Name == "SelectAllCheckBox") is { } checkBox)
{
_selectAllCheckBox = checkBox;
_selectAllCheckBox.Content = TableViewLocalizedStrings.SelectAllParenthesized;
_selectAllCheckBox.Checked += OnSelectAllCheckBoxChecked;
_selectAllCheckBox.Unchecked += OnSelectAllCheckBoxUnchecked;
_optionsFlyoutViewModel.SetSelectAllCheckBoxState();
}

#if !WINDOWS
if (menuItem?.FindDescendant<ListView>(x => x.Name is "FilterItemsList") is { } filterItemsList)
{
filterItemsList.Margin = new Thickness(12, 0, 0, 0);
}
#endif

if (menuItem?.FindDescendant<TextBox>(x => x.Name == "SearchBox") is { } searchBox)
{
_searchBox = searchBox;
Expand All @@ -260,8 +270,7 @@ protected override void OnApplyTemplate()
menuItem.PreviewKeyUp += static (_, e) => e.Handled = e.Key is VirtualKey.Space;
}

SetFilterButtonVisibility();
EnsureGridLines();
_filterItemsList = menuItem?.FindDescendant<ListView>(x => x.Name is "FilterItemsList");
}

/// <summary>
Expand Down Expand Up @@ -321,6 +330,11 @@ private async void OnOptionsFlyoutOpening(object? sender, object e)
await Task.Delay(100);
await FocusManager.TryFocusAsync(_searchBox, FocusState.Programmatic);
}

if (_filterItemsList is not null && _optionsFlyoutViewModel.FilterItems.Count > 0)
{
_filterItemsList.ScrollIntoView(_optionsFlyoutViewModel.FilterItems[0]);
}
}

/// <summary>
Expand Down
37 changes: 32 additions & 5 deletions src/Themes/TableViewColumnHeader.xaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:WinUI.TableView"
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:not_win="http://uno.ui/not_win">
xmlns:not_win="http://uno.ui/not_win"
xmlns:converters="using:WinUI.TableView.Converters"
mc:Ignorable="not_win">

<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///WinUI.TableView/Themes/Resources.xaml" />
</ResourceDictionary.MergedDictionaries>

<converters:BoolToVisibilityConverter x:Key="BoolToVisibility" />

<Style x:Key="DefaultTableViewColumnHeaderStyle"
TargetType="local:TableViewColumnHeader">
<Setter Property="FontWeight"
Expand Down Expand Up @@ -150,7 +155,7 @@
<MenuFlyoutItem.Template>
<ControlTemplate TargetType="MenuFlyoutItem">
<Grid Margin="8,4"
Width="220"
Width="250"
Height="300">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
Expand All @@ -167,6 +172,7 @@
<ListView x:Name="FilterItemsList"
Grid.Row="2"
SelectionMode="None"
not_win:Margin="12,0,0,0"
ItemsSource="{Binding FilterItems}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem"
Expand All @@ -179,13 +185,34 @@
Value="36" />
<Setter Property="Height"
Value="36" />
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}">
<TextBlock Text="{Binding Value}"
TextWrapping="NoWrap" />
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<TextBlock Text="{Binding Value}"
TextWrapping="NoWrap" />

<StackPanel Grid.Column="1"
Orientation="Horizontal"
Visibility="{Binding DataContext.TableView.ShowFilterItemsCount, ElementName=FilterItemsList, Converter={StaticResource BoolToVisibility}}">
<TextBlock Text="(" />
<TextBlock Text="{Binding Count}" />
<TextBlock Text=")" />
</StackPanel>
</Grid>
</CheckBox>
</DataTemplate>
</ListView.ItemTemplate>
Expand Down
Loading