Data binding the TextBlock.Inlines
Asked Answered
P

10

39

My WPF App receives a stream of messages from a backend service that I need to display in the UI. These messages vary widely and I want to have different visual layout (string formats, colors, Fonts, icons, whatever etc.) for each message.

I was hoping to just be able to create an inline (Run, TextBlock, Italic etc) for each message then somehow put them all in a ObservableCollection<> and using he magic of WPF Data Binding on my TextBlock.Inlines in the UI. I couldn't find how to do this, is this possible?

Pandich answered 24/12, 2009 at 21:51 Comment(0)
B
14

This is not possible because the TextBlock.Inlines property is not a dependency property. Only dependency properties can be the target of a data binding.

Depending on your exact layout requirements you may be able to do this using an ItemsControl, with its ItemsPanel set to a WrapPanel and its ItemsSource set to your collection. (Some experimentation may be required here because an Inline is not a UIElement, so its default rendering will probably be done using ToString() rather than being displayed.)

Alternatively, you may need to build a new control, e.g. MultipartTextBlock, with a bindable PartsSource property and a TextBlock as its default template. When the PartsSource was set your control would attach a CollectionChanged event handler (directly or via CollectionChangedEventManager), and update the TextBlock.Inlines collection from code as the PartsSource collection changed.

In either case, caution may be required if your code is generating Inline elements directly (because an Inline can't be used in two places at the same time). You may alternatively want to consider exposing an abstract model of text, font, etc. (i.e. a view model) and creating the actual Inline objects via a DataTemplate. This may also improve testability, but obviously adds complexity and effort.

Bluma answered 25/12, 2009 at 0:21 Comment(0)
T
15

You could add a Dependency Property to a TextBlock Subclass

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList",typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = sender as BindableTextBlock;
        ObservableCollection<Inline> list = e.NewValue as ObservableCollection<Inline>;
        list.CollectionChanged += new     System.Collections.Specialized.NotifyCollectionChangedEventHandler(textBlock.InlineCollectionChanged);
    }

    private void InlineCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        {
            int idx = e.NewItems.Count -1;
            Inline inline = e.NewItems[idx] as Inline;
            this.Inlines.Add(inline);
        }
    }
}
Test answered 3/3, 2012 at 13:8 Comment(6)
TextBlock is sealed on windows phone 8Ineludible
Thanks a million for this. I had to make a couple of minor changes to get it to work.Bethsaida
Result in InvalidOperation: "The calling thread cannot access this object because a different thread owns it". On the line: this.Inlines.Add.... so this is not a real solutionImplantation
@Implantation use dispatcher of main threadFictionalize
Using this subclass inside an <ItemsControl> my bound Inline elements constantly showed empty. Like @Bethsaida already mentioned, in this case the OnPropertyChanged method must be adjusted, for example like shown here.Unlay
I'm looking for a solution to this same problem because I want to move a huge amount of processing out of a loop run on the UI dispatcher. Data binding to the text block, including some runs to control the font colour, is a rational solution. Having to dispatch the addition on the UI thread defests the purpose entirely.Kuopio
B
14

This is not possible because the TextBlock.Inlines property is not a dependency property. Only dependency properties can be the target of a data binding.

Depending on your exact layout requirements you may be able to do this using an ItemsControl, with its ItemsPanel set to a WrapPanel and its ItemsSource set to your collection. (Some experimentation may be required here because an Inline is not a UIElement, so its default rendering will probably be done using ToString() rather than being displayed.)

Alternatively, you may need to build a new control, e.g. MultipartTextBlock, with a bindable PartsSource property and a TextBlock as its default template. When the PartsSource was set your control would attach a CollectionChanged event handler (directly or via CollectionChangedEventManager), and update the TextBlock.Inlines collection from code as the PartsSource collection changed.

In either case, caution may be required if your code is generating Inline elements directly (because an Inline can't be used in two places at the same time). You may alternatively want to consider exposing an abstract model of text, font, etc. (i.e. a view model) and creating the actual Inline objects via a DataTemplate. This may also improve testability, but obviously adds complexity and effort.

Bluma answered 25/12, 2009 at 0:21 Comment(0)
H
14

This is an alternative solution which utilizes WPF behaviors/attached properties:

public static class TextBlockExtensions
{
    public static IEnumerable<Inline> GetBindableInlines ( DependencyObject obj )
    {
        return (IEnumerable<Inline>) obj.GetValue ( BindableInlinesProperty );
    }

    public static void SetBindableInlines ( DependencyObject obj, IEnumerable<Inline> value )
    {
        obj.SetValue ( BindableInlinesProperty, value );
    }

    public static readonly DependencyProperty BindableInlinesProperty =
        DependencyProperty.RegisterAttached ( "BindableInlines", typeof ( IEnumerable<Inline> ), typeof ( TextBlockExtensions ), new PropertyMetadata ( null, OnBindableInlinesChanged ) );

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

        if ( Target != null )
        {
            Target.Inlines.Clear ();
            Target.Inlines.AddRange ( (System.Collections.IEnumerable) e.NewValue );
        }
    }
}

