views:

1285

answers:

4

Im using the wpf datagrid and am looking for a way to set the height on all of the rows when the user adjusts one of them. I know the datagrid has a RowHeight property that sets all of the row heights at once, but the how of catching an individual row height changed escapes me

+3  A: 

There aren't any events that could be use directly for that. What you could do is use another event that is fired when you resize the rows and other things. The event I'm thinking of right now is PreviewMouseUp, which is release when you release the mouse button anywhere if you datagrid.

What you could do is when the event is fired, you could check the row height of all of your rows and find the one that is different, then update all rows with it.

David Brunelle
+1  A: 

As far as I know there is no event that is raised when you resize a row's height.

My first suggestion would be to set the RowStyle in order to create a binding (OneWay) between the the DataGridRow's height property and the datagrid's RowHeight property, but if you check the Row's height after you resize it, it is unchanged, the ActualHeight is the property that contains the row's "actual" height when you resize it, and ActualHeight cannot be set because "it does not have an accessible set accessor".

After trying this I thought: Where does DataGridRow's ActualHeight gets its value from?

I remembered this post that explains how to detect which cell and row got clicked and also shows the DataGrid's default template visual tree.

By trial and error (using the visual tree image in the link above) I found that it was DataGridCellPresenter that stored the Height that was being used (actually I'm not 100% sure about this, it was the first class that had the height changed up the visual tree since DataGridCell).

Apparently DataGrid doesn't expose the API to get the DataGridCellsPresenter from a DataGridRow (as I found out here)

So my first approach was to get all the DataGridCellPresenter in the DataGrid (through the visual tree) after it has been populated and programatically create a binding between the Height property of the DataGridPresenter and the RowHeight property of the DataGrid.

Here's the code to do that (my DataGrid's instance name is dataGrid1):

Getting all the DataGridCellPresenter:

    void GetAllDataGridCellPresenters(DependencyObject parent, List<DataGridCellsPresenter> presenters) 
    {
        int numberOfChildren = VisualTreeHelper.GetChildrenCount(parent);         
        for (int i = 0; i < numberOfChildren; i++)
        {
            if (VisualTreeHelper.GetChild(parent, i) is DataGridCellsPresenter)
            {
                presenters.Add(VisualTreeHelper.GetChild(parent, i) as DataGridCellsPresenter);
            }
            else if (VisualTreeHelper.GetChild(parent, i) != null)
            {
                GetAllDataGridCellPresenters(VisualTreeHelper.GetChild(parent, i), presenters);
            }
            else
                return;
        }
    }

Setting the bindings programatically on all of them (call this when the Loaded event is raised by the DataGrid):

    void SetBindingInDataGridPresenter()
    {
        List<DataGridCellsPresenter> presenters = new List<DataGridCellsPresenter>();
        GetAllDataGridCellPresenters(dataGrid1, presenters);
        foreach (DataGridCellsPresenter presenter in presenters)
        {    
            Binding binding = new Binding("RowHeight");
            binding.Source = dataGrid1;
            binding.Mode = BindingMode.TwoWay;
            presenter.SetBinding(DataGridCellsPresenter.HeightProperty, binding);
        }
    }

(Note: Setting the binding as OneWayToSource didn't work, I really don't know why, I'm probably missing something obvious here...)

This did work... sort of... because I used the Visual Tree to get the DataGridCellsPresenter I only got the visible ones :P, but this shows it can be done this way.

So, finally, the right way to do it would be to supply the DataGrid control template, it can be just as the default one except with the DataGridCellsPresenter's Height property data bound to the RowHeight property of the DataGrid.

I know this does not show exactly how to do it, but you just have to learn (so do I :P) to redefine a control's template; somehow get the default DataGrid template (or if you're already using another then great, you probably know more than me about it and already know how to do it in order to get the DataGridCellsPresenter Height property automatically bound to the RowHeight DataGrid property) and change it with that bit of magic that gets both height properties bound.

Rui
A: 

