Binding Inlines in TextBlock

Roland, 21.04.2016

For source code go to https://github.com/rolandzpl/WpfHighlighter

TextBlock control in WPF has Inlines property that isn't a DependencyProperty. That makes it impossible to bind it directly to the view model. To make it possible, an attached property and some value converters can come to the rescue.

So the attached property implementation looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace WPF_BindingInlinesInTextBlock
{
    public class Attached
    {
        public static IEnumerable<Inline> GetInlines(DependencyObject d)
        {
            return (IEnumerable<Inline>)d.GetValue(InlinesProperty);
        }

        public static void SetInlines(DependencyObject d, IEnumerable<Inline> value)
        {
            d.SetValue(InlinesProperty, value);
        }

        // Using a DependencyProperty as the backing store for Inlines.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InlinesProperty =
            DependencyProperty.RegisterAttached("Inlines", typeof(IEnumerable<Inline>), typeof(Attached),
                new FrameworkPropertyMetadata(OnInlinesPropertyChanged));

        private static void OnInlinesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var textBlock = d as TextBlock;
            if (textBlock == null)
            {
                return;
            }
            var inlinesCollection = textBlock.Inlines;
            inlinesCollection.Clear();
            inlinesCollection.AddRange((IEnumerable<Inline>)e.NewValue);
        }
    }
}

Considering following view model:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WPF_BindingInlinesInTextBlock
{
    public class ViewModel : INotifyPropertyChanged
    {
        private string text;

        public ViewModel(string text)
        {
            this.text = text;
        }

        public string Text
        {
            get { return text; }
            set
            {
                text = value;
                FirePropertyChanged("Text");
            }
        }

        private void FirePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

We would like to have the value converter as follows:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Documents;

namespace WPF_BindingInlinesInTextBlock
{
    public class StringToInlinesConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter,
            CultureInfo culture)
        {
            var text = value as string;
            if (text == null)
            {
                return null;
            }
            var words = GetWords(text);
            return GetInlines(words).ToList();
        }

        private static IEnumerable<Inline> GetInlines(IEnumerable<string> words)
        {
            return words.Select(x => new Run(x));
        }

        private static IEnumerable<string> GetWords(string text)
        {
            return text.Split(' ');
        }

        public object ConvertBack(object value, Type targetType, object parameter,
            CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Now let's wire it all together in XAML:

<Window x:Class="WPF_BindingInlinesInTextBlock.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_BindingInlinesInTextBlock"
        mc:Ignorable="d" Title="MainWindow">
    <Grid>
        <Grid.Resources>
            <local:StringToInlinesConverter x:Key="stringToInlinesConverter" />
        </Grid.Resources>
        <TextBlock local:Attached.Inlines="{Binding Text, 
            Converter={StaticResource stringToInlinesConverter}}" />
    </Grid>
</Window>

That's it. Test property is being split into words and converter into a collection of inlines. Then it gets bound to attached property which would set the Inlines property on a TextBlock control.