30
votes

Standard WPF 4 Datagrid.

Let' say I have datagrid 200 pixels wide, and 2 columns. I would like the columns take always entire space, meaning if the user resizes the first column to 50 pixels, the last one would be 150.

Initially I set width 100 pixels for the 1st column, and * for the last one (in XAML).

I thought the problem is with removing the virtual, 3rd column, as explained here:

http://wpf.codeplex.com/Thread/View.aspx?ThreadId=58939

but there is no difference really -- still, when resizing the columns, I get some extra space on right -- with virtual column, it is a virtual column (white color by default), without it, it is empty space (gray by default).

QUESTION: how to enforce the constraint, that no matter how the user resizes the columns,

sum(columns width)==datagrid width

?

Edits

Yes, I use WPF 4.

WORKAROUND

I marked one of the answers as solution, but actually it is not a solution due to WPF design. It is simply what WPF can do at best, and it is not very good -- first of all the option CanUserResize for column means really IsResizeable and this option when turned on contradicts Width set to *. So without some really smart trick you end up with:

  • datagrid which last column in superficially resizable but in fact it is not, and little space on right is shown (i.e. the virtual column is not resizable) -- for last column: CanUserResize=true, Width=*

  • datagrid which last column cannot be resized by user and it is shown accordingly, initially no space on right is shown, but it can be shown when user resizes any element of datagrid -- for last column: CanUserResize=false, Width=*

So far I can see two problems with WPF datagrid:

  • misleading naming
  • contradiction of features

I am still all ears to how really solve this issue.

7

7 Answers

22
votes

Set the width for the data grid to "Auto". You're allowing the columns to resize correctly within the grid itself, but you've hard-wired the width to 200.

UPDATE: Base on @micas's comment, I may have misread. If that's the case, try using 100 for the left column's width and 100* for the right column (note the asterisk). This will default the width for the right column to 100 but allow it to resize to fill the grid.

8
votes

You can set a column width to star on code. In your constructor, add:

    Loaded += (s, e) => dataGrid1.Columns[3].Width =
new DataGridLength(1, DataGridLengthUnitType.Star);
4
votes

I've just implemented this as an attached behavior. The problem is when you set the DataGrid's last column to *, it does resize to fit, but then all the auto-fitting of the other cells messes up. To resolve this, the attached behaviour does a manual auto-fit of other (non last) cells.

This also works when resizing the other columns - once loaded, you can resize and the last column will always fill. Note this behavior works once on the Loaded event

// Behavior usage: <DataGrid DataGridExtensions.LastColumnFill="True"/>
public class DataGridExtensions
{
    public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));

    public static void SetLastColumnFill(DataGrid element, bool value)
    {
        element.SetValue(LastColumnFillProperty, value);
    }

    public static bool GetLastColumnFill(DataGrid element)
    {
        return (bool)element.GetValue(LastColumnFillProperty);
    }

    private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGrid = d as DataGrid;
        if (dataGrid == null) return;

        dataGrid.Loaded -= OnDataGridLoaded;
        dataGrid.Loaded += OnDataGridLoaded;
    }        

    private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        var lastColumn = dataGrid.Columns.LastOrDefault();
        if(lastColumn != null)
            lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);

        // Autofit all other columns
        foreach (var column in dataGrid.Columns)
        {
            if (column == lastColumn) break;

            double beforeWidth = column.ActualWidth;
            column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
            double sizeCellsWidth = column.ActualWidth;
            column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
            double sizeHeaderWidth = column.ActualWidth;
            column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
        }
    }
}
0
votes

Be forewarned: It's a hack....

I registered to the "AutoGeneratedColumns" event in the "OnLastColumnFillChanged" method of Dr. ABT's class and copied the Loaded method into it, and it works. I haven't really thoroughly tested it yet, so YMMV.

My change:

    private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGrid = d as DataGrid;
        if (dataGrid == null) return;

        dataGrid.Loaded -= OnDataGridLoaded;
        dataGrid.Loaded += OnDataGridLoaded;

        dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
        dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
    }

    private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        var lastColumn = dataGrid.Columns.LastOrDefault();
        if (lastColumn != null)
            lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);

        // Autofit all other columns
        foreach (var column in dataGrid.Columns)
        {
            if (column == lastColumn) break;

            double beforeWidth = column.ActualWidth;
            column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
            double sizeCellsWidth = column.ActualWidth;
            column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
            double sizeHeaderWidth = column.ActualWidth;
            column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
        }
    }

Oh, and don't forget to add the namespace to the XAML declaration! :)

Up top:

xmlns:ext="clr-namespace:TestProject.Extensions"

And then in the DataGrid declaration:

ext:DataGridExtensions.LastColumnFill="True"

Update: I said that the mileage would vary! Mine certainly did.

That whole "autofit columns" bit caused some of my columns in a DataGrid with a variable number of columns to only be as wide as the column header. I removed that portion, and now it seems to be working on all of the DataGrids in the application.

Now I have:

    private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        UpdateColumnWidths(dataGrid);

    }

    private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        UpdateColumnWidths(dataGrid);
    }

    private static void UpdateColumnWidths(DataGrid dataGrid)
    {
        var lastColumn = dataGrid.Columns.LastOrDefault();
        if (lastColumn == null) return;

        lastColumn.Width = new DataGridLength(1.0d, DataGridLengthUnitType.Star);

    }
