Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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);
        }
    }
}

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.

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

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);
    }
}

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()));
}

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()));
}

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top