diff --git a/ILSpy/Metadata/MetadataColumnBuilder.cs b/ILSpy/Metadata/MetadataColumnBuilder.cs index 85045663c..a5d7e43e1 100644 --- a/ILSpy/Metadata/MetadataColumnBuilder.cs +++ b/ILSpy/Metadata/MetadataColumnBuilder.cs @@ -59,15 +59,15 @@ namespace ILSpy.Metadata { ArgumentNullException.ThrowIfNull(entryType); return descriptorCache.GetOrAdd(entryType, BuildDescriptors) - .Select(d => BuildColumn(d, filter: null)) + .Select(BuildColumn) .ToArray(); } /// /// Populates with both Columns and - /// ColumnFilters in matched order. Each column's header is a vertical pair — - /// the column name above a small TextBox bound two-way to the column's filter — so - /// the user can filter every column independently and the predicate ANDs the lot. + /// ColumnFilters in matched order. The view renders a column-aligned filter + /// row above the grid bound to the page's ColumnFilters; the predicate ANDs + /// every non-empty filter against its own column. /// public static void Populate(MetadataTablePageModel page) => Populate(page, typeof(TEntry)); @@ -80,9 +80,8 @@ namespace ILSpy.Metadata var columns = new List(descriptors.Length); foreach (var d in descriptors) { - var filter = new ColumnFilter(d.Name); - page.ColumnFilters.Add(filter); - columns.Add(BuildColumn(d, filter)); + page.ColumnFilters.Add(new ColumnFilter(d.Name)); + columns.Add(BuildColumn(d)); } page.Columns = columns; } @@ -103,53 +102,16 @@ namespace ILSpy.Metadata return list.ToArray(); } - static DataGridColumn BuildColumn(ColumnDescriptor d, ColumnFilter? filter) + static DataGridColumn BuildColumn(ColumnDescriptor d) { var column = d.Info?.Kind == ColumnKind.Token ? (DataGridColumn)BuildTokenColumn(d.Property, d.Info) : BuildTextColumn(d.Property, d.Info); - column.Header = filter is null ? d.Name : (object)BuildHeader(d.Name, filter); - // Header is a Panel when filters are wired, so callers can't recover the column - // name via Header.ToString(). Stash the name on Tag for hover-tooltip lookup, - // "Go to token" context-menu resolution, and any future cell-level navigation. + column.Header = d.Name; column.Tag = d.Name; return column; } - static Control BuildHeader(string columnName, ColumnFilter filter) - { - var label = new TextBlock { - Text = columnName, - FontWeight = FontWeight.SemiBold, - HorizontalAlignment = HorizontalAlignment.Stretch, - }; - var box = new TextBox { - MinHeight = 0, - Padding = new Thickness(2, 1), - Margin = new Thickness(0, 2, 0, 0), - FontWeight = FontWeight.Normal, - HorizontalAlignment = HorizontalAlignment.Stretch, - Text = filter.Text, - }; - // Wire TextBox <-> ColumnFilter imperatively. Avalonia's INPC binding plugin - // holds a WeakReference to the source and resolves the target lazily; for - // Header-hosted controls it reliably comes back null on write-back, throwing - // "Non-static method requires a target" out of PropertyInfo.SetValue. The plain - // event subscription side-steps the weak-ref machinery. - box.TextChanged += (_, _) => { - if (filter.Text != box.Text) - filter.Text = box.Text; - }; - filter.PropertyChanged += (_, e) => { - if (e.PropertyName == nameof(ColumnFilter.Text) && box.Text != filter.Text) - box.Text = filter.Text; - }; - return new StackPanel { - Orientation = Orientation.Vertical, - Children = { label, box }, - }; - } - static DataGridTextColumn BuildTextColumn(PropertyInfo prop, ColumnInfoAttribute? info) { var binding = new Binding(prop.Name) { Mode = BindingMode.OneWay }; diff --git a/ILSpy/Views/MetadataTablePage.axaml b/ILSpy/Views/MetadataTablePage.axaml index cc190d1b0..b9998060a 100644 --- a/ILSpy/Views/MetadataTablePage.axaml +++ b/ILSpy/Views/MetadataTablePage.axaml @@ -7,21 +7,30 @@ x:Class="ILSpy.Views.MetadataTablePage" x:DataType="vm:MetadataTablePageModel"> - - + + + + + + + diff --git a/ILSpy/Views/MetadataTablePage.axaml.cs b/ILSpy/Views/MetadataTablePage.axaml.cs index 195b42f35..4ddf4bcb1 100644 --- a/ILSpy/Views/MetadataTablePage.axaml.cs +++ b/ILSpy/Views/MetadataTablePage.axaml.cs @@ -25,6 +25,7 @@ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; @@ -165,11 +166,13 @@ namespace ILSpy.Views // the new schema has more columns than the old one. grid.ItemsSource = Array.Empty(); grid.Columns.Clear(); + ClearFilterRow(); itemsView = null; if (boundModel == null) return; foreach (var c in boundModel.Columns) grid.Columns.Add(c); + BuildFilterRow(grid, boundModel); var model = boundModel; itemsView = new DataGridCollectionView(model.Items) { Filter = item => MetadataTablePageModel.MatchesFilters(item, model.ColumnFilters), @@ -177,6 +180,64 @@ namespace ILSpy.Views grid.ItemsSource = itemsView; } + void ClearFilterRow() + { + var grid = this.FindControl("Grid"); + if (grid != null) + grid.LayoutUpdated -= SyncFilterRowWidths; + var row = this.FindControl("FilterRow"); + row?.Children.Clear(); + } + + void BuildFilterRow(DataGrid grid, MetadataTablePageModel model) + { + var row = this.FindControl("FilterRow"); + if (row is null) + return; + for (int i = 0; i < grid.Columns.Count && i < model.ColumnFilters.Count; i++) + { + var filter = model.ColumnFilters[i]; + var box = new TextBox { + MinHeight = 0, + Padding = new Thickness(2, 1), + Margin = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Stretch, + Tag = grid.Columns[i], + PlaceholderText = filter.ColumnName, + Text = filter.Text, + }; + // Two-way bridge between the box and the filter via plain events. The filter's + // PropertyChanged handler is hooked from the page model side and re-raises a + // single ColumnFilterChanged the view uses to refresh the collection view. + box.TextChanged += (_, _) => { + if (filter.Text != box.Text) + filter.Text = box.Text; + }; + filter.PropertyChanged += (_, e) => { + if (e.PropertyName == nameof(ColumnFilter.Text) && box.Text != filter.Text) + box.Text = filter.Text; + }; + row.Children.Add(box); + } + // Snap each input to the corresponding column's ActualWidth on every layout + // pass — DataGridColumn.ActualWidth isn't a styled property, so we can't + // subscribe to it; piggy-backing on LayoutUpdated is cheap and reliable. + grid.LayoutUpdated += SyncFilterRowWidths; + SyncFilterRowWidths(grid, EventArgs.Empty); + } + + void SyncFilterRowWidths(object? sender, EventArgs e) + { + var row = this.FindControl("FilterRow"); + if (row is null) + return; + foreach (var child in row.Children) + { + if (child is TextBox box && box.Tag is DataGridColumn column) + box.Width = column.ActualWidth; + } + } + void ApplyScrollTarget() { if (boundModel?.ScrollToRow is not int row)