3

My question is similar to this: WPF Generate TextBlock Inlines but I don't have enough reputation to comment. Here is the attached property class:

public class Attached
{
    public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
        "FormattedText",
        typeof(string),
        typeof(TextBlock),
        new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static void SetFormattedText(DependencyObject textBlock, string value)
    {
        textBlock.SetValue(FormattedTextProperty, value);
    }

    public static string GetFormattedText(DependencyObject textBlock)
    {
        return (string)textBlock.GetValue(FormattedTextProperty);
    }

    private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBlock = d as TextBlock;
        if (textBlock == null)
        {
            return;
        }

        var formattedText = (string)e.NewValue ?? string.Empty;
        formattedText = string.Format("<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{0}</Span>", formattedText);

        textBlock.Inlines.Clear();
        using (var xmlReader = XmlReader.Create(new StringReader(formattedText)))
        {
            var result = (Span)XamlReader.Load(xmlReader);
            textBlock.Inlines.Add(result);
        }
    }
}

I'm using this attached property class and trying to apply it to a textblock to make the text recognize inline values like bold, underline, etc from a string in my view model class. I have the following XAML in my textblock:

<TextBlock Grid.Row="1" Grid.Column="1" TextWrapping="Wrap" my:Attached.FormattedText="test" />

However I get nothing at all in the textblock when I start the program. I also would like to bind the text to a property on my view model eventually but wanted to get something to show up first...

Sorry this is probably a newbie question but I can't figure out why it's not working. It doesn't give me any error here, just doesn't show up. If I try to bind, it gives me the error:

{"A 'Binding' cannot be set on the 'SetFormattedText' property of type 'TextBlock'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject."}

Community
  • 1
  • 1
sfaust
  • 2,089
  • 28
  • 54
  • At StackOverflow, you are *supposed* to ask questions as questions and *not* as comments and it's good that you provided a link to the other question. However, you need to provide *[all of the relevant code that is required to reproduce your problem](http://stackoverflow.com/help/mcve)*, as this is one reason that the community might decide to delete your question. – Sheridan Feb 19 '14 at 22:54
  • Ok sorry, thanks for the pointers. I updated to include the source code for the class I'm trying to use and a little more detail of where I'm trying to go with it... – sfaust Feb 19 '14 at 23:45

1 Answers1

5

First, the type of property needs to be a class name, not the type TextBlock:

public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
    "FormattedText",
    typeof(string),
    typeof(TextBlock), <----- Here

Second, the handler is not called, it must be registered here:

new FrameworkPropertyMetadata(string.Empty, 
                              FrameworkPropertyMetadataOptions.AffectsMeasure,
                              YOUR_PropertyChanged_HANDLER)

Thirdly, an example to work, you need to specify the input string like this:

<Bold>My little text</Bold>

Working example is below:

XAML

<Window x:Class="InlineTextBlockHelp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:InlineTextBlockHelp"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <TextBlock Name="TestText"
                   this:AttachedPropertyTest.FormattedText="TestString"
                   Width="200"
                   Height="100" 
                   TextWrapping="Wrap" />

        <Button Name="TestButton"
                Width="100"
                Height="30"
                VerticalAlignment="Top"
                Content="TestClick" 
                Click="Button_Click" />
    </Grid>
</Window>

Code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        string inlineExpression = "<Bold>Once I saw a little bird, go hop, hop, hop.</Bold>";
        AttachedPropertyTest.SetFormattedText(TestText, inlineExpression);
    }
}

public class AttachedPropertyTest
{
    public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
        "FormattedText",
        typeof(string),
        typeof(AttachedPropertyTest),
        new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, FormattedTextPropertyChanged));

    public static void SetFormattedText(DependencyObject textBlock, string value)
    {
        textBlock.SetValue(FormattedTextProperty, value);
    }

    public static string GetFormattedText(DependencyObject textBlock)
    {
        return (string)textBlock.GetValue(FormattedTextProperty);
    }

    private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBlock = d as TextBlock;

        if (textBlock == null)
        {
            return;
        }

        var formattedText = (string)e.NewValue ?? string.Empty;
        formattedText = string.Format("<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{0}</Span>", formattedText);

        textBlock.Inlines.Clear();
        using (var xmlReader = XmlReader.Create(new StringReader(formattedText)))
        {
            var result = (Span)XamlReader.Load(xmlReader);
            textBlock.Inlines.Add(result);
        }
    }
}

