I didn't know how to properly word my issue in a limited title so I will try to explain it as good as I can. I made gifs for showcases but noticed after I don't have enough reputation points.

EDIT: I have enough reputation now for gifs!

I have a WPF application where I make use of DataGrid to display a list of ViewModels. This DataGrid is populated with data from a folder that you can open with a FolderBrowserDialog attached to a button. When you've set the path before, the application remembers it and on startup it will populate the DataGrid automatically.

This means there are two states: opening the application without the DataGrid populated, and opening the application with the DataGrid populated. This is important to note for later on.

The problem that I'm trying to solve is that whenever the DataGrid is populated with data, I need all my columns to auto resize to the width of their content. However, the last column's width needs to fill all the remaining empty space so the row inside of the DataGrid is selectable throughout the whole width of the DataGrid. At the same time, when you resize the application to where the DataGrid's width becomes narrower than the total width of the columns combined, a horizontal scrollbar needs to appear so you can scroll through the colums with their width still at the same size as their content.

This needs to happen not only when the DataGrid gets populated with Data through the button, but also when the application starts with the data automatically populating the DataGrid.

My difficulties have been finding a solution that works for any situation.

I have tried many approaches but I'll name the most succesful ones:

Approach 1: Set width of all columns to auto, with the last column to fill ( * )

Code:

<Grid Grid.Column="0"
      Grid.ColumnSpan="2"
      Grid.Row="3" 
      Background="#f7f5f2"
      Margin="10">
    <DataGrid x:Name="ModList" 
              ItemsSource="{Binding Mods}"
              Style="{DynamicResource DataGridStyle1}" 
              CellStyle="{DynamicResource DataGridCellStyle1}" 
              ColumnHeaderStyle="{DynamicResource DataGridColumnHeaderStyle1}" 
              RowStyle="{DynamicResource DataGridRowStyle1}"
              RowDetailsTemplate="{DynamicResource DataGridRowDetailsTemplate1}"
              Margin="10"
              dd:DragDrop.IsDragSource="True" 
              dd:DragDrop.IsDropTarget="True" 
              dd:DragDrop.SelectDroppedItems="True" 
              dd:DragDrop.DropHandler="{Binding}" 
              dd:DragDrop.DropTargetAdornerBrush="#484D54">
     
        <DataGrid.Columns>
            <DataGridTemplateColumn x:Name="DataGridColumnEnabled"
                                    Width="Auto">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <CheckBox x:Name="CheckBoxIsEnabled"
                                  IsChecked="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged}"
                                  Command="{Binding DataContext.ToggleCheckBoxCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTextColumn x:Name="DataGridColumnLoadorder"
                                Header="Loadorder" 
                                IsReadOnly="True" 
                                Width="Auto" 
                                Binding="{Binding LoadOrder, UpdateSourceTrigger=PropertyChanged}" 
                                CanUserSort="False"/>
            <DataGridTextColumn x:Name="DataGridColumnMod"
                                Header="Mod" 
                                IsReadOnly="True"
                                Width="Auto"
                                Binding="{Binding DisplayName}" 
                                CanUserSort="False"
                                VirtualizingPanel.VirtualizationMode="Standard"/>
            <DataGridTemplateColumn x:Name="DataGridColumnNotification"
                                    IsReadOnly="True"
                                    Width="Auto"
                                    CanUserSort="False">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image Source="/WarningIcon.png"
                               Height="16"
                               Width="16"
                               HorizontalAlignment="Stretch"
                               VerticalAlignment="Stretch"
                               Cursor="Help"
                               Visibility="{Binding HasConflicts}">
                            <ToolTipService.ToolTip>
                                <ToolTip Content="Mod(s) detected altering the same asset(s). &#x0a;This may be intentional, please check before committing loadorder."/>
                            </ToolTipService.ToolTip>
                        </Image>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTextColumn x:Name="DataGridColumnAuthor"
                                Header="Author" 
                                IsReadOnly="True" 
                                Width="Auto" 
                                Binding="{Binding Author}" 
                                CanUserSort="False"/>
            <DataGridTextColumn x:Name="DataGridColumnVersion"
                                Header="Version" 
                                IsReadOnly="True" 
                                Width="Auto"
                                Binding="{Binding Version}" 
                                CanUserSort="False"/>
            <DataGridTextColumn x:Name="DataGridColumnSource"
                                Header="Source"
                                IsReadOnly="True"
                                Width="*"
                                Binding="{Binding Source}"
                                CanUserSort="False"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Pro's:

