Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get WPF Grid columns defined with star to clip content?

Tags:

c#

wpf

xaml

I have a Grid control that is proportioned using star e.g.

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="50*" />
    <ColumnDefinition Width="100*" />
    <ColumnDefinition Width="50*" />
</Grid.ColumnDefinitions>

However putting a long TextBlock in the grid that overflows causes the proportions to be upset. e.g.

<TextBlock Text="Foo" Grid.Column="0" />
<TextBlock Text="Some long text here which overflows" Grid.Column="1" />
<TextBlock Text="Foo" Grid.Column="2" />

This causes the central column to be more than double the other two. How do I maintain the specified proportions? Is it possible to clip the content?

I have set TextTrimming="CharacterEllipsis" on the TextBlocks but no luck.

Edit

Crucially it seems, the Grid is inside a DataTemplate, paste the following to observe the behaviour,

<!-- FallbackValue is just a quick hack to get some rows to show at design-time -->
<ListBox ItemsSource="{Binding Foo, FallbackValue=1234}"
             HorizontalContentAlignment="Stretch">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="50*" />
                        <ColumnDefinition Width="100*" />
                        <ColumnDefinition Width="50*" />
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Foo" Grid.Column="0" />
                    <TextBlock Text="Some long text here which overflows" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                    <TextBlock Text="Foo"  Grid.Column="2" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

The reason why this is important is that I have another Grid as a sibling of the ListBox which displays the 'headers' for the columns shown in the ListBox as follows,

<Grid>
    ... Headers and column definitions here
</Grid>

<ListBox ...>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                ... Matching column definitions here
            </Grid>
        </DateTemplate>
    </ListBox.ItemTemplate>
</ListBox>

and so it is important that the columns match up.

I have tried to bind the ColumnDefinitions inside the DataTemplate to the external Grid ColumnDefinitions but I cannot get easily a binding reference to it.

like image 340
Brendan Avatar asked Aug 29 '14 11:08

Brendan


2 Answers

This is one of the most annoying problems with WPF. Since the available space yielded to the templated grid is infinite, the actual content will take as much space as it wants.

The simplest way is to fix a certain width to the Grid, but that solves only the situations where there's no resizing.

Whereas you want to stretch the ListBox size (width, in the specific), unfortunately I guess that there's no any better solution other than a custom converter.

Here is my solution:

<Window.Resources>
    <local:MyConv x:Key="cv1" />
</Window.Resources>
<Grid>
    <ListBox 
        ItemsSource="{Binding Foo, FallbackValue=1234}"
        HorizontalContentAlignment="Stretch"
        >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBox}, Converter={StaticResource cv1}}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="50*" />
                        <ColumnDefinition Width="100*" />
                        <ColumnDefinition Width="50*" />
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Foo" Grid.Column="0" />
                    <TextBlock Text="Some long text here which overflows" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                    <TextBlock Text="Foo"  Grid.Column="2" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

And the converter:

class MyConv : IValueConverter
{
    public object Convert(
        object value, 
        Type targetType, 
        object parameter, 
        System.Globalization.CultureInfo culture
        )
    {
        return (double)value - 30.0;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
like image 72
Mario Vernari Avatar answered Nov 15 '22 00:11

Mario Vernari


Even though this is an old post I'm adding my findings as they might be relevant for other people reading this post. I had a similar issue (my * columns weren't dividing the width evenly as expected anymore, they were just sizing based on the content). The root cause here was that I had a ListView with an ItemsSource linked to a List. The ListView in WPF contains a ScrollViewer and a ScrollViewer doesn't have a fixed width. Without a fixed width a Grid can't properly determine what width to give to a * column and switches to a different sizing method.

Solution I now use an ItemsControl which doesn't contain a ScrollViewer and thus the Width is known allowing the Grid to properly size it's columns.

For more details on how exactly the Grid handles it's sizing I suggest you decompile the Grid class and have a look at the following method:

protected override Size MeasureOverride(Size constraint) 

This is my MainWindow.xaml from my test application (comment out the ListView to see the difference in behaviour):