0
votes

Here's a very simple answer, all performed in code behind. :-) All columns will be auto-sized; the final column will fill all remaining space.

// build your datasource, e.g. perhaps you have a:
List<Person> people = ...

// create your grid and set the datasource
var dataGrid = new DataGrid();
dataGrid.ItemsSource = people;

// set AutoGenerateColumns to false and manually define your columns
// this is the price for using my solution :-)
dataGrid.AutoGenerateColumns = false;

// example of creating the columns manually.
// there are obviously more clever ways to do this 
var col0 = new DataGridTextColumn();
col0.Binding = new Binding("LastName");
var col1 = new DataGridTextColumn();
col1.Binding = new Binding("FirstName");
var col2 = new DataGridTextColumn();
col2.Binding = new Binding("MiddleName");
dataGrid.Columns.Add(col0);
dataGrid.Columns.Add(col1);
dataGrid.Columns.Add(col2);

// Set the width to * for the last column
col2.Width = new DataGridLength(1, DataGridLengthUnitType.Star);

0
votes

I might be a bit late, but you can try my code from this question. I extended original grid and added method for the last column stretching:

private void StretchLastColumnToTheBorder()
{
    if (ViewPortWidth.HasValue)
    {
        var widthSum = 0d;
        for (int i = 0; i < Columns.Count; i++)
        {
            if (i == Columns.Count - 1 && ViewPortWidth > widthSum + Columns[i].MinWidth)
            {
                var newWidth = Math.Floor(ViewPortWidth.Value - widthSum);
                Columns[i].Width = new DataGridLength(newWidth, DataGridLengthUnitType.Pixel);
                return;
            }
            widthSum += Columns[i].ActualWidth;
        }
    }
}

where ViewPortWidth is:

public double? ViewPortWidth 
{ 
    get 
    {
        return FindChild<DataGridColumnHeadersPresenter>(this, "PART_ColumnHeadersPresenter")?.ActualWidth;
    } 
}

So, you have to find the visual child (answer from here) of type DataGridColumnHeadersPresenter, which has the width of the viewport and calculate the width of the last column. To do it automatically, you can fire this method on LayoutUpdated event. Additionally, you can add a DependencyProperty, indicating, whether automatical stretching of the last column should be performed.

0
votes

Based on the update pennyrave gave to DR.ABT's answer I made a further update to get this to work better. It's still a hack, but it seems to work better than either of their answers when I'm constantly updating the DataGrid's ItemsSource property. If I try to use a star or auto width anywhere, WPF insists that all the columns are only 20 pixels wide, so I hardcode them all based off the auto values they were set to.

I've added an invoke to the AutoGeneratedColumns event to make it delay a bit. Without this delay, all the columns insist that they are only 20 pixels wide. They still sometimes do, but I've got a check for that and it seems to work, (but the columns are rendered wrong, then corrected a millisecond later.)

Ideally we'd apply the column sizes after WPF figures out what the automatic sizes would be, and before the DataGrid is rendered, but I can't find any way to get my code to run there. It's either too early or too late.

public class DataGridExtensions
{
    public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));

    public static void SetLastColumnFill(DataGrid element, bool value)
    {
        element.SetValue(LastColumnFillProperty, value);
    }

    public static bool GetLastColumnFill(DataGrid element)
    {
        return (bool)element.GetValue(LastColumnFillProperty);
    }

    private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGrid = d as DataGrid;
        if (dataGrid == null) return;

        dataGrid.Loaded -= OnDataGridLoaded;
        dataGrid.Loaded += OnDataGridLoaded;

        dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
        dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
    }

    private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => afterInvoke(dataGrid)));
    }

    private static void afterInvoke(DataGrid dataGrid)
    {
        bool nonMin = false;
        foreach (var col in dataGrid.Columns)
        {
            if (col.ActualWidth != col.MinWidth)
            {
                nonMin = true;
            }
        }
        if(nonMin)
        {
            OnDataGridLoaded(dataGrid, null);
        }
    }

    public static void OnDataGridLoaded(object sender, RoutedEventArgs e)
    {
        var dataGrid = sender as DataGrid;
        if (dataGrid == null) return;

        // set size of columns
        double sizeSoFar = 0;
        for(int i =0; i < dataGrid.Columns.Count; i++)
        {
            var column = dataGrid.Columns[i];

            //if last column
            if (i == dataGrid.Columns.Count-1)
            {
                sizeSoFar = dataGrid.ActualWidth - sizeSoFar - 2;//2 pixels of padding
                if(column.ActualWidth != sizeSoFar)
                {
                    column.MinWidth = sizeSoFar;
                    column.Width = new DataGridLength(sizeSoFar);
                }
            }
            else //not last column
            {
                double beforeWidth = column.ActualWidth;
                column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
                double sizeCellsWidth = column.ActualWidth;
                column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
                double sizeHeaderWidth = column.ActualWidth;
                column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));

                sizeSoFar += column.MinWidth; //2 pixels of padding and 1 of border
            }
        }
    }
}

Remember to add something like xmlns:Util="clr-namespace:MyProject.Util" to your window tag at the top of your xaml and then you can use Util:DataGridExtensions.LastColumnFill="True" in your DataGrid tag.