Rows are selectable throughout the DataGrid no matter the width.

Full Selectable Row

Cons:

Columns get mashed together when resizing and no horizontal scrollbar (it does flicker while resizing which I dont know why that is though).

Mashed columns

Populating empty DataGrid with Data causes columns to not auto resize to content.

Columns not auto resizing

Approach 2: Same as above but handling MinWidth of columns in code-behind

Code:

Added event to DataGrid in my XAML:

SizeChanged="ModList_SizeChanged"

Code-behind:

public partial class MainWindow : Window
{
    
    // Other code

    private void ModList_SizeChanged(object sender, EventArgs e)
    {
        DataGridColumnEnabled.MinWidth = DataGridColumnEnabled.ActualWidth;
        DataGridColumnLoadorder.MinWidth = DataGridColumnLoadorder.ActualWidth;
        DataGridColumnMod.MinWidth = DataGridColumnMod.ActualWidth;
        DataGridColumnNotification.MinWidth = DataGridColumnNotification.ActualWidth;
        DataGridColumnAuthor.MinWidth = DataGridColumnAuthor.ActualWidth;
        DataGridColumnVersion.MinWidth = DataGridColumnVersion.ActualWidth;
        DataGridColumnSource.MinWidth = 200;
    }
}

Pro:

Horizontal scrollbar now appears when resizing window

Horizontal scrollbar

Con

Populating empty DataGrid with Data still causes columns to not auto resize to content.

Conclusion

The last approach is the one I've been using now. I need to find a solution which basically accomplishes the same pro's, but also automatically resizes all the columns when I populate the DataGrid.

I'm starting to feel like I'm thinking in the wrong direction which is why I've decided to seek help through here.

I really hope someone could provide me with a proper solution to have an auto resizing DataGrid that makes use of a horizontal scrollbar when the window is too small.

Thank you for reading this post and I hope I've structured it in a decent way.

Looking forward to your suggestions!

1

There are 1 best solutions below

0
BionicCode On

You must not set the column's width to * width as this would change the arrangement behavior as it will force to maximize the space for the * column. Hence auto columns will shrink (based on the layout algorithm of the DataGrid.

Instead, you must explicitly calculate the width for the last column to make it fill. To preserve the default column sizing (like size to cells or size to headers) you must allow the DataGid to complete the original layout algorithm. Then finally adjust the last column.

For a graceful solution you may want to consider extending the DataGrid so that you can override ArrangeOverride to prepend the custom calculations for the last column.

The following example extends DataGrid to implement the logic internally. Alternatively, you can move the adjustment logic to an attached behavior or to the event handler of the DataGrid.SizeChanged event. From a design perspective extending DataGrid is the cleanest solution as the layout logic is properly encapsulated by the element itself.

<!-- 
     Setting the DataGrid.ColumnWidth is not required. 
     This example does this because one requirement was 
     to size the columns based on the cell content.
-->
<ExtendedDataGrid LastChildFill="True"
                  ColumnWidth="{x:Static DataGridLength.SizeToCells}" />
public class ExtendedDataGrid : DataGrid
{
  public bool LastChildFill
  {
    get => (bool)GetValue(LastChildFillProperty);
    set => SetValue(LastChildFillProperty, value);
  }