In your XAML, use it like this:

<TextBlock MyBehaviors:TextBlockExtensions.BindableInlines="{Binding Foo}" />

This saves you from having to inherit from TextBlock. It could just as well work using an ObservableCollection instead of IEnumerable, in that case you'd need to subscribe to collection changes.

Honeycutt answered 1/12, 2017 at 18:54 Comment(1)
On my WindowModel I defined public ObservableCollection<Inline> ProcessTrackerInlines { get; set; } and binded it to TextBlockExtensions.BindableInlines="{Binding ProcessTrackerInlines, Mode=OneWay}". Added a method loadProcessTracker to the windowModel to fill the ProcessTrackerInlines and everything worked fine, but if I latter(by clicking a button) try to add an Inline, and call PropertyChanged . The new inline does not show on the control. I can Add the code if needed.Phore
M
6

In version 4 of WPF you will be be able to bind to a Run object, which may solve your problem.

I have solved this problem in the past by overriding an ItemsControl and displaying the text as items in the ItemsControl. Look at some of the tutorials that Dr. WPF has done on this kind of stuff: http://www.drwpf.com

Melissiamelita answered 26/12, 2009 at 12:41 Comment(0)
B
6

Thanks Frank for your solution. I had to make a couple of minor changes to make it work for me.

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>) GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty =
        DependencyProperty.Register("InlineList", typeof (ObservableCollection<Inline>), typeof (BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = (BindableTextBlock) sender;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange((ObservableCollection<Inline>) e.NewValue);
    }
}
Bethsaida answered 28/5, 2015 at 19:1 Comment(2)
Better add a textBlock.Inlines.Clear() before the AddRange in order to reset the inlines when changedWeariless
then how do i use this? sry im newPfaff
I
4

If i am getting your requirement correctly, you can manually check for the coming messages and for each message you can add an element to TextBlock.Inlines property. It will not take any DataBinding. I have done this with the following:

public string MyBindingPath
{
    get { return (string)GetValue(MyBindingPathProperty); }
    set { SetValue(MyBindingPathProperty, value); }
}

// Using a DependencyProperty as the backing store for MyBindingPath.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(Window2), new UIPropertyMetadata(null, OnPropertyChanged));

private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    (sender as Window2).textBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
Interleaf answered 25/12, 2009 at 3:55 Comment(1)
What does the XAML look like for your above example?Phyllotaxis
S
1

The Suggestion from Pavel Anhikouski works perfectly. Here the missing part with databinding in MVVM. Use the AddTrace property in the viewmodel to add content to the OutputBlock in the window. The backing property MyBindingPath in the window is not needed.

ViewModel:

private string _addTrace;
public string AddTrace
{
  get => _addTrace;
  set
  {
    _addTrace = value;
    NotifyPropertyChanged();
  }
}

public void StartTrace()
{
  AddTrace = "1\n";
  AddTrace = "2\n";
  AddTrace = "3\n";
}

TraceWindow.xaml:

  <Grid>
    <ScrollViewer Name="Scroller" Margin="0" Background="#FF000128">
      <TextBlock Name="OutputBlock"  Foreground="White" FontFamily="Consolas" Padding="10"/>
    </ScrollViewer>
  </Grid>

TraceWindow.xaml.cs:

public TraceWindow(TraceWindowModel context)
{
  DataContext = context;
  InitializeComponent();

  //bind MyBindingPathProperty to AddTrace
  Binding binding = new Binding("AddTrace");
  binding.Source = context;
  this.SetBinding(MyBindingPathProperty, binding);
}

public static readonly DependencyProperty MyBindingPathProperty =
        DependencyProperty.Register("MyBindingPath", typeof(string), typeof(TraceWindow), new UIPropertyMetadata(null, OnPropertyChanged));



private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
  (sender as TraceWindow).OutputBlock.Inlines.Add(new Run(e.NewValue.ToString()));
}
Superclass answered 11/12, 2019 at 10:6 Comment(0)
S
1

Most recently I had a similar task to solve, namely; having unlimited number of url links inserted to a custom message box text content, and have a binding path to this text. I decided to post my implementation here seeing that this thread had some evolution of different great ideas... Here is my solution:

