Monday, March 03, 2008

WPF: Adventures in Formatting

Problem: I needed to have a really long string text in a bound WPF ListView column wrap instead of running on until the end of the text.

Solution: After doing quite a bit of searching around I was unable to find a good concrete example that worked in my particular case. The closest example I found looked something like this:


<ListView Height="Auto" Name="lsvErrorSummary2" ScrollViewer.CanContentScroll="False">

<ListView.View>

<GridView>

<GridViewColumn Header="Message" Width="200" DisplayMemberBinding="{Binding Path=Property}">

<GridViewColumn.CellTemplate>

<DataTemplate>

<TextBlock TextWrapping="Wrap" Text="{Binding}" />

</DataTemplate>

</GridViewColumn.CellTemplate>

</GridViewColumn>

</GridView>

</ListView.View>

</ListView>


Maddeningly enough this doesn't work as I spent at least an hour trying to find some magical combination of properties that would get it to work. The reason this doesn't work seems to be the fact that when we use the "DisplayMemberBinding" to bind the column WPF is using the binding for both the data and how to display the data, or at the very least WPF is just using the default for displaying the bound data. That being said I was able to solve it by making some changes.

The first thing I did was to remove the DataTemplate out into a Resources area (Window.Resources would work) and make sure to give it an x:Key="" attribute. After doing that I pulled out the GridViewColumn.CellTemplate tags as well. All that's left to do is to set the Text attribute of your DataTemplate's TextBlock equal to your real binding property (i.e. Text="{Binding Path=Message}" and point the CellTemplate attribute to the DataTemplate's x:Key (i.e. CellTemplate="{StaticResource CustomListViewItems}"). The end result should look similar to below:


<Window.Resources>
<DataTemplate x:Key="CustomListViewItems">
<TextBlock TextWrapping="Wrap" Text="{Binding Path=Message}" />
</DataTemplate>
</Window.Resources>

<ListView Height="Auto" Name="lsvErrorSummary" Width="511" ScrollViewer.CanContentScroll="False">
<ListView.View>
<GridView>
<GridViewColumn Header="Message" Width="200" CellTemplate="{StaticResource CustomListViewItems}">
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>

So far so good, now here's the hard part: this only works well when the column has a fixed width, but the TextWrapping setting will fail to take effect otherwise. In order to have it work with a flexible width a little bit of hacking is required. The easiest way to do this is using the ActualWidth property on the GridViewColumn you're wrapping. The following code shows doing this with two columns in the ListView:


((GridView)myListView.View).Columns[1].Width =
myListView.ActualWidth - ((GridView)myListView.View).Columns[0].ActualWidth - 5

With this code I'm using Columns[1] as the column that wraps and I am determining the width with a little padding on the end (hence the "- 5"). There may be a more effecient way of doing this, but the math is pretty straight forward and you can run this code whenever the ListView would resize in order to maintain the text wrapping.

2 comments:

Ben said...

The example you found seems to be mostly correct, but I think you just should bind the data directly to the textblock, and skip the DisplayMemberBinding.

(note: i tried to paste the code here, but this stupid thing thinks it's HTML tags.)

Also, for this to work, you need to set a column width--otherwise WPF will size your column as wide as it can. Your user can change the size of the column and it will re-wrap.

Mike Junkin said...

You need to use a style to set the ListViewItem HorizontalContentAlignment to Sretch, then in your GridViewColumn.CellTemplate wrap the textblock in a stackpanel. Set the stack panels Orientation to vertical. Set the TextBlock TextWrapping to Wrap. Finally turn off the horizontal scroll bar ScrollViewer.HorizontalScrollBarVisibility=Disabled You can leave the scroll bar on but the text will then only wrap when the user resizes the column.