Question

I have a view model with a Text property. The view model implements INotifyPropertyChanged.

public string Text
{
    get { return _text; }
    set
    {
        _text = value;
        NotifyPropertyChanged("Text");
    }
}
private string _text;

protected void NotifyPropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

A DataTemplate displays the Text of the view model in a TextBlock. The text block has wrapping turned on.

<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"/>
    </Grid>
</DataTemplate>

I have a requirement to show any text that was trimmed in another view. For example if my text was "Just some text" and only "Just some" was displayed I would need to display "text" in another view. Is there a simple way for the view model to determine what was displayed on screen without knowing that the text was displayed in a TextBlock with text wrapping?

I have looked at using the FormattedText class in the view model. However, it requires a lot of information that the view model does not have like Typeface and font size.

Was it helpful?

Solution 2

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.

OTHER TIPS

In my opinion the viewmodel is not the correct place to accomplish that. The viewmodel just contains the data for the view. What the view shows is a thing of the view. If the text is trimmed, only display the first part. You can handle that in some event handlers of the view. Some other view could be bound to the same viewmodel. In that other view you change the event handlers that only the other party are shown.

In order to reduce duplication you could define an user control that wraps this functionality.

Anyway you should check your requirement, doesn´t sounds quiet well to me but thats just my opinion ;-)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top