The concept: The flow of xaml TextBlock content:

<TextBlock>
  ...
  <Inline>
  <Hyperlink <Inline>>
  <Inline>
  <Hyperlink <Inline>>
  ...
  1. My x:Name=MixedText TextBlock element receives its value as a single text formated as:

"...some text here...[link-text|url-link]...some other text here... etc."

Sample:

"Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A."

  1. I do my parsing and all elements' injection to my MixedText TextBlock element at the DataContextChanged event.

The xaml part: Defining the binding path (MixedText). ...

         <TextBlock Grid.Row="3" Grid.Column="1" 
                    x:Name="HyperlinkContent" 
                    TextWrapping="Wrap"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left"
                    Text="{Binding Path = MixedText}">
        </TextBlock>

The ViewModel part: Defining the binding path property.

    public string MixedText
    {
        get { return _mixedText; }
        set
        {
            _mixedText = value;
            OnPropertyChanged();
        }
    }
    string _mixedText;

The MultipartTextHandler class where I implement the MixedText parsing and dynamic xaml injection model preparation.

class MultipartTextHandler
{
    public static IEnumerable<(int Index, Type Type, object Control, string Text, bool IsHyperlink)> CreateControls(string multipartText)
    {
        // 1. Return null if no multipart text is found. This will be just an ordinary text passed to a binding path.
        var multipartTextCollection = GetMultipartTextCollection(multipartText);
        if (!multipartTextCollection.Any())
            return Enumerable.Empty<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        var result = new List<(int Index, Type Type, object Control, string Text, bool IsHyperlink)>();

        // 2. Process multipart texts that have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink != null))
        {
            var hyperlink = new Hyperlink { NavigateUri = new Uri(e.Hyperlink) };
            hyperlink.Click += (sender, e1) => Process.Start(new ProcessStartInfo(new Uri(e.Hyperlink).ToString()));
            hyperlink.Inlines.Add(new Run { Text = e.Text });
            result.Add((Index: e.Index, Type: typeof(Hyperlink), Control: hyperlink, Text: e.Text, IsHyperlink: true));
        }

        // 3. Process multipart texts that do not have Hyperlink content.
        foreach (var e in multipartTextCollection.Where(x => x.Hyperlink == null))
        {
            var inline = new Run { Text = e.Text };
            result.Add((Index: e.Index, Type: typeof(Inline), Control: inline, Text: e.Text, IsHyperlink: false));
        }

        return result.OrderBy(x => x.Index);
    }

    /// <summary>
    /// Returns list of Inline and Hyperlink segments.
    /// Parameter sample:
    /// "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son &#x2600."
    /// </summary>
    /// <param name="multipartText">See sample on comment</param>
    static IEnumerable<(int Index, string Text, string Hyperlink)> GetMultipartTextCollection(string multipartText)
    {
        // 1. Make sure we have a url string in parameter argument.
        if (!ContainsURL(multipartText))
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2a. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '[' || x == ']') % 2 != 0)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        // 2b. Make sure format of url link fits to our parsing schema.
        if (multipartText.Count(x => x == '|') != multipartText.Count(x => x == '[' || x == ']') / 2)
            return Enumerable.Empty<(int Index, string Text, string Hyperlink)>();

        var result = new List<(int Index, string Text, string Hyperlink)>();

        // 3. Split to Inline and Hyperlink segments.
        var multiParts = multipartText.Split(new char[] { '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var part in multiParts)
        {
            // Hyperlink segment must contain inline and Hyperlink splitter checked in step 2b.
            if (part.Contains('|'))
            {
                // 4a. Split the hyperlink segment of the overall multipart text to Hyperlink's inline
                // and Hyperlink "object" contents. Note that the 1st part is the text that will be
                // visible inline text with 2nd part that will have the url link "under."
                var hyperPair = part.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                // 4b. Add hyperlink record to the return list: Make sure we keep the order in which 
                // these values are set at multipartText. Note that Hyperlink's inline, and Hyperlink 
                // url texts are added to Text: and Hyperlink: properties separately.
                result.Add((Index: result.Count + 1, Text: hyperPair[0], Hyperlink: hyperPair[1]));
            }
            else
            {
                // 5. This text will be an inline element either before or after the hyperlink element.
                // So, Hyperlink parameter we will set null to later process differently.
                result.Add((Index: result.Count + 1, Text: part, Hyperlink: null));
            }
        }

        return result;
    }

    /// <summary>
    /// Returns true if a text contains a url string (pattern).
    /// </summary>
    /// <param name="Text"></param>
    /// <returns></returns>
    static bool ContainsURL(string Text)
    {
        var pattern = @"([a-zA-Z\d]+:\/\/)?((\w+:\w+@)?([a-zA-Z\d.-]+\.[A-Za-z]{2,4})(:\d+)?(\/)?([\S]+))";
        var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
        return regex.IsMatch(Text);
    }
}

