Skip to content

Commit 173433e

Browse files
BornToBeRootclaude
andcommitted
Fix: Columns can be resized beyond the window in all tools (#3557)
Roll out DataGridHelper.FillRemainingWidth (prototype in the IP Scanner, renamed from FillLastColumn) to every DataGrid with a star column: the star width is removed, the grid fills the remaining width with a pixel width - the column marked with the new DataGridHelper.IsFillColumn (if the star column was not the last one, e.g. Traceroute hostname, Firewall, DNS Lookup, About) or the last visible column. Grids with several star columns (port / OID profiles): the widest one fills, the others get a MinWidth. Fill columns without MinWidth get MinWidth 150. RefreshStarColumnsOnFirstRow removed (no star columns left). Convention added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9c288e9 commit 173433e

28 files changed

Lines changed: 170 additions & 153 deletions

‎.claude/skills/feature-review/references/conventions.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ Reference: `HostsFileEditorView.xaml`, `FirewallView.xaml`.
102102
Other users: `RunCommandViewModel.Suggestions`. Not yet used (same `Clear()` + `ForEach(Results.Add)` pattern on every
103103
refresh / auto refresh): `ConnectionsViewModel`, `ListenersViewModel`, `NeighborTableViewModel`,
104104
`HostsFileEditorViewModel` — report as a performance finding when reviewing them.
105+
- Column widths: no star (`Width="*"`) columns in a `DataGrid` — with a visible star column WPF only lets a column
106+
grow by taking the width from the columns to its right, so columns can't be made wider than the window (#3557).
107+
Use `wpfHelpers:DataGridHelper.FillRemainingWidth="True"` on the grid: the last visible column (or the one marked with
108+
`wpfHelpers:DataGridHelper.IsFillColumn="True"`) fills the remaining width with a pixel width, every column can be
109+
resized freely and the grid scrolls horizontally. Give the fill column a `MinWidth`.
105110
- Bindings on items that implement `INotifyPropertyChanged` (e.g. `IPScannerHostInfo`, view models via `BindingProxy`)
106111
use **plain paths** (`IsPingPending`, `PingInfo.Status`, `Data.ScanOptions.ResolveHostname`), not the typed form
107112
`(network:Type.Property)`. WPF registers a `PropertyChanged` listener with the property name the path parser stored;

‎Source/NETworkManager.Utilities.WPF/DataGridHelper.cs‎

Lines changed: 39 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,84 +13,55 @@ namespace NETworkManager.Utilities.WPF;
1313
/// </summary>
1414
public static class DataGridHelper
1515
{
16-
/// <summary>
17-
/// When set to <see langword="true"/>, forces star-sized columns to recompute their width after
18-
/// the first row is realized. Without this, an initially empty <see cref="DataGrid"/> sizes star
19-
/// columns to <c>MinWidth</c> because the inner <see cref="System.Windows.Controls.ScrollViewer"/>
20-
/// measures with infinite width; only a window resize would otherwise trigger a correct re-measure.
21-
/// The handler unsubscribes itself after the first row so there is no ongoing overhead.
22-
/// </summary>
23-
public static readonly DependencyProperty RefreshStarColumnsOnFirstRowProperty =
24-
DependencyProperty.RegisterAttached(
25-
"RefreshStarColumnsOnFirstRow",
26-
typeof(bool),
27-
typeof(DataGridHelper),
28-
new PropertyMetadata(false, OnRefreshStarColumnsOnFirstRowChanged));
29-
30-
public static void SetRefreshStarColumnsOnFirstRow(DataGrid element, bool value) =>
31-
element.SetValue(RefreshStarColumnsOnFirstRowProperty, value);
32-
33-
public static bool GetRefreshStarColumnsOnFirstRow(DataGrid element) =>
34-
(bool)element.GetValue(RefreshStarColumnsOnFirstRowProperty);
35-
36-
private static void OnRefreshStarColumnsOnFirstRowChanged(DependencyObject d,
37-
DependencyPropertyChangedEventArgs e)
38-
{
39-
if (d is not DataGrid dataGrid || e.NewValue is not bool enabled || !enabled)
40-
return;
41-
42-
dataGrid.LoadingRow += Handler;
43-
return;
44-
45-
void Handler(object sender, DataGridRowEventArgs args)
46-
{
47-
dataGrid.LoadingRow -= Handler;
48-
49-
dataGrid.Dispatcher.BeginInvoke(new Action(() =>
50-
{
51-
foreach (var column in dataGrid.Columns)
52-
{
53-
if (!column.Width.IsStar)
54-
continue;
55-
56-
var width = column.Width;
57-
column.Width = 0;
58-
column.Width = width;
59-
}
60-
}), DispatcherPriority.ContextIdle);
61-
}
62-
}
63-
64-
#region FillLastColumn
16+
#region FillRemainingWidth
6517

6618
/// <summary>
67-
/// When set to <see langword="true"/>, the last visible column fills the remaining width of the
68-
/// <see cref="DataGrid"/> - like a star (<c>*</c>) column, but with a pixel width. With a visible star column the
19+
/// When set to <see langword="true"/>, a column fills the remaining width of the <see cref="DataGrid"/> - like a
20+
/// star (<c>*</c>) column, but with a pixel width: the visible column marked with <see cref="IsFillColumnProperty"/>,
21+
/// otherwise the last visible column. With a visible star column the
6922
/// DataGrid only lets a column grow by taking the width from the columns to its right (down to their
7023
/// <c>MinWidth</c>), so a column can't be resized wider than the grid (also not with a double-click on the
7124
/// gripper), there is never a horizontal scrollbar (#3557). Without star columns a column grows freely and the
7225
/// grid scrolls horizontally.
7326
/// <para>
7427
/// The fill column gets max(base width, available width - other visible columns). The base width is its
7528
/// <c>MinWidth</c>, or the width the user resized it to. It is updated when the size of the grid / its viewport
76-
/// changes and when another column is resized or shown / hidden. A column that is no longer the last visible one
29+
/// changes and when another column is resized or shown / hidden. A column that no longer fills (e.g. hidden)
7730
/// gets its original width back. Don't use star columns in the same grid.
7831
/// </para>
7932
/// </summary>
80-
public static readonly DependencyProperty FillLastColumnProperty =
33+
public static readonly DependencyProperty FillRemainingWidthProperty =
8134
DependencyProperty.RegisterAttached(
82-
"FillLastColumn",
35+
"FillRemainingWidth",
8336
typeof(bool),
8437
typeof(DataGridHelper),
85-
new PropertyMetadata(false, OnFillLastColumnChanged));
38+
new PropertyMetadata(false, OnFillRemainingWidthChanged));
8639

87-
public static void SetFillLastColumn(DataGrid element, bool value) =>
88-
element.SetValue(FillLastColumnProperty, value);
40+
public static void SetFillRemainingWidth(DataGrid element, bool value) =>
41+
element.SetValue(FillRemainingWidthProperty, value);
8942

90-
public static bool GetFillLastColumn(DataGrid element) =>
91-
(bool)element.GetValue(FillLastColumnProperty);
43+
public static bool GetFillRemainingWidth(DataGrid element) =>
44+
(bool)element.GetValue(FillRemainingWidthProperty);
9245

93-
// State of a grid with FillLastColumn (subscriptions, current fill column)
46+
/// <summary>
47+
/// Marks the column that fills the remaining width (see <see cref="FillRemainingWidthProperty"/>), if it isn't the
48+
/// last column (e.g. a description in the middle of the columns). While it is hidden, the last visible column
49+
/// fills.
50+
/// </summary>
51+
public static readonly DependencyProperty IsFillColumnProperty =
52+
DependencyProperty.RegisterAttached(
53+
"IsFillColumn",
54+
typeof(bool),
55+
typeof(DataGridHelper),
56+
new PropertyMetadata(false));
57+
58+
public static void SetIsFillColumn(DataGridColumn element, bool value) =>
59+
element.SetValue(IsFillColumnProperty, value);
60+
61+
public static bool GetIsFillColumn(DataGridColumn element) =>
62+
(bool)element.GetValue(IsFillColumnProperty);
63+
64+
// State of a grid with FillRemainingWidth (subscriptions, current fill column)
9465
private static readonly DependencyProperty FillStateProperty =
9566
DependencyProperty.RegisterAttached(
9667
"FillState",
@@ -104,7 +75,7 @@ public static bool GetFillLastColumn(DataGrid element) =>
10475
private static readonly DependencyPropertyDescriptor ColumnVisibilityDescriptor =
10576
DependencyPropertyDescriptor.FromProperty(DataGridColumn.VisibilityProperty, typeof(DataGridColumn));
10677

107-
private static void OnFillLastColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
78+
private static void OnFillRemainingWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
10879
{
10980
if (d is not DataGrid dataGrid)
11081
return;
@@ -239,22 +210,25 @@ private void Update()
239210
if (!dataGrid.IsLoaded)
240211
return;
241212

242-
var lastVisibleColumn = dataGrid.Columns
213+
var visibleColumns = dataGrid.Columns
243214
.Where(x => x.Visibility == Visibility.Visible)
244215
.OrderBy(x => x.DisplayIndex)
245-
.LastOrDefault();
216+
.ToList();
217+
218+
// Marked fill column, otherwise the last visible column
219+
var fillColumn = visibleColumns.FirstOrDefault(GetIsFillColumn) ?? visibleColumns.LastOrDefault();
246220

247221
_isUpdating = true;
248222

249223
try
250224
{
251-
if (!ReferenceEquals(lastVisibleColumn, _fillColumn))
225+
if (!ReferenceEquals(fillColumn, _fillColumn))
252226
{
253227
// Give the previous fill column its original width back
254228
if (_fillColumn != null && dataGrid.Columns.Contains(_fillColumn))
255229
_fillColumn.Width = _fillColumnOriginalWidth;
256230

257-
_fillColumn = lastVisibleColumn;
231+
_fillColumn = fillColumn;
258232
_lastSetWidth = double.NaN;
259233

260234
if (_fillColumn == null)

‎Source/NETworkManager/Views/AboutView.xaml‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
1010
xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters"
1111
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
12+
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
1213
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
1314
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:AboutViewModel}">
1415
<UserControl.Resources>
@@ -249,7 +250,8 @@
249250
<DataGrid Grid.Column="0" Grid.Row="2"
250251
ItemsSource="{Binding LibrariesView}"
251252
SelectedItem="{Binding SelectedLibraryInfo}"
252-
Margin="0,0,0,10">
253+
Margin="0,0,0,10"
254+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
253255
<DataGrid.Resources>
254256
<ContextMenu x:Key="RowContextMenu" Opened="ContextMenu_Opened" MinWidth="150">
255257
<MenuItem Header="{x:Static localization:Strings.OpenProject}"
@@ -292,7 +294,7 @@
292294
Binding="{Binding (documentation:LibraryInfo.Name)}" MinWidth="150" />
293295
<DataGridTextColumn Header="{x:Static localization:Strings.Description}"
294296
Binding="{Binding (documentation:LibraryInfo.Description)}" MinWidth="300"
295-
Width="*">
297+
wpfHelpers:DataGridHelper.IsFillColumn="True">
296298
<DataGridTextColumn.ElementStyle>
297299
<Style>
298300
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
@@ -330,7 +332,8 @@
330332
<DataGrid Grid.Column="0" Grid.Row="5"
331333
x:Name="DataGridExternalServices"
332334
ItemsSource="{Binding ExternalServicesView}"
333-
SelectedItem="{Binding SelectedExternalServicesInfo}" Margin="0,0,0,20">
335+
SelectedItem="{Binding SelectedExternalServicesInfo}" Margin="0,0,0,20"
336+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
334337
<DataGrid.Resources>
335338
<ContextMenu x:Key="RowContextMenu" Opened="ContextMenu_Opened" MinWidth="150">
336339
<MenuItem Header="{x:Static localization:Strings.OpenWebsite}"
@@ -362,7 +365,7 @@
362365
Binding="{Binding (documentation:ExternalServicesInfo.Name)}" MinWidth="150" />
363366
<DataGridTextColumn Header="{x:Static localization:Strings.Description}"
364367
Binding="{Binding (documentation:ExternalServicesInfo.Description)}"
365-
MinWidth="300" Width="*">
368+
MinWidth="300">
366369
<DataGridTextColumn.ElementStyle>
367370
<Style>
368371
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
@@ -377,7 +380,8 @@
377380
<DataGrid Grid.Column="0" Grid.Row="7"
378381
x:Name="DataGridResources"
379382
ItemsSource="{Binding ResourcesView}"
380-
SelectedItem="{Binding SelectedResourceInfo}">
383+
SelectedItem="{Binding SelectedResourceInfo}"
384+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
381385
<DataGrid.Resources>
382386
<ContextMenu x:Key="RowContextMenu" Opened="ContextMenu_Opened" MinWidth="150">
383387
<MenuItem Header="{x:Static localization:Strings.OpenWebsite}"
@@ -408,8 +412,7 @@
408412
<DataGridTextColumn Header="{x:Static localization:Strings.Name}"
409413
Binding="{Binding (documentation:ResourceInfo.Name)}" MinWidth="150" />
410414
<DataGridTextColumn Header="{x:Static localization:Strings.Description}"
411-
Binding="{Binding (documentation:ResourceInfo.Description)}" MinWidth="300"
412-
Width="*">
415+
Binding="{Binding (documentation:ResourceInfo.Description)}" MinWidth="300">
413416
<DataGridTextColumn.ElementStyle>
414417
<Style>
415418
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />

‎Source/NETworkManager/Views/ConnectionsView.xaml‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
SelectedItem="{Binding SelectedResult}"
104104
SelectedItemsList="{Binding SelectedResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
105105
Sorting="DataGrid_OnSorting"
106-
wpfHelpers:DataGridHelper.RefreshStarColumnsOnFirstRow="True">
106+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
107107
<DataGrid.Resources>
108108
<ContextMenu x:Key="RowContextMenu" Opened="ContextMenu_Opened" MinWidth="150">
109109
<MenuItem Header="{x:Static localization:Strings.CopyDots}"
@@ -215,8 +215,7 @@
215215
SortMemberPath="ProcessName" MinWidth="150" />
216216
<DataGridTextColumn Header="{x:Static Member=localization:Strings.ProcessPath}"
217217
Binding="{Binding Path=(network:ConnectionInfo.ProcessPath)}"
218-
SortMemberPath="ProcessPath" MinWidth="150"
219-
Width="*" />
218+
SortMemberPath="ProcessPath" MinWidth="150" />
220219
</DataGrid.Columns>
221220
</controls:MultiSelectDataGrid>
222221
<WrapPanel Grid.Column="0" Grid.Row="4" Orientation="Horizontal"

‎Source/NETworkManager/Views/DNSLookupView.xaml‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
ItemsSource="{Binding ResultsView}"
154154
SelectedItem="{Binding SelectedResult}"
155155
SelectedItemsList="{Binding SelectedResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
156-
wpfHelpers:DataGridHelper.RefreshStarColumnsOnFirstRow="True">
156+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
157157
<controls:MultiSelectDataGrid.Resources>
158158
<ContextMenu x:Key="RowContextMenu" Opened="ContextMenu_Opened" MinWidth="150">
159159
<MenuItem Header="{x:Static Member=localization:Strings.CopyDots}"
@@ -257,8 +257,7 @@
257257
<DataGridTextColumn Header="{x:Static localization:Strings.Result}"
258258
Binding="{Binding Path=(network:DNSLookupRecordInfo.Result)}"
259259
SortMemberPath="Result"
260-
MinWidth="200"
261-
Width="*">
260+
MinWidth="200">
262261
<DataGridTextColumn.ElementStyle>
263262
<Style>
264263
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />

‎Source/NETworkManager/Views/FirewallView.xaml‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@
9696
ItemsSource="{Binding ResultsView}"
9797
SelectedItem="{Binding SelectedResult}"
9898
SelectedItemsList="{Binding SelectedResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
99-
wpfHelpers:DataGridHelper.RefreshStarColumnsOnFirstRow="True"
100-
RowDetailsVisibilityMode="Collapsed">
99+
RowDetailsVisibilityMode="Collapsed"
100+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
101101
<controls:MultiSelectDataGrid.InputBindings>
102102
<KeyBinding Command="{Binding EditRuleCommand}" Key="F2" />
103103
<KeyBinding Command="{Binding DeleteRuleCommand}" Key="Delete" />
@@ -601,8 +601,7 @@
601601
<!-- Remote ports -->
602602
<DataGridTextColumn Header="{x:Static localization:Strings.RemotePort}"
603603
SortMemberPath="RemotePortsDisplay"
604-
MinWidth="100"
605-
Width="*">
604+
MinWidth="100">
606605
<DataGridTextColumn.Binding>
607606
<Binding Path="(firewall:FirewallRule.RemotePortsDisplay)">
608607
<Binding.TargetNullValue>

‎Source/NETworkManager/Views/IPScannerSettingsView.xaml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
99
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
1010
xmlns:utilities="clr-namespace:NETworkManager.Utilities;assembly=NETworkManager.Utilities"
11+
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
1112
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
1213
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:IPScannerSettingsViewModel}">
1314
<StackPanel>
@@ -65,7 +66,8 @@
6566
IsOn="{Binding ResolveMACAddress}" Margin="0,0,0,20" />
6667
<TextBlock Text="{x:Static localization:Strings.CustomCommands}" Style="{StaticResource HeaderTextBlock}" />
6768
<DataGrid x:Name="DataGridCustomCommands" ItemsSource="{Binding CustomCommands}"
68-
SelectedItem="{Binding SelectedCustomCommand}" Height="200" Margin="0,0,0,10">
69+
SelectedItem="{Binding SelectedCustomCommand}" Height="200" Margin="0,0,0,10"
70+
wpfHelpers:DataGridHelper.FillRemainingWidth="True">
6971
<DataGrid.Resources>
7072
<ContextMenu x:Key="RowContextMenu" Opened="RowContextMenu_OnOpened" MinWidth="150">
7173
<MenuItem Header="{x:Static localization:Strings.EditDots}"
@@ -97,8 +99,7 @@
9799
Binding="{Binding (utilities:CustomCommandInfo.FilePath)}" MinWidth="150"
98100
Width="Auto" />
99101
<DataGridTextColumn Header="{x:Static localization:Strings.Arguments}"
100-
Binding="{Binding (utilities:CustomCommandInfo.Arguments)}" MinWidth="200"
101-
Width="*" />
102+
Binding="{Binding (utilities:CustomCommandInfo.Arguments)}" MinWidth="200" />
102103
</DataGrid.Columns>
103104
<DataGrid.InputBindings>
104105
<KeyBinding Command="{Binding EditCustomCommandCommand}" Key="F2" />

0 commit comments

Comments
 (0)