I arrived on this by trial and error, so long ass you are using an ItemsSource data source it should work fine. It should work with virtual rows and causes only a brief visual pause and it switches over (this seems mainly down to column autogeneration so can be avoided).

As hacks go it has the advantage of simplicity and the use of mechanics which are not expected to change.

The heuristic on user triggering of the action might be improved but it has not failed on me yet.

using Microsoft.Windows.Controls;
using Microsoft.Windows.Controls.Primitives;

public static class DataGridExtensions
{
    public static void LinkRowHeightsToUserChange(this DataGrid dataGrid)
    {
        double? heightToApply = null;
        bool userTriggered = false;

        if (dataGrid.RowHeaderStyle == null)
            dataGrid.RowHeaderStyle = new Style(typeof(DataGridRowHeader));
        if (dataGrid.RowStyle == null)
            dataGrid.RowStyle = new Style(typeof(DataGridRow));

        dataGrid.RowStyle.Setters.Add(new EventSetter()
        {
            Event = DataGridRow.SizeChangedEvent,
            Handler = new SizeChangedEventHandler((r, sizeArgs) =>
            {
                if (userTriggered && sizeArgs.HeightChanged)
                        heightToApply = sizeArgs.NewSize.Height;
            })
        });
        dataGrid.RowHeaderStyle.Setters.Add(new EventSetter()
        {
            Event = DataGridRowHeader.PreviewMouseDownEvent,
            Handler = new MouseButtonEventHandler(
                (rh,e) => userTriggered = true)
        });
        dataGrid.RowHeaderStyle.Setters.Add(new EventSetter()
        {
            Event = DataGridRowHeader.MouseLeaveEvent,
            Handler = new MouseEventHandler((o, mouseArgs) =>
            {
                if (heightToApply.HasValue)
                {
                    userTriggered = false;
                    var itemsSource = dataGrid.ItemsSource;
                    dataGrid.ItemsSource = null;
                    dataGrid.RowHeight = heightToApply.Value;
                    dataGrid.ItemsSource = itemsSource;
                    heightToApply = null;
                }
            })
        });
    }
ShuggyCoUk
this works, although it is very slow when virtualization is on.
Aran Mulholland
sorry, meant when column virtualization is off.., if we turned it on, we couldnt scroll horizontally, but the rows did resize nicely.
Aran Mulholland
Yeah, I like that you only pay for the feature significantly when someone actually resizes but it is costly.You can probably cache the columns in some way, drop the source and recreate it without regenerating the columns.
ShuggyCoUk
we removed the lines that set the itemsSource to null and then sets it again. we found these lines could give us issues if the row heights were being adjusted and someone double clicked one of the cells and there was no items source. It also speeds it up significantly. do you remember the rationale behind this?
Aran Mulholland
@Aran I'm afraid I can't remember :( teach me not to comment weird code. If it works without then there's a fair chance it's solid.
ShuggyCoUk
+1  A: 

@Aran

do you remember the rationale behind this?

I can tell you: If you remove both lines to unset and reset the items source (which indeed slows the whole process quite a bit), the row you resize will have its height definitively set.

It seems when you resize a row, you change its Height directly, and this overrides any value you set to the dataGrid's RowHeight property for this row in particular. So basically, here is what you can get :

dataGrid's RowHeight = 20

you change the Height of one Row (say the 5th) to 30 => this row's Height is set to 30 and the dataGrid's RowHeight is set to 30. Everything looking good so far.

now, change another row's Height back to 20 (say the 2nd row). you Set this row's Height to 20 and the DataGrid'RowHeight to 20, which puts all the other rows to 20, EXCEPT the 5th row which stays at 30. (because it had been forced to this value before)

emptying the source and resetting it forces each row to be reloaded and take the dataGrid's RowHeight into account, which eliminates the problem.

David
funny, that is exactly the behaviour we have, i keep meaning to have a look at it but haven't got around to it, thanks.
Aran Mulholland