The link provided by @NETscape pointed me in the right direction. The link pointed me to another page that talked about using attached properties to expose an IsTextWrapped property on TextBlock. I came up with a similar approach that exposes the index of the trim.
I created a static class to expose a property that holds the index where wrapping occurs. Another property enables or disables the calculation of the index. For my case I only need to know where wrapping occurs in a few scenarios. I am calculating HiddenTextIndex with the Loaded event of the TextBlock. This only works because I know that the text will not be changing within the context of the view. I did have to refactor the code that feeds data into my view model a little to ensure it would not be required to change after it was initially loaded.
public static class TextBlockWrapService
{
public static readonly DependencyProperty IsWrapAwareProperty =
DependencyProperty.RegisterAttached("IsWrapAware", typeof(bool), typeof(TextBlockWrapService),
new PropertyMetadata(false, IsWrapAwarePropertyChanged));
public static readonly DependencyProperty HiddenTextIndexProperty =
DependencyProperty.RegisterAttached("HiddenTextIndex", typeof(int), typeof(TextBlockWrapService),
new PropertyMetadata(-1));
public static bool GetIsWrapAware(TextBlock block)
{
if (block == null)
return false;
return (bool)block.GetValue(IsWrapAwareProperty);
}
public static void SetIsWrapAware(TextBlock block, bool value)
{
if (block == null)
return;
block.SetValue(IsWrapAwareProperty, value);
}
public static int GetHiddenTextIndex(TextBlock block)
{
if (!GetIsWrapAware(block))
return -1;
return (int)block.GetValue(HiddenTextIndexProperty);
}
public static void SetHiddenTextIndex(TextBlock block, int value)
{
if (block == null)
return;
block.SetValue(HiddenTextIndexProperty, value);
}
private static void IsWrapAwarePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var block = d as TextBlock;
if (block == null || e.NewValue == null)
return;
bool wrapAware;
if (!bool.TryParse(e.NewValue.ToString(), out wrapAware))
return;
if (wrapAware)
block.Loaded += TextBlock_Loaded;
else
block.Loaded -= TextBlock_Loaded;
}
private static void TextBlock_Loaded(object sender, RoutedEventArgs e)
{
UpdateHiddenTextIndex(sender as TextBlock);
}
private static void UpdateHiddenTextIndex(TextBlock block)
{
if (block == null)
return;
if (!GetIsWrapAware(block) || block.TextTrimming == TextTrimming.None)
SetHiddenTextIndex(block, -1);
else
SetHiddenTextIndex(block, CalculateHiddenTextIndex(block));
}
public static int CalculateHiddenTextIndex(TextBlock block)
{
var typeface = new Typeface(block.FontFamily,
block.FontStyle,
block.FontWeight,
block.FontStretch);
if (string.IsNullOrWhiteSpace(block.Text) ||
block.ActualHeight <= 1 ||
!TextIsTrimmedAtLength(block, typeface, block.Text.Length) ||
TextIsTrimmedAtLength(block, typeface, 0))
return -1;
var untrimmedLength = 1;
var trimmedLength = block.Text.Length;
while (untrimmedLength < trimmedLength - 1)
{
var untestedLength = (trimmedLength + untrimmedLength) / 2;
if (untestedLength <= untrimmedLength || untestedLength >= trimmedLength)
break;
if (TextIsTrimmedAtLength(block, typeface, untestedLength))
trimmedLength = untestedLength;
else
untrimmedLength = untestedLength;
}
return untrimmedLength;
}
private static bool TextIsTrimmedAtLength(TextBlock block, Typeface typeface, int length)
{
var formattedText = new FormattedText(block.Text.Substring(0, length),
Thread.CurrentThread.CurrentCulture,
block.FlowDirection,
typeface,
block.FontSize,
block.Foreground) { MaxTextWidth = block.ActualWidth };
return formattedText.Height > block.ActualHeight;
}
}
IsWrapAware will allow me to only calculate the index when it is required. The HiddenTextIndex is the index where wrapping occurs. The HiddenTextIndex is passed to the view model through binding in the xaml.
<DataTemplate DataType="{x:Type viewModels:TextViewModel}">
<Grid Width="{Binding Path=Width, Mode=OneWay}"
Height="{Binding Path=Height, Mode=OneWay}">
<TextBlock Text="{Binding Path=Text, Mode=OneWay}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
TextAlignment="Left"
utility:TextBlockWrapService.IsWrapAware="True"
utility:TextBlockWrapService.HiddenTextIndex="{Binding Path=HiddenTextIndex, Mode=OneWayToSource}"/>
</Grid>
</DataTemplate>
There is a corresponding HiddenTextIndex property on the view model. At this point I can use Substring to determine what is hidden and what is displayed.