Initially be plain text, after clicking on the Button will be assigned to inline text.

Example for MVVM version

To use this example in MVVM style, you need to create the appropriate property in the Model/ViewModel and associate it with the attached dependency property like this:

<TextBlock Name="TestText"
           PropertiesExtension:TextBlockExt.FormattedText="{Binding Path=InlineText, 
                                                                    Mode=TwoWay,
                                                                    UpdateSourceTrigger=PropertyChanged}"
           Width="200"
           Height="100" 
           TextWrapping="Wrap" />

Property in Model/ViewModel must support method NotifyPropertyChanged.

Here is a full sample:

AttachedProperty

public class TextBlockExt
{
    public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
        "FormattedText",
        typeof(string),
        typeof(TextBlockExt),
        new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, FormattedTextPropertyChanged));

    public static void SetFormattedText(DependencyObject textBlock, string value)
    {
        textBlock.SetValue(FormattedTextProperty, value);
    }

    public static string GetFormattedText(DependencyObject textBlock)
    {
        return (string)textBlock.GetValue(FormattedTextProperty);
    }

    private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBlock = d as TextBlock;

        if (textBlock == null)
        {
            return;
        }

        var formattedText = (string)e.NewValue ?? string.Empty;
        formattedText = string.Format("<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{0}</Span>", formattedText);

        textBlock.Inlines.Clear();
        using (var xmlReader = XmlReader.Create(new StringReader(formattedText)))
        {
            var result = (Span)XamlReader.Load(xmlReader);
            textBlock.Inlines.Add(result);
        }
    }
}

MainViewModel

public class MainViewModel : NotificationObject
{
    private string _inlineText = "";

    public string InlineText
    {
        get
        {
            return _inlineText;
        }

        set
        {
            _inlineText = value;
            NotifyPropertyChanged("InlineText");
        }
    }
}

MainWindow.xaml

<Window x:Class="InlineTextBlockHelp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ViewModels="clr-namespace:InlineTextBlockHelp.ViewModels"
        xmlns:PropertiesExtension="clr-namespace:InlineTextBlockHelp.PropertiesExtension"
        Title="MainWindow" Height="350" Width="525"
        ContentRendered="Window_ContentRendered">

    <Window.DataContext>
        <ViewModels:MainViewModel />
    </Window.DataContext>

    <Grid>
        <TextBlock Name="TestText"
                   PropertiesExtension:TextBlockExt.FormattedText="{Binding Path=InlineText, 
                                                                            Mode=TwoWay,
                                                                            UpdateSourceTrigger=PropertyChanged}"
                   Width="200"
                   Height="100" 
                   TextWrapping="Wrap" />
    </Grid>
</Window>

Code-behind (just for test)

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_ContentRendered(object sender, EventArgs e)
    {
        MainViewModel mainViewModel = this.DataContext as MainViewModel;

        mainViewModel.InlineText = "<Bold>Once I saw a little bird, go hop, hop, hop.</Bold>";
    }
}

This example is available at this link.

Anatoliy Nikolaev
  • 22,370
  • 15
  • 69
  • 68
  • I apologize, I was not ignoring you, just got pulled away to put out a fire and just now got back to this. Thank you that is very helpful in understanding what is going on. However this requires you to press a button in order to set the text to the new value. What I really need is to bind the value to a property on my view model, which may change while the interface is open. I don't see how I can do this... is it possible? Also I'm upvoting for a very helpful answer but not marking it as answer yet as I'm looking for the binding piece... – sfaust Feb 21 '14 at 16:56
  • Awesome, marked as answer! I downloaded your example and the only oddity is that it keeps giving me 'Undefined CLR Namespace' errors for your two custom namespaces. I have checked them out and they seem to be correct so I'm not sure why. It builds and functions fine but not sure why the designer show those errors... – sfaust Feb 21 '14 at 21:09
  • Actually I just modified my project based on what you posted and I don't get the designer errors in my project. Not sure what the issue is in the sample but doesn't matter. That is exactly what I needed. Thank you, wish I could upvote it again! – sfaust Feb 21 '14 at 21:16
  • 1
    This is a great answer and it fixed my problem. Only problem I found is that the `formattedText` in `TextBlockExt.FormattedTextPropertyChanged` should be XML encoded as per http://stackoverflow.com/a/19498780/492 ...but just the plain text in the XML, not the markup. – CAD bloke Dec 29 '14 at 12:40