2
votes

I have a UserControl with following code (simplified to make it readable):

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" />
    <TextBlock Grid.Row="1" />
    <DataGrid Grid.Row="2" />
    <TextBlock Grid.Row="3" />
</Grid>

Now I want all of the controls to be displayed in a stack, but limited to the size of the window.

The problem is that when I have a lot of data in the DataGrid, it grows beyond the borders and the last TextBlock is not visible - nor the DataGrid scrollbar appears.

When I set third row definition for Star (*), the size of the DataGrid is fine for big amount of items, but in case there are only few items in DataGrid, the last TextBlock appears on the bottom of the screen (not directly after the DataGrid as needed).

When instead of Grid I user StackPanel, it looks the same as in the code above. If I user DockPanel, DataGrid is properly scrollable, but the last TextBlock is not visible at all.

I would imagine the solution to define third row as Height="Auto" and MaxHeight="*", but that's obviously not possible.

Could you help?

3
I am not so sure I understand what you want to achieve. Do you want: 1. Always display 4 controls. 2. Make the last TextBlock exactly at the bottom of DataGrid, but not necessarily at the bottom of the parent grid?Thế Long

3 Answers

1
votes

You will need to do this programmatically, not in xaml. This is because you want it to do two different things:

  1. Keep the last TextBlock close to the DataGrid if there are only a few items.
  2. Keep the last TextBlock visible if the DataGrid has a large amount of items.

Doing this will require you to hook into events in the code-behind, determine whether or not the last TextBlock disappears, then adjust the Height="Auto" or Height="*" accordingly on the RowDefinition, then UpdateLayout.

Here's a sample project. I replaced your DataGrid with a TextBlock for simplicity.

XAML:

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Make Grid.Row=2 Long, But Keep Text 3 Visible" Click="Button_Click" HorizontalAlignment="Center" Margin="5" Padding="5,10"/>
        <Grid Grid.Row="1" x:Name="myGrid">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition x:Name="myRowDefinition" Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Text="This is Text 1" Background="Red"/>
            <TextBlock Grid.Row="1" Text="This is Text 2" Background="Green"/>
            <TextBlock Grid.Row="2" x:Name="myDataGrid" FontSize="64" Text="{Binding Output}" TextWrapping="Wrap" Background="Blue"/>
            <TextBlock Grid.Row="3" x:Name="lastTextBlock" Text="This is Text 3" Background="Violet"/>
        </Grid>
    </Grid>

Code-behind:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string output;

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += OnLoaded;
            this.DataContext = this;
        }

        /// <summary>
        /// Handles the SizeChanged event of your data grid.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MyDataGrid_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (!IsUserVisible(lastTextBlock, this))
            {
                if (this.myRowDefinition.Height == GridLength.Auto)
                {
                    // Edit the row definition and redraw it
                    this.myRowDefinition.Height = new GridLength(1, GridUnitType.Star);
                }
            }
            else
            {
                if (this.myRowDefinition.Height != GridLength.Auto && CanDataGridBeSmaller(this.myRowDefinition.ActualHeight))
                {
                    // If the datagrid can be smaller, change the row definition back to Auto
                    this.myRowDefinition.Height = GridLength.Auto;
                }
            }
            this.UpdateLayout();
        }

        /// <summary>
        /// It is possible for the DataGrid to take on more space than it actually needs.  This can happen if you are messing with the window resizing.
        /// So always check to make sure that if you can make the DataGrid smaller, that it stays small.
        /// </summary>
        /// <param name="actualHeight">the actual height of the DataGrid's row definition</param>
        /// <returns></returns>
        private bool CanDataGridBeSmaller(double actualHeight)
        {
            // Create a dummy control that is equivalent to your datagrid (for my purposes, I used a Textblock for simplicity, so I will recreate it fully here.
            TextBlock dummy = new TextBlock() { TextWrapping = TextWrapping.Wrap, FontSize = 64, Text = this.Output };
            dummy.Measure(new Size(this.myGrid.ActualWidth, this.myGrid.ActualHeight));

            // Get the dummy height and compare it to the actual height
            if (dummy.DesiredSize.Height < myRowDefinition.ActualHeight)
                return true;
            return false;
        }

        /// <summary>
        /// This method determines if the control is fully visible to the user or not.
        /// </summary>
        private bool IsUserVisible(FrameworkElement element, FrameworkElement container)
        {
            if (!element.IsVisible)
                return false;

            Rect bounds = element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
            Rect rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
            return rect.Contains(bounds);
        }

        /// <summary>
        /// This is purely for setup.
        /// </summary>
        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            this.myDataGrid.SizeChanged += MyDataGrid_SizeChanged;
            this.Output = "This row is short, so Text 3 below me should be flush with my bottom.";
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public string Output { get => this.output; set { this.output = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Output))); } }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Output = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
        }
    }

Example Output when you start:

enter image description here

Example Output after you click the button at the top:

enter image description here

1
votes

@Tam Bui --- Sorry for not replying in the comment section, but I ran out of characters :/

Your solution works, thank you. But in case of large amount of data in DataGrid it seems to not be efficient - looks like it's loading all rows at once (like for Auto setting).

Based on your solution, I came up with more efficient and simpler one:

private void OnSizeChanged(object sender, RoutedEventArgs e)
{
    if (!IsLoaded) return;

    AdjustGridSize();
}

private void AdjustGridSize()
{
    GridRowDefinition.Height = new GridLength(1, GridUnitType.Star);
    UpdateLayout();

    ExpensesGrid.MaxHeight = GridRowDefinition.ActualHeight;
    GridRowDefinition.Height = GridLength.Auto;
}

GridRowDefinition is a definition of row in which DataGrid sits and ExpensesGrid is my DataGrid.

Also, there should also be AdjustGridSize method called in Loaded event invocation to initially adjust the size.

Let me know if you see any disadvantages of this solution.

0
votes

I suggest you use a DockPanel where the DataGrid is the filling last child. Set the DockPanel.MaxHeight to the parent ActualHeight as a constraint, but don't set the Height, then when the list has few items, the whole DockPanel will shrink.

Full example:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <!-- TEST data for demonstration -->
        <XmlDataProvider x:Key="MockList" XPath="/MockObjects/*" >
            <x:XData >
                <MockObjects xmlns="">
                    <MockObject Name="Test 1" />
                    <MockObject Name="Test 2" />
                    <MockObject Name="Test 3" />
                    <MockObject Name="Test 4" />
                    <MockObject Name="Test 5" />
                    <MockObject Name="Test 6" />
                    <MockObject Name="Test 7" />
                    <MockObject Name="Test 8" />
                    <MockObject Name="Test 9" />
                </MockObjects>
            </x:XData>
        </XmlDataProvider>
    </Window.Resources>
    <!-- Stretching Parent-->
    <Grid Name="parentGrid">
        <DockPanel Width="200" MaxHeight="{Binding ElementName=parentGrid,Path=ActualHeight}" VerticalAlignment="Top" HorizontalAlignment="Left">
            <TextBlock DockPanel.Dock="Top">Test</TextBlock>
            <TextBlock DockPanel.Dock="Top">Test</TextBlock>
            <!-- Notice change of order here -->
            <TextBlock DockPanel.Dock="Bottom" Background="LightBlue">Test</TextBlock>
            
            <DataGrid ItemsSource="{Binding Source={StaticResource MockList}, XPath=/MockObjects/MockObject}" AutoGenerateColumns="False">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Text" Binding="{Binding XPath=@Name}"/>
                </DataGrid.Columns>
            </DataGrid>
        </DockPanel>
    </Grid>
</Window>