Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TextRange not usable in XAML #9925

Open
sjb-sjb opened this issue Aug 27, 2024 · 2 comments
Open

TextRange not usable in XAML #9925

sjb-sjb opened this issue Aug 27, 2024 · 2 comments
Labels
area-TextBlocks TextBlock, RichTextBlock feature proposal New feature proposal team-Controls Issue for the Controls team

Comments

@sjb-sjb
Copy link

sjb-sjb commented Aug 27, 2024

Describe the bug

One can't use TextRange in XAML because StartIndex and Length are fields not properties. One also can't create a derived class having such properties, because it is sealed. This makes it very difficult to use TextRange in a XAML data template.

Steps to reproduce the bug

Create a ListView with a resource for the DataTemplate as shown in the screenshot. Observe that the StartIndex and Length cannot be assigned or bound.

Expected behavior

Should be able to use TextRange in XAML.

Screenshots

image

NuGet package version

None

Windows version

No response

Additional context

No response

@sjb-sjb sjb-sjb added the bug Something isn't working label Aug 27, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Aug 27, 2024
@sjb-sjb
Copy link
Author

sjb-sjb commented Aug 27, 2024

My workaround in the meantime is to derive a custom TextHighlighter.

@codendone codendone added feature proposal New feature proposal area-TextBlocks TextBlock, RichTextBlock team-Controls Issue for the Controls team and removed bug Something isn't working needs-triage Issue needs to be triaged by the area owners labels Aug 29, 2024
@sjb-sjb
Copy link
Author

sjb-sjb commented Sep 21, 2024

This is for further information on the use case and to provide the workaround.

What I am trying to do is to highlight text in a rich text block based on a Match that was obtained from RegEx. The match is in an object that is being bound to a DataTemplate (specifically there is a Match property in the items in the ItemsSource of a ListView). Since this is in a DataTemplate, the Match has to be bound using Binding rather than x:Bind).

The idea I mentioned above of deriving from TextHighlighter did not work because the Match cannot be bound to a property in the TextHighlighter-derived class, owing to the fact that TextHighlighter is not a DependencyObject. Another idea would be to derive from RichTextBlock and add a Match property, but this doesn't work because RichTextBlock is sealed.

The solution is to use attached properties on the RichTextBlock to set the Match (and the Foreground and Background brushes). When the Match attached property is set, it sets the TextHighlighters property of the RichTextBlock.

The point here is this is much too hard. One should be able to easily set the highlighting on a RichTextBlock using binding.

The item class used in the ListView's ItemsSource looks something like this:

public class MyItem {
    Match Match { get; }
    string LocalizedName { get; }
} 

The DataTemplate for the ListView looks something like this:

<DataTemplate x:Name="mytemplate"  x:DataType="f:MyItem">
    <ListViewItem 
        Height="20"
        >
        <RichTextBlock 
            Style="{StaticResource BodyRichTextBlockStyle}"
            f:MatchHighlighter.Match="{Binding Match, Mode=OneWay}"
            f:MatchHighlighter.Foreground="{StaticResource FwkHighlightForegroundBrush}"
            f:MatchHighlighter.Background="{StaticResource FwkHighlightBackgroundBrush}"
            >
            <Paragraph>
                <Run Text="{Binding LocalizedName, Mode=OneWay}"/>
            </Paragraph>
        </RichTextBlock>
    </ListViewItem>
</DataTemplate>

And here are the attached properties:

public static class MatchHighlighter
{
    public static Match? GetMatch(RichTextBlock richTextBlock) => (Match?)richTextBlock.GetValue(MatchProperty);

    public static void SetMatch(RichTextBlock richTextBlock, Match? match) => richTextBlock.SetValue(MatchProperty, match);

    public static readonly DependencyProperty MatchProperty = DependencyProperty.RegisterAttached("Match", typeof(Match), typeof(MatchHighlighter), new PropertyMetadata(default(Match?), new PropertyChangedCallback(OnMatchChanged)));

    private static void OnMatchChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        if (sender is not RichTextBlock richTextBlock) { throw new InvalidOperationException($"Attempted to set {nameof(MatchHighlighter)}.Match attached property on an object other than a {nameof(RichTextBlock)}"); }
        Match? match = (Match?)args.NewValue;
        Brush foreground = (Brush?)MatchHighlighter.GetForeground(richTextBlock) ?? new SolidColorBrush(Colors.Red);
        Brush background = (Brush?)MatchHighlighter.GetBackground(richTextBlock) ?? new SolidColorBrush(Colors.Yellow);

        richTextBlock.TextHighlighters.Clear();
        if (match != null) {
            TextHighlighter textHighlighter = new TextHighlighter() { Foreground = foreground, Background = background };
            textHighlighter.Ranges.Add(new TextRange(match.Index, match.Length));
            richTextBlock.TextHighlighters.Add(textHighlighter);
        }
    }

    public static Brush? GetForeground(RichTextBlock richTextBlock) => (Brush?)richTextBlock.GetValue(ForegroundProperty);

    public static void SetForeground(RichTextBlock richTextBlock, Brush? match) => richTextBlock.SetValue(ForegroundProperty, match);

    public static readonly DependencyProperty ForegroundProperty = DependencyProperty.RegisterAttached("Foreground", typeof(Brush), typeof(MatchHighlighter), new PropertyMetadata(default(Brush?), new PropertyChangedCallback(OnForegroundChanged)));

    private static void OnForegroundChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        if (sender is not RichTextBlock richTextBlock) { throw new InvalidOperationException($"Attempted to set {nameof(MatchHighlighter)}.Foreground attached property on an object other than a {nameof(RichTextBlock)}"); }
        Match? match = (Match?)MatchHighlighter.GetMatch(richTextBlock);
        Brush? foreground = (Brush?)args.NewValue;

        if (foreground != null && match != null) {
            TextHighlighter textHighlighter = richTextBlock.TextHighlighters.FirstOrDefault() ?? throw new InvalidOperationException("Match set but texthighlighter not set.");
            textHighlighter.Foreground = foreground;
        }
    }

    public static Brush? GetBackground(RichTextBlock richTextBlock) => (Brush?)richTextBlock.GetValue(BackgroundProperty);

    public static void SetBackground(RichTextBlock richTextBlock, Brush? match) => richTextBlock.SetValue(BackgroundProperty, match);

    public static readonly DependencyProperty BackgroundProperty = DependencyProperty.RegisterAttached("Background", typeof(Brush), typeof(MatchHighlighter), new PropertyMetadata(default(Brush?), new PropertyChangedCallback(OnBackgroundChanged)));

    private static void OnBackgroundChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        if (sender is not RichTextBlock richTextBlock) { throw new InvalidOperationException($"Attempted to set {nameof(MatchHighlighter)}.Background attached property on an object other than a {nameof(RichTextBlock)}"); }
        Match? match = (Match?)MatchHighlighter.GetMatch(richTextBlock);
        Brush? background = (Brush?)args.NewValue;

        if (background != null && match != null) {
            TextHighlighter textHighlighter = richTextBlock.TextHighlighters.FirstOrDefault() ?? throw new InvalidOperationException("Match set but texthighlighter not set.");
            textHighlighter.Background = background;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-TextBlocks TextBlock, RichTextBlock feature proposal New feature proposal team-Controls Issue for the Controls team
Projects
None yet
Development

No branches or pull requests

3 participants
@sjb-sjb @codendone and others