The Code-behind stuff.

  1. Inside the view constructor:

    this.DataContextChanged += MessageBoxView_DataContextChanged;

  2. The MessageBoxView_DataContextChanged implementation.

     private void MessageBoxView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
     {
         var viewModel = (MessageBoxViewModel)e.NewValue;
         var mixedText = viewModel.MixedText;
         var components = MultipartTextHandler.CreateControls(mixedText);
         this.HyperlinkContent.Inlines.Clear();
         this.HyperlinkContent.Text = null;
         foreach (var content in components)
         {
             if (content.Type == typeof(Inline))
                 this.HyperlinkContent.Inlines.Add(new Run { Text = content.Text });
    
             else if (content.Type == typeof(Hyperlink))
                 this.HyperlinkContent.Inlines.Add((Hyperlink)content.Control);
         }
     }
    

The usage, from my console application.

    static void Test()
    {
        var viewModel = new MessageBox.MessageBoxViewModel()
        {
            MixedText = "Please visit the Microsoft [site|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information], and download the Windows 7 SP1, complete the SP1 installation then re-run the installer again. Go to [roblox|https://www.roblox.com] site to relax a bit like my son \u263A.",
        };
        var view = new MessageBox.MessageBoxView();
        view.DataContext = viewModel; // Here is where all fun stuff happens

        var application = new System.Windows.Application();
        application.Run(view);

        Console.WriteLine("Hello World!");
    }

The actual dialog display view:

enter image description here

Seedman answered 27/3, 2022 at 4:20 Comment(0)
P
0
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized

Public Class BindableTextBlock
Inherits TextBlock

Public Property InlineList As ObservableCollection(Of Inline)
    Get
        Return GetValue(InlineListProperty)
    End Get

    Set(ByVal value As ObservableCollection(Of Inline))
        SetValue(InlineListProperty, value)
    End Set
End Property

Public Shared ReadOnly InlineListProperty As DependencyProperty = _
                       DependencyProperty.Register("InlineList", _
                       GetType(ObservableCollection(Of Inline)), GetType(BindableTextBlock), _
                       New UIPropertyMetadata(Nothing, AddressOf OnInlineListPropertyChanged))

Private Shared Sub OnInlineListPropertyChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs)
    Dim textBlock As BindableTextBlock = TryCast(sender, BindableTextBlock)
    Dim list As ObservableCollection(Of Inline) = TryCast(e.NewValue, ObservableCollection(Of Inline))
    If textBlock IsNot Nothing Then
        If list IsNot Nothing Then
            ' Add in the event handler for collection changed
            AddHandler list.CollectionChanged, AddressOf textBlock.InlineCollectionChanged
            textBlock.Inlines.Clear()
            textBlock.Inlines.AddRange(list)
        Else
            textBlock.Inlines.Clear()

        End If
    End If
End Sub

''' <summary>
''' Adds the items to the inlines
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub InlineCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
    Select Case e.Action
        Case NotifyCollectionChangedAction.Add
            Me.Inlines.AddRange(e.NewItems)
        Case NotifyCollectionChangedAction.Reset
            Me.Inlines.Clear()
        Case NotifyCollectionChangedAction.Remove
            For Each Line As Inline In e.OldItems
                If Me.Inlines.Contains(Line) Then
                    Me.Inlines.Remove(Line)
                End If
            Next
    End Select
End Sub

End Class

I think you may need some additional code on the PropertyChanged handler, so to initialise the textBlock.Inlines if the bound collection already has content, and to clear any existing context.

Postnasal answered 6/1, 2013 at 23:48 Comment(0)
V
0

Everyone given good solutions, but I had a similar problem and after hours looking for solutions I decide try directly bind to default content. Without Dependency Properties. Sorry my obsolete english... hehehehe

[ContentProperty("Inlines")]
public partial class WindowControl : UserControl
{
    public InlineCollection Inlines { get => txbTitle.Inlines; }
}

Ok, lets use this on your xaml file...

<local:WindowControl>
    .:: Register Logbook : Connected User - <Run Text="{Binding ConnectedUser.Name}"/> ::.
</local:WindowControl>

And voila!

It's because they bind inlines is unnecessary, you can modify de parts of a text from another control contents without a binding, this solution help-me.

Versed answered 24/7, 2020 at 19:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.