 <Window x:Class="WPFSO.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:wpfso="clr-namespace:WPFSO"        
            Title="MainWindow" Height="150" Width="525">
        <Window.DataContext>
            <wpfso:SharedSizeScopeViewModel />
        </Window.DataContext>
        <Window.Resources>
            <DataTemplate DataType="{x:Type wpfso:TestViewModel}">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" x:Name="SecondColumn" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" x:Name="FourthColumn" />
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Column="0" Text="{Binding Name}" />
                    <TextBlock Grid.Column="1" Background="LightGray" Text="{Binding Name2}"/>                
                    <TextBlock Grid.Column="2" Text="{Binding Name3}"/>
                    <TextBlock Grid.Column="3" Background="Orange" Text="{Binding Name4}"/>

                    <!--<TextBlock Grid.Column="1" Background="Blue" HorizontalAlignment="Stretch" />
                    <TextBlock Grid.Column="3" Background="Orange" HorizontalAlignment="Stretch" />-->
                </Grid>
            </DataTemplate>

            <DataTemplate x:Key="MainDataTemplate" DataType="wpfso:SharedSizeScopeViewModel" >
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <CheckBox Grid.Row="0" Grid.ColumnSpan="4" HorizontalAlignment="Left" FlowDirection="RightToLeft" Margin="0,0,0,25">
                        <TextBlock FlowDirection="LeftToRight" Text="Show differences" Style="{StaticResource LabelStyle}" />
                    </CheckBox>

                    <TextBlock Grid.Row="1" Grid.Column="0" Text="PropertyName" Style="{StaticResource LabelStyle}" />
                    <TextBlock Grid.Row="1" Grid.Column="1" Text="Previous value" Style="{StaticResource LabelStyle}" />
                    <TextBlock Grid.Row="1" Grid.Column="3" Text="Current value" Style="{StaticResource LabelStyle}" />

                    <ListView Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4"  ItemsSource="{Binding Entries}" HorizontalAlignment="Stretch" Margin="0" HorizontalContentAlignment="Stretch"/>
                </Grid>
            </DataTemplate>
        </Window.Resources>
        <Grid Name="RootGrid">

           <ItemsControl ItemsSource="{Binding Entries}" />
           <!--<ListView ItemsSource="{Binding Entries}" />-->

        </Grid>
    </Window>

The ViewModels used during this test:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WPFSO
{
    public class SharedSizeScopeViewModel : INotifyPropertyChanged
    {

        public SharedSizeScopeViewModel()
        {
            var testEntries = new ObservableCollection<TestViewModel>();

            testEntries.Add(new TestViewModel
            {
                Name = "Test",
                Name2 = "Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong test",
                Name3 = "Short test",
                Name4 = "Nothing"


            });

            Entries = testEntries;        
        }

        private ObservableCollection<TestViewModel> _entries;

        public ObservableCollection<TestViewModel> Entries
        {
            get { return _entries; }
            set
            {
                _entries = value; 
                OnPropertyChanged();
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

First viewmodel

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WPFSO
{
    public class SharedSizeScopeViewModel : INotifyPropertyChanged
    {

        public SharedSizeScopeViewModel()
        {
            var testEntries = new ObservableCollection<TestViewModel>();

            testEntries.Add(new TestViewModel
            {
                Name = "Test",
                Name2 = "Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong test",
                Name3 = "Short test",
                Name4 = "Nothing"


            });

            Entries = testEntries;        
        }

        private ObservableCollection<TestViewModel> _entries;

        public ObservableCollection<TestViewModel> Entries
        {
            get { return _entries; }
            set
            {
                _entries = value; 
                OnPropertyChanged();
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Second viewmodel

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WPFSO
{
    public class TestViewModel : INotifyPropertyChanged
    {    
        private string _name;
        private string _name2;
        private string _name3;
        private string _name4;

        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged();
            }
        }

        public string Name2
        {
            get { return _name2; }
            set
            {
                _name2 = value;
                OnPropertyChanged();
            }
        }

        public string Name3
        {
            get { return _name3; }
            set
            {
                _name3 = value;
                OnPropertyChanged();
            }
        }

        public string Name4
        {
            get { return _name4; }
            set
            {
                _name4 = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
like image 26
Starceaker Avatar answered Nov 15 '22 00:11

Starceaker