diff --git a/README.md b/README.md index dfb7b7c0..bcdccdca 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,21 @@ public class Item : INotifyPropertyChanged Build and run your application. You should see the `TableView` populated with the rows and cells from your `ViewModel`. +## Native AOT Compatibility + +TableView relies heavily on runtime bindings and dynamic value resolution, which are not fully compatible with IL trimming and Native AOT by default. + +To use TableView in Native AOT applications, bound types should be decorated with the `WinRT.GeneratedBindableCustomProperty` attribute to preserve the required binding metadata. +```cs +[WinRT.GeneratedBindableCustomProperty] +public partial class Person +{ + public string Name { get; set; } + + public int Age { get; set; } +} +``` + ## Customization You can customize the appearance and behavior of the `TableView` by modifying its properties, templates, and styles. For example: diff --git a/WinUI.TableView.slnx b/WinUI.TableView.slnx index 64845ab8..f115bfbd 100644 --- a/WinUI.TableView.slnx +++ b/WinUI.TableView.slnx @@ -1,5 +1,9 @@ + + + + diff --git a/samples/AotTestApp/AotTestApp.csproj b/samples/AotTestApp/AotTestApp.csproj new file mode 100644 index 00000000..8d555f0c --- /dev/null +++ b/samples/AotTestApp/AotTestApp.csproj @@ -0,0 +1,71 @@ + + + WinExe + net10.0-windows10.0.19041.0 + 10.0.17763.0 + AotTestApp + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + true + false + true + enable + true + true + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + False + True + False + True + + \ No newline at end of file diff --git a/samples/AotTestApp/App.xaml b/samples/AotTestApp/App.xaml new file mode 100644 index 00000000..46f6f503 --- /dev/null +++ b/samples/AotTestApp/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/samples/AotTestApp/App.xaml.cs b/samples/AotTestApp/App.xaml.cs new file mode 100644 index 00000000..a068abaa --- /dev/null +++ b/samples/AotTestApp/App.xaml.cs @@ -0,0 +1,48 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.UI.Xaml.Shapes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace AotTestApp; +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : Application +{ + private Window? _window; + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + _window = new MainWindow(); + _window.Activate(); + } +} diff --git a/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png b/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..7440f0d4 Binary files /dev/null and b/samples/AotTestApp/Assets/LockScreenLogo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/SplashScreen.scale-200.png b/samples/AotTestApp/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000..32f486a8 Binary files /dev/null and b/samples/AotTestApp/Assets/SplashScreen.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png b/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..53ee3777 Binary files /dev/null and b/samples/AotTestApp/Assets/Square150x150Logo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png b/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..f713bba6 Binary files /dev/null and b/samples/AotTestApp/Assets/Square44x44Logo.scale-200.png differ diff --git a/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..dc9f5bea Binary files /dev/null and b/samples/AotTestApp/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/samples/AotTestApp/Assets/StoreLogo.png b/samples/AotTestApp/Assets/StoreLogo.png new file mode 100644 index 00000000..a4586f26 Binary files /dev/null and b/samples/AotTestApp/Assets/StoreLogo.png differ diff --git a/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png b/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..8b4a5d0d Binary files /dev/null and b/samples/AotTestApp/Assets/Wide310x150Logo.scale-200.png differ diff --git a/samples/AotTestApp/DataFaker.cs b/samples/AotTestApp/DataFaker.cs new file mode 100644 index 00000000..c85d7cc0 --- /dev/null +++ b/samples/AotTestApp/DataFaker.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; + +namespace AotTestApp; + +/// +/// A native AOT-compatible data faker for generating sample data. +/// +public static class DataFaker +{ + private static readonly Random _random = new(); + + // First names + private static readonly string[] FirstNames = + [ + "James", "Mary", "Robert", "Patricia", "Michael", "Jennifer", "William", "Linda", "David", "Barbara", + "Richard", "Elizabeth", "Joseph", "Susan", "Thomas", "Jessica", "Charles", "Sarah", "Christopher", "Karen", + "Daniel", "Nancy", "Matthew", "Lisa", "Anthony", "Betty", "Mark", "Margaret", "Donald", "Sandra", + "Steven", "Ashley", "Paul", "Kimberly", "Andrew", "Donna", "Joshua", "Carol", "Kenneth", "Michelle", + "Kevin", "Emily", "Brian", "Melissa", "George", "Deborah", "Edward", "Stephanie", "Ronald", "Rebecca", + "Timothy", "Sharon", "Jason", "Brenda", "Jeffrey", "Amber", "Ryan", "Anna", "Jacob", "Pamela", + "Gary", "Nicole", "Nicholas", "Emma", "Eric", "Helen", "Jonathan", "Samantha", "Stephen", "Katherine" + ]; + + // Last names + private static readonly string[] LastNames = + [ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", + "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", + "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", + "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Peterson", "Phillips", "Campbell", + "Parker", "Evans", "Edwards", "Collins", "Reyes", "Stewart", "Morris", "Morales", "Murphy", "Cook", + "Rogers", "Morgan", "Peterson", "Cooper", "Reed", "Bell", "Gomez", "Murray", "Freeman", "Wells", + "Webb", "Simpson", "Stevens", "Tucker", "Porter", "Hunter", "Hicks", "Crawford", "Henry", "Boyd" + ]; + + // Job titles + public static readonly Designation[] JobTitles = + [ + new(1,"Software Developer"), + new(2,"Manager"), + new(3,"Sales Representative") , + new(4,"Accountant") , + new(5,"Analyst"), + new(6,"Engineer"), + new(7,"Designer"), + new(8,"Teacher"), + new(9,"Consultant"), + new(10,"Executive"), + new(11,"Administrator"), + new(12,"Coordinator"), + new(13,"Director"), + new(14,"Supervisor"), + new(15,"Specialist"), + new(16,"Technician"), + new(17,"Operator"), + new(18,"Clerk"), + new(19,"Assistant"), + new(20,"Officer"), + new(21,"Agent"), + new(22,"Associate"), + new(23,"Architect"), + new(24,"Planner"), + new(25,"Scientist"), + new(26,"Researcher"), + new(27,"Programmer"), + new(28,"Nurse"), + new(29,"Doctor"), + new(30,"Lawyer"), + new(31,"Marketing Manager"), + new(32,"Product Manager"), + new(33,"Business Analyst"), + new(34,"Data Scientist"), + new(35,"DevOps Engineer") + ]; + + // Department names + internal static readonly string[] Departments = + [ + "Sales", "Marketing", "Engineering", "Finance", "Human Resources", + "Operations", "IT", "Legal", "Research", "Development", + "Quality Assurance", "Customer Service", "Production", "Logistics", "Planning", + "Maintenance", "Administration", "Strategic Planning", "Corporate Communications", "Treasury" + ]; + + // Street suffixes + private static readonly string[] StreetSuffixes = + [ + "Street", "Avenue", "Road", "Boulevard", "Drive", "Lane", "Court", "Circle", + "Way", "Parkway", "Plaza", "Square", "Trail", "Ridge", "Hill", "Oak" + ]; + + // Cities + private static readonly string[] Cities = + [ + "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", + "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose", + "Austin", "Jacksonville", "Seattle", "Denver", "Boston", + "Portland", "Miami", "Atlanta", "Las Vegas", "Detroit" + ]; + + // States + private static readonly string[] States = + [ + "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY" + ]; + + private static readonly string[] Regions = [ + "East", "West", "North", "South" + ]; + + // Genders + internal static readonly string[] Genders = ["Male", "Female", "Non-binary", "Genderfluid", "Agender", "Bigender", "Genderqueer", "Two-Spirit", "Prefer not to say"]; + + // Avatar images (using placeholder service URLs) + private static readonly string[] AvatarSeeds = + [ + "avatar1", "avatar2", "avatar3", "avatar4", "avatar5", + "avatar6", "avatar7", "avatar8", "avatar9", "avatar10" + ]; + + public static string FirstName() + { + return FirstNames[_random.Next(FirstNames.Length)]; + } + + public static string LastName() + { + return LastNames[_random.Next(LastNames.Length)]; + } + + public static string FullName() + { + return $"{FirstName()} {LastName()}"; + } + + public static string Email(string? firstName = null, string? lastName = null) + { + firstName ??= FirstName(); + lastName ??= LastName(); + var domains = new[] { "example.com", "test.com", "sample.com", "data.com" }; + var domain = domains[_random.Next(domains.Length)]; + return $"{firstName.ToLower()}.{lastName.ToLower()}@{domain}"; + } + + public static string Gender() + { + return Genders[_random.Next(Genders.Length)]; + } + + public static DateOnly PastDate(int yearsBack = 50, DateOnly? maxDate = null) + { + maxDate ??= DateOnly.FromDateTime(DateTime.Now); + var startDate = maxDate.Value.AddYears(-yearsBack); + var daysRange = maxDate.Value.DayNumber - startDate.DayNumber; + var randomDays = _random.Next(daysRange); + return startDate.AddDays(randomDays); + } + + public static TimeOnly TimeOfDay() + { + var hours = _random.Next(0, 24); + var minutes = _random.Next(0, 60); + var seconds = _random.Next(0, 60); + return new TimeOnly(hours, minutes, seconds); + } + + public static bool Boolean(float truePercentage = 0.5f) + { + return _random.NextSingle() < truePercentage; + } + + public static int Integer(int min = 0, int max = int.MaxValue) + { + return _random.Next(min, max); + } + + public static decimal Decimal(decimal min = 0, decimal max = 1000) + { + return ((decimal)_random.NextDouble() * (max - min)) + min; + } + + public static string Department() + { + return Departments[_random.Next(Departments.Length)]; + } + + public static Designation JobTitle() + { + return JobTitles[_random.Next(JobTitles.Length)]; + } + + public static string Address() + { + var streetNumber = _random.Next(1, 9999); + var streetName = FirstName(); + var suffix = StreetSuffixes[_random.Next(StreetSuffixes.Length)]; + var city = Cities[_random.Next(Cities.Length)]; + var state = States[_random.Next(States.Length)]; + var zip = _random.Next(10000, 99999); + return $"{streetNumber} {streetName} {suffix}, {city}, {state} {zip}"; + } + + public static string Avatar() + { + var seed = AvatarSeeds[_random.Next(AvatarSeeds.Length)]; + var id = _random.Next(1, 100); + return $"https://api.dicebear.com/7.x/avataaars/svg?seed={seed}{id}"; + } + + public static string ZipCode() + { + return _random.Next(10000, 99999).ToString(); + } + + public static string State() + { + return States[_random.Next(States.Length)]; + } + + public static string Region() + { + return Regions[_random.Next(Regions.Length)]; + } + + public static string City() + { + return Cities[_random.Next(Cities.Length)]; + } +} diff --git a/samples/AotTestApp/ExampleModel.cs b/samples/AotTestApp/ExampleModel.cs new file mode 100644 index 00000000..fb225a43 --- /dev/null +++ b/samples/AotTestApp/ExampleModel.cs @@ -0,0 +1,76 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace AotTestApp; + +[WinRT.GeneratedBindableCustomProperty] +public partial class Designation : ObservableObject +{ + public Designation(int id, string? title) + { + Id = id; + Title = title; + } + + [ObservableProperty] + public partial int Id { get; set; } + + + [ObservableProperty] + public partial string? Title { get; set; } +} + +[WinRT.GeneratedBindableCustomProperty] +public partial class User : ObservableObject +{ + [ObservableProperty] + [Display(ShortName = "First Name")] + public partial string? FirstName { get; set; } + + [ObservableProperty] + [Display(ShortName = "Last Name")] + public partial string? LastName { get; set; } + + [ObservableProperty] + public partial string? Email { get; set; } + + [ObservableProperty] + public partial string? Gender { get; set; } + + [ObservableProperty] + public partial DateOnly Dob { get; set; } +} + +[WinRT.GeneratedBindableCustomProperty] +public partial class ExampleModel : ObservableObject +{ + [ObservableProperty] + public partial int Id { get; set; } + + [ObservableProperty] + [Display(ShortName = "Active At")] + public partial TimeOnly ActiveAt { get; set; } + + [ObservableProperty] + [Display(ShortName = "Is Active")] + public partial bool IsActive { get; set; } + + [ObservableProperty] + public partial string? Department { get; set; } + + [ObservableProperty] + public partial Designation? Designation { get; set; } + + [ObservableProperty] + public partial string? Address { get; set; } + + [ObservableProperty] + [Display(AutoGenerateField = false)] + public partial string? Avatar { get; set; } + + public Uri AvatarUrl => new(Avatar ?? string.Empty); + + [ObservableProperty] + [Display(AutoGenerateField = false)] + public partial User? User { get; set; } +} diff --git a/samples/AotTestApp/MainViewModel.cs b/samples/AotTestApp/MainViewModel.cs new file mode 100644 index 00000000..460150af --- /dev/null +++ b/samples/AotTestApp/MainViewModel.cs @@ -0,0 +1,72 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace AotTestApp; + +public partial class MainViewModel : ObservableObject +{ + public MainViewModel() + { + foreach (var item in ItemsList) + { + Items.Add(item); + } + + Genders = [.. DataFaker.Genders]; + Departments = [.. DataFaker.Departments]; + Designations = [.. DataFaker.JobTitles]; + } + + public static void InitializeItems() + { + var startId = 1; + var startDate = new DateOnly(1970, 1, 1); + + ItemsList.Clear(); + + for (var i = 0; i < 1_000; i++) + { + var firstName = DataFaker.FirstName(); + var lastName = DataFaker.LastName(); + var email = DataFaker.Email(firstName, lastName); + var gender = DataFaker.Gender(); + var dob = DataFaker.PastDate(50, startDate); + ItemsList.Add(new ExampleModel + { + Id = startId++, + IsActive = DataFaker.Boolean(), + ActiveAt = DataFaker.TimeOfDay(), + Department = DataFaker.Department(), + Designation = DataFaker.JobTitle(), + Address = DataFaker.Address(), + Avatar = DataFaker.Avatar(), + User = new User + { + FirstName = firstName, + LastName = lastName, + Email = email, + Gender = gender, + Dob = dob + } + }); + } + } + + public static List ItemsList { get; } = []; + + [ObservableProperty] + public partial ObservableCollection Items { get; set; } = []; + + public List Genders { get; set; } = []; + + public List Departments { get; set; } = []; + + public List Designations { get; set; } = []; + + [ObservableProperty] + public partial ExampleModel? SelectedItem { get; set; } +} + diff --git a/samples/AotTestApp/MainWindow.xaml b/samples/AotTestApp/MainWindow.xaml new file mode 100644 index 00000000..ee2e172c --- /dev/null +++ b/samples/AotTestApp/MainWindow.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AotTestApp/MainWindow.xaml.cs b/samples/AotTestApp/MainWindow.xaml.cs new file mode 100644 index 00000000..4457c08e --- /dev/null +++ b/samples/AotTestApp/MainWindow.xaml.cs @@ -0,0 +1,17 @@ +using Microsoft.UI.Xaml; +using WinUI.TableView; + +namespace AotTestApp; + +public sealed partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + + MainViewModel.InitializeItems(); + ViewModel = new MainViewModel(); + } + + public MainViewModel ViewModel { get; } +} \ No newline at end of file diff --git a/samples/AotTestApp/Package.appxmanifest b/samples/AotTestApp/Package.appxmanifest new file mode 100644 index 00000000..b6a63309 --- /dev/null +++ b/samples/AotTestApp/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + AotTestApp + WaheedAhmed + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AotTestApp/Properties/launchSettings.json b/samples/AotTestApp/Properties/launchSettings.json new file mode 100644 index 00000000..d571cee3 --- /dev/null +++ b/samples/AotTestApp/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "AotTestApp (Package)": { + "commandName": "MsixPackage" + }, + "AotTestApp (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/samples/AotTestApp/app.manifest b/samples/AotTestApp/app.manifest new file mode 100644 index 00000000..c5cdad37 --- /dev/null +++ b/samples/AotTestApp/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/ColumnFilterHandler.cs b/src/ColumnFilterHandler.cs index 9c6e94cd..8a1b67f3 100644 --- a/src/ColumnFilterHandler.cs +++ b/src/ColumnFilterHandler.cs @@ -69,13 +69,13 @@ private IEnumerable GetFilterItemsWithCount(TableViewColumn else filterValues.Add(value, 1); } - IEnumerable nullFilterItem = nullCount > 0 ? [new TableViewFilterItem(isNullItemSelected, null, nullCount)] : []; + IEnumerable nullFilterItem = nullCount > 0 ? [new TableViewFilterItem(isNullItemSelected, null, nullCount, true)] : []; return [.. nullFilterItem,.. filterValues.Select(x => { var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) || (column.IsFiltered && SelectedValues[column].Contains(x.Key)); - return new TableViewFilterItem(isSelected, x.Key, x.Value); + return new TableViewFilterItem(isSelected, x.Key, x.Value, true); }) .OrderByDescending(x=>x.Count)]; } diff --git a/src/Controls/TableViewFilterItemsControl.xaml b/src/Controls/TableViewFilterItemsControl.xaml new file mode 100644 index 00000000..76b3b6fa --- /dev/null +++ b/src/Controls/TableViewFilterItemsControl.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controls/TableViewFilterItemsControl.xaml.cs b/src/Controls/TableViewFilterItemsControl.xaml.cs new file mode 100644 index 00000000..834af92a --- /dev/null +++ b/src/Controls/TableViewFilterItemsControl.xaml.cs @@ -0,0 +1,185 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Windows.System; + +namespace WinUI.TableView.Controls; + +/// +/// Represents the control that displays filter items in the filter flyout of a TableViewColumnHeader. +/// +public partial class TableViewFilterItemsControl : UserControl +{ + private bool _canSetState = true; + private ICollection? _filterItems; + + /// + /// Initializes a new instance of the class. + /// + public TableViewFilterItemsControl() + { + InitializeComponent(); + } + + /// + /// Initializes the state of the . + /// + internal async void Initialize() + { + FilterItems = TableView?.FilterHandler?.GetFilterItems(ColumnHeader?.Column!, null).ToList(); + + if (searchBox is not null) + { + await Task.Delay(100); + await FocusManager.TryFocusAsync(searchBox, FocusState.Programmatic); + } + + if (filterItemsList is not null && filterItemsList.Items.Count > 0) + { + filterItemsList.ScrollIntoView(filterItemsList.Items[0]); + } + } + + /// + /// Clears the search box text. + /// + internal void ClearSearchBox() + { + if (searchBox is not null) + { + searchBox.Text = string.Empty; + } + } + + private void OnSearchBoxTextChanged(object sender, TextChangedEventArgs e) + { + FilterItems = TableView?.FilterHandler?.GetFilterItems(ColumnHeader?.Column!, searchBox!.Text); + } + + /// + /// Handles the KeyDown or PreviewKeyDown event for the searchBox. + /// + private void OnSearchBoxKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Enter && searchBox?.Text.Length > 0) + { + ColumnHeader?.ExecuteOkCommand(); + + e.Handled = true; + } + } + + /// + /// Handles the Checked and Unchecked event for the selectAllCheckBox. + /// + private void OnSelectAllCheckBoxCheckChanged(object sender, RoutedEventArgs e) + { + SetFilterItemsState(selectAllCheckBox.IsChecked is true); + } + + /// + /// Sets the state of the select all checkbox. + /// + internal void SetSelectAllCheckBoxState() + { + if (selectAllCheckBox is null || !_canSetState) + { + return; + } + + + selectAllCheckBox.IsChecked = _filterItems?.All(x => x.IsSelected) ?? false ? true + : _filterItems?.All(x => !x.IsSelected) ?? false ? false + : null; + } + + /// + /// Sets the state of the filter items. + /// + /// The state to set. + internal void SetFilterItemsState(bool isSelected) + { + _canSetState = false; + + foreach (var item in filterItemsList.Items.OfType()) + { + item.IsSelected = isSelected; + } + + _canSetState = true; + } + + /// + /// Attaches property changed handlers to the filter items. + /// + private void AttachPropertyChangedHandlers() + { + if (_filterItems?.Count > 0) + { + foreach (var item in _filterItems) + { + item.PropertyChanged += OnFilterItemPropertyChanged; + } + } + } + + /// + /// Detaches property changed handlers from the filter items. + /// + private void DetachPropertyChangedHandlers() + { + if (_filterItems?.Count > 0) + { + foreach (var item in _filterItems) + { + item.PropertyChanged -= OnFilterItemPropertyChanged; + } + } + } + + /// + /// Handles the PropertyChanged event for filter items. + /// + private void OnFilterItemPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + SetSelectAllCheckBoxState(); + } + + /// + /// Gets a value indicating whether to apply the filter based on the control state. + /// + internal bool ShouldApplyFilter => selectAllCheckBox.IsChecked is not true || !string.IsNullOrEmpty(searchBox.Text); + + /// + /// Gets or sets the filter items for the control. + /// + internal ICollection? FilterItems + { + get => _filterItems; + set + { + if (_filterItems == value) return; + + DetachPropertyChangedHandlers(); + _filterItems = value; + filterItemsList.ItemsSource = _filterItems; + AttachPropertyChangedHandlers(); + SetSelectAllCheckBoxState(); + } + } + + /// + /// Gets or sets the column header associated with the filter items control. + /// + public TableViewColumnHeader? ColumnHeader { get; internal set; } + + /// + /// Gets or sets the TableView associated with the filter items control. + /// + public TableView? TableView { get; internal set; } +} diff --git a/src/Extensions/TypeExtensions.cs b/src/Extensions/TypeExtensions.cs index ffb3672f..13b83f17 100644 --- a/src/Extensions/TypeExtensions.cs +++ b/src/Extensions/TypeExtensions.cs @@ -124,7 +124,7 @@ public static bool IsPrimitive(this Type? dataType) /// public static bool IsInheritedFromIComparable(this Type type) { - return type.GetInterfaces().Any(i => i == typeof(IComparable)); + return typeof(IComparable).IsAssignableFrom(type); } private static Type? FindGenericType(Type definition, Type type) diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index d90e7d26..e0f762a8 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -60,7 +60,7 @@ public partial class TableView /// /// Identifies the ShowExportOptions dependency property. /// - public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false)); + public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false, OnShowExportOptionsChanged)); /// /// Identifies the AutoGenerateColumns dependency property. @@ -827,6 +827,17 @@ private static void OnSelectionModeChanged(DependencyObject d, DependencyPropert } } + /// + /// Handles changes to the ShowExportOptions property. + /// + private static void OnShowExportOptionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + tableView._headerRow?.SetExportOptionsVisibility(); + } + } + /// /// Handles changes to the AutoGenerateColumns property. /// diff --git a/src/TableViewColumnHeader.OptionComamnds.cs b/src/TableViewColumnHeader.OptionComamnds.cs new file mode 100644 index 00000000..fb78faca --- /dev/null +++ b/src/TableViewColumnHeader.OptionComamnds.cs @@ -0,0 +1,77 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using WinUI.TableView.Extensions; +using SD = WinUI.TableView.SortDirection; + +namespace WinUI.TableView; + +partial class TableViewColumnHeader +{ + private bool _commandsInitialized; + private readonly StandardUICommand _sortAscendingCommand = new() { Label = TableViewLocalizedStrings.SortAscending }; + private readonly StandardUICommand _sortDescendingCommand = new() { Label = TableViewLocalizedStrings.SortDescending }; + private readonly StandardUICommand _clearSortingCommand = new() { Label = TableViewLocalizedStrings.ClearSorting }; + private readonly StandardUICommand _clearFilterCommand = new() { Label = TableViewLocalizedStrings.ClearFilter }; + private readonly StandardUICommand _okCommand = new() { Label = TableViewLocalizedStrings.Ok }; + private readonly StandardUICommand _cancelCommand = new() { Label = TableViewLocalizedStrings.Cancel }; + + /// + /// Sets commands to option menu items. + /// + private void SetOptionCommands() + { + InitializeCommands(); + + if (GetTemplateChild("SortAscendingMenuItem") is MenuFlyoutItem sortAscendingMenuItem) + sortAscendingMenuItem.Command = _sortAscendingCommand; + if (GetTemplateChild("SortDescendingMenuItem") is MenuFlyoutItem sortDescendingMenuItem) + sortDescendingMenuItem.Command = _sortDescendingCommand; + if (GetTemplateChild("ClearSortingMenuItem") is MenuFlyoutItem clearSortingMenuItem) + clearSortingMenuItem.Command = _clearSortingCommand; + if (GetTemplateChild("ClearFilterMenuItem") is MenuFlyoutItem clearFilterMenuItem) + clearFilterMenuItem.Command = _clearFilterCommand; + if (GetTemplateChild("ActionButtonsMenuItem") is MenuFlyoutItem actionButtonsMenuItem) + { + actionButtonsMenuItem.ApplyTemplate(); + + if (actionButtonsMenuItem.FindDescendant