0

I have an external control which displays a layout formed from a label and an input control. My label requires special formatting (subscript) but it currently only supports direct text.

So my approach is to create a custom TextBlock implementation, which exposes a new InlineContent dependency property that, once set, converts the content and adds it to it's actual Inlines collection.

For the layout control I add a custom DataTemplate which binds the label content to the InlineContent property of my custom text block.

ExtendedTextBlock.cs:

private static void InlinesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (!(d is ExtendedTextBlock b)) return;

    b.Inlines.Clear();
    if (e.NewValue is InlineCollection collection)
        b.Inlines.AddRange(collection);
    if (e.NewValue is Span span)
        b.Inlines.AddRange(span.Inlines);
    if (e.NewValue is Run run)
        b.Inlines.Add(run);
    if (e.NewValue is string str)
        b.Inlines.Add(new Run(str));
}

DataTemplate:

<DataTemplate>
    <controls:ExtendedTextBlock InlineContent="{Binding}" />
</DataTemplate>

Label:

<dxlc:LayoutItem.Label>
    <Span>
        <Run>Right (R</Run>
        <Run Typography.Variants="Subscript">R</Run>
        <Run>)</Run>
    </Span>
</dxlc:LayoutItem.Label>

This works fine for regular text (strings) but when I set a Span as my label's content, then I get the following exception:

System.Windows.Markup.XamlParseException: 'Collection was modified; enumeration operation may not execute.' Inner Exception: InvalidOperationException: Collection was modified; enumeration operation may not execute.

This occurs in line b.Inlines.AddRange(span.Inlines). Why so? I don't understand which collection changes.

Binding directly to Text does not work. Then I only see 'System.Documents.Text.Span` but not the span actually being rendered.

SharpShade
  • 1,761
  • 2
  • 32
  • 45
  • Two notes. Checking `if (!(d is ExtendedTextBlock b))` is redundant. The property can not be set on any DependencyObject that is not an ExtendedTextBlock. Better use a cast to get an `InvalidCastException` in case the code is used in an unexpected way. Also use `else if` instead of a chain of `if` statements. – Clemens Sep 06 '19 at 10:28
  • Thanks for the comment. Well, I wasn't sure about that. The if's were mainly because I just quickly tried around and normally only one of these cases can occur at the same time, but you're right nonetheless. Changed it. – SharpShade Sep 06 '19 at 10:50

1 Answers1

1

No idea why that happens, but copying Span.Inlines to a new collection solves the problem:

using System.Linq;
...

b.Inlines.AddRange(span.Inlines.ToList());
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • It does work but the text is displayed without any effects (no subscript) for some reason. I also tried adding the span directly, but then I get an exception saying `Specified element is already the logical child of another element. Disconnect it first.`. How can I preserve the subscript, though? – SharpShade Sep 06 '19 at 10:57
  • Yeah I just tried it. It only works for numbers, but not for letters. – SharpShade Sep 06 '19 at 11:01
  • You need a font that supports it. MS Docs samples use `FontFamily="Palatino Linotype"`. – Clemens Sep 06 '19 at 11:03
  • No, one of the comments in the [marked answer](https://stackoverflow.com/questions/2095583/set-superscript-and-subscript-in-formatted-text-in-wpf) says that there's a known bug (sadly the link to MS Connect doesn't work, so I don't know if that is the same bug we're talking about). Nonetheless it works when setting `BaselineAlignment` property instead of the attached `Typography.Variants` property. – SharpShade Sep 06 '19 at 11:04
  • Well, for me it works with `FontFamily="Palatino Linotype"`. – Clemens Sep 06 '19 at 11:05