Привязка данных к TextBlock.Inlines

Мое приложение WPF получает поток сообщений от серверной службы, которые мне нужно отобразить в пользовательском интерфейсе. Эти сообщения сильно различаются, и я хочу иметь различный визуальный макет (форматы строк, цвета, шрифты, значки и т. д.) для каждого сообщения.

Я надеялся просто создать встроенный (Run, TextBlock, Italic и т. д.) для каждого сообщения, а затем каким-то образом поместить их все в ObservableCollection<> и использовать магию привязки данных WPF к моему TextBlock.Inlines в пользовательском интерфейсе. Я не нашел как это сделать, возможно ли это?


person will    schedule 24.12.2009    source источник


Ответы (9)


Это невозможно, так как свойство TextBlock.Inlines не является свойством зависимости. Только свойства зависимостей могут быть целью привязки данных.

В зависимости от ваших точных требований к макету вы можете сделать это, используя ItemsControl, с его ItemsPanel, установленным на WrapPanel, и его ItemsSource, установленным на вашу коллекцию. (Здесь могут потребоваться некоторые эксперименты, потому что Inline не является UIElement, поэтому его рендеринг по умолчанию, вероятно, будет выполняться с использованием ToString(), а не отображаться.)

В качестве альтернативы вам может потребоваться создать новый элемент управления, например. MultipartTextBlock с привязываемым свойством PartsSource и TextBlock в качестве шаблона по умолчанию. Когда PartsSource был установлен, ваш элемент управления прикреплял обработчик событий CollectionChanged (напрямую или через CollectionChangedEventManager) и обновлял коллекцию TextBlock.Inlines из кода по мере изменения коллекции PartsSource.

В любом случае может потребоваться осторожность, если ваш код генерирует элементы Inline напрямую (поскольку Inline нельзя использовать в двух местах одновременно). В качестве альтернативы вы можете рассмотреть возможность предоставления абстрактной модели текста, шрифта и т. д. (то есть модели представления) и создания фактических объектов Inline с помощью файла DataTemplate. Это также может улучшить тестируемость, но, очевидно, добавляет сложности и усилий.

person itowlson    schedule 25.12.2009

Вы можете добавить свойство зависимости в подкласс TextBlock

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);
        }
    }
}
person Frank A.    schedule 03.03.2012
comment
TextBlock запечатан на Windows Phone 8 - person thumbmunkeys; 07.06.2013
comment
Миллион благодарностей за это. Мне пришлось внести пару незначительных изменений, чтобы заставить его работать. - person LawMan; 28.05.2015

Это альтернативное решение, которое использует поведение/присоединенные свойства WPF:

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

В своем XAML используйте его следующим образом:

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

Это избавляет вас от необходимости наследовать от TextBlock. Он также может работать с использованием ObservableCollection вместо IEnumerable, в этом случае вам нужно будет подписаться на изменения коллекции.

person B. Tossings    schedule 01.12.2017
comment
В моей WindowModel я определил public ObservableCollection‹Inline› ProcessTrackerInlines { get; набор; } и привязал его к TextBlockExtensions.BindableInlines={Binding ProcessTrackerInlines, Mode=OneWay}. Добавил метод loadProcessTracker в windowModel для заполнения ProcessTrackerInlines, и все работало нормально, но если я последний (нажав кнопку), попытаюсь добавить Inline и вызову PropertyChanged . Новая встроенная строка не отображается в элементе управления. Я могу добавить код, если нужно. - person El Bayames; 28.12.2020
comment
Работает как шарм.. - person NecroMancer; 17.04.2021

В версии 4 WPF вы сможете привязываться к объекту Run, что может решить вашу проблему.

В прошлом я решал эту проблему, переопределяя ItemsControl и отображая текст как элементы в ItemsControl. Взгляните на некоторые учебные пособия, подготовленные доктором WPF по подобным вещам: http://www.drwpf.com

person timothymcgrath    schedule 26.12.2009

Спасибо Фрэнк за ваше решение. Мне пришлось внести пару незначительных изменений, чтобы заставить его работать на меня.

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);
    }
}
person LawMan    schedule 28.05.2015
comment
Лучше добавить textBlock.Inlines.Clear() перед AddRange, чтобы сбросить встроенные строки при изменении - person lelimacon; 20.01.2017
comment
тогда как мне это использовать? извини я новенький - person ; 28.11.2019

Если я правильно понял ваше требование, вы можете вручную проверить поступающие сообщения, и для каждого сообщения вы можете добавить элемент в свойство TextBlock.Inlines. Он не будет принимать никаких DataBinding. Я сделал это со следующим:

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()));
}
person viky    schedule 25.12.2009
comment
Как выглядит XAML для приведенного выше примера? - person discorax; 26.10.2010

Предложение от Павла Ангиковского работает отлично. Здесь недостающая часть с привязкой данных в MVVM. Используйте свойство AddTrace в модели представления, чтобы добавить содержимое в OutputBlock в окне. Резервное свойство MyBindingPath в окне не требуется.

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()));
}
person Gerhard Schmeusser    schedule 11.12.2019

Все давали хорошие решения, но у меня была похожая проблема, и после нескольких часов поиска решений я решил попробовать напрямую привязаться к содержимому по умолчанию. Без свойств зависимостей. Извините за мой устаревший английский... хе-хе-хе

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

Хорошо, давайте применим это к вашему файлу xaml...

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

И вуаля!

Это связано с тем, что привязка встроенных строк не нужна, вы можете изменять части текста из другого содержимого элемента управления без привязки, это решение поможет мне.

person Vinicius    schedule 24.07.2020

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

Я думаю, вам может понадобиться дополнительный код в обработчике PropertyChanged, чтобы инициализировать textBlock.Inlines, если связанная коллекция уже имеет контент, и очистить любой существующий контекст.

person BadBiki    schedule 06.01.2013