  public static readonly DependencyProperty LastChildFillProperty = DependencyProperty.Register(
    "LastChildFill",
    typeof(bool),
    typeof(ExtendedDataGrid),
    new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsArrange, OnLastChildFillChanged));

  private static void OnLastChildFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    => ((ExtendedDataGrid)d).OnLastChildFillChanged((bool)e.OldValue, (bool)e.NewValue);

  private const string VerticalScrollBarPartName = "PART_VerticalScrollBar";
  private ScrollBar PART_VerticalScrollBar { get; set; }
  private ScrollBarVisibility HorizontalScrollBarVisibilityInternal { get; set; }

  public ExtendedDataGrid()
  {
    this.HorizontalScrollBarVisibilityInternal = this.HorizontalScrollBarVisibility;
    this.Loaded += OnLoaded;
  }

  private void OnLoaded(object sender, RoutedEventArgs e)
  {
    this.Loaded -= OnLoaded;

    if (TryFindVisualChildElementByName(this, VerticalScrollBarPartName, out ScrollBar scrollBar))
    {
      this.PART_VerticalScrollBar = scrollBar;
    }
  }

  protected override Size ArrangeOverride(Size arrangeBounds)
  {
    arrangeBounds = base.ArrangeOverride(arrangeBounds);
    HandleLastColumnFill(arrangeBounds.Width);

    return arrangeBounds;
  }

  protected virtual void OnLastChildFillChanged(bool oldValue, bool newValue)
  {
    if (newValue)
    {
      // Backup original horizontalScrollBarVisibility
      this.HorizontalScrollBarVisibilityInternal = this.HorizontalScrollBarVisibility;
    }
    else
    {
      // Restore original horizontalScrollBarVisibility
      SetCurrentValue(HorizontalScrollBarVisibilityProperty, this.HorizontalScrollBarVisibilityInternal);
    }
  }

  private void HandleLastColumnFill(double availableWidth)
  {
    if (!this.LastChildFill
      || !this.IsLoaded
      || !this.HasItems)
    {
      return;
    }

    ScrollBarVisibility horizontalScrollBarVisibility = TryMakeLastColumnFill(availableWidth)
      ? ScrollBarVisibility.Disabled
      : ScrollBarVisibility.Visible;
    SetCurrentValue(HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility);
  }

  private bool TryMakeLastColumnFill(double availableWidth)
  {
    double totalPrecedingColumnWidth = this.Columns.SkipLast(1).Sum(column => column.ActualWidth);
    double verticalScrollBarWidth = this.PART_VerticalScrollBar?.Width ?? 0;
    DataGridColumn lastColumn = this.Columns.Last();
    double maxCellContentWidth = this.Items
      .Cast<object>()
      .Max(item => lastColumn.GetCellContent(item).DesiredSize.Width);
    double desiredLastColumnWidth = maxCellContentWidth + verticalScrollBarWidth;
    double lastColumnAvailableWidth = availableWidth - (totalPrecedingColumnWidth);
    bool canLastColumnFill = lastColumnAvailableWidth > desiredLastColumnWidth;
    DataGridLength defaultColumnWidth = this.ColumnWidth;
    if (canLastColumnFill)
    {
      lastColumn.Width = new DataGridLength(lastColumnAvailableWidth, DataGridLengthUnitType.Pixel);
    }
    else
    {
      lastColumn.Width = defaultColumnWidth;
    }

    return canLastColumnFill;
  }

  private static bool TryFindVisualChildElementByName<TChild>(DependencyObject parent,
  string childElementName,
  out TChild resultElement) where TChild : FrameworkElement
  {
    resultElement = null;
    if (parent is Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
    {
      DependencyObject child = VisualTreeHelper.GetChild(parent, i);
      if (child is FrameworkElement frameworkElement
        && frameworkElement.Name.Equals(childElementName, StringComparison.OrdinalIgnoreCase))
      {
        resultElement = frameworkElement as TChild;
        return true;
      }

      if (child.TryFindVisualChildElementByName<TChild>(childElementName, out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

Another (better) solution that avoids the calculations is to make use of the native resize behavior that can be configured by setting the DataGrid.ColumnWidth property:

The easiest is to configure the DataGrid to distribute the available space evenly amongst all columns. You do this by setting DataGrid.ColumnWidth property to *.

If you only want to make the last column to fill the remaining space, you must set the width of all columns except the last to an explicit value or Auto while DataGrid.ColumnWidth is set to *.

To prevent the columns from shrinking you have to set the DataGridColumn.MinWidth property. This also guarateens that a horizontal scroll bar will appear.
And if you don't want to distribute the available space evenly between all columns you must also set the DataGridCOlumn.Width property.

For example, to make only the last column fill the remaining space you must set DataGrid.ColumnWidth to *, and all columns except the last must have an explicit DataGridColumn.Width (e.g. Auto) and all columns, including the last, must have their DataGridColumn.MinWidth set. If DataGridColumn.Width is set to Auto then the DataGrid will calculate the size based on the bigger width of header and cell.

The key is that when DataGrid.ColumnWidth is set to * or e.g. DataGridLength.SizeToCells then the DataGrid will automatically recalculate the layout if the condition has changed.
Setting the width locally on the DataGridColumn results in a single calculation, the moment the DataGridCell is created.

Available space is distributed evenly amongst all columns:

<DataGrid ColumnWidth="*" />

Only particular columns will share the available space. In the following example, only the last column will fill the remaining space:

<DataGrid ColumnWidth="*"
          AutoGenerateColumns="False">
  <DataGrid.Columns>
    <DataGridTextColumn Header="Non stretching column"
                        Width="Auto"
                        MinWidth="100" />
    <DataGridTextColumn Header="Filling column" 
                        MinWidth="100" />
  </DataGrid.Columns>
</DataGrid>