Привязка данных к TextBlock.Inlines - PullRequest
27 голосов
/ 25 декабря 2009

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

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

Ответы [ 7 ]

13 голосов
/ 03 марта 2012

Вы можете добавить свойство зависимости в подкласс 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);
        }
    }
}
11 голосов
/ 25 декабря 2009

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

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

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

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

6 голосов
/ 26 декабря 2009

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

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

4 голосов
/ 25 декабря 2009

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

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()));
    }
3 голосов
/ 01 декабря 2017

Я понимаю, что этот вопрос очень старый, но я все равно решил поделиться альтернативным решением. Он использует поведения 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 , в этом случае вам нужно будет подписаться на изменения коллекции.

3 голосов
/ 28 мая 2015

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

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);
    }
}
0 голосов
/ 07 января 2013
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, если в связанной коллекции уже есть контент, и очистить любой существующий контекст.

...