将数据绑定到TextBlock.Inlines

37

我的WPF应用程序从后端服务接收一系列消息,我需要在UI中显示这些消息。这些消息的内容各不相同,而我希望每个消息都有不同的视觉布局(例如字符串格式、颜色、字体、图标等)。

我希望能够为每条消息创建一个内联元素(例如Run、TextBlock、Italic等),然后以某种方式将它们全部放入ObservableCollection<> 中,并通过WPF数据绑定在UI中使用TextBlock.Inlines来展示。但我找不到如何实现这一点,这是否可能?

10个回答

15

你可以给一个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);
        }
    }
}

1
TextBlock 在 Windows Phone 8 上是封闭的。 - thumbmunkeys
1
非常感谢您。我只需要做一些小的更改就可以让它正常工作了。 - LawMan
在这行代码:this.Inlines.Add...上,出现了InvalidOperation的结果:"调用线程无法访问此对象,因为不同的线程拥有它"。因此,这不是一个真正的解决方案。 - Defd
@Defd 使用主线程的调度程序 - Trương Quốc Khánh
<ItemsControl>中使用这个子类时,我的绑定的内联元素一直显示为空。就像@LawMan已经提到的那样,在这种情况下,必须调整OnPropertyChanged方法,例如像这里所示。 - SourceSeeker
我正在寻找解决这个相同问题的方法,因为我想将大量处理从在UI调度程序上运行的循环中移出。数据绑定到文本块,包括一些运行以控制字体颜色,是一个合理的解决方案。必须在UI线程上调度添加操作完全违背了初衷。 - undefined

14

这是不可能的,因为TextBlock.Inlines属性不是依赖属性。只有依赖属性可以成为数据绑定的目标。

根据您确切的布局要求,您可以使用ItemsControl,将其ItemsPanel设置为WrapPanel,并将其ItemsSource设置为您的集合。(可能需要进行一些实验,因为Inline不是UIElement,所以它的默认呈现可能是使用ToString()而不是显示。)

或者,您可能需要构建一个新控件,例如MultipartTextBlock,它具有可绑定的PartsSource属性和一个TextBlock作为其默认模板。当设置PartsSource时,您的控件将附加一个CollectionChanged事件处理程序(直接或通过CollectionChangedEventManager),并在PartsSource集合更改时从代码更新TextBlock.Inlines集合。

在任何情况下,如果您的代码直接生成Inline元素(因为Inline无法同时在两个位置使用),则可能需要谨慎。您还可以考虑公开文本、字体等的抽象模型(即视图模型),并通过DataTemplate创建实际的Inline对象。这可能还可以提高可测试性,但显然会增加复杂性和工作量。


13

这是一种使用 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,在这种情况下,您需要订阅集合更改。


在我的 WindowModel 中,我定义了公共的 ObservableCollection<Inline> ProcessTrackerInlines { get; set; } 并将其绑定到 TextBlockExtensions.BindableInlines="{Binding ProcessTrackerInlines, Mode=OneWay}"。我添加了一个名为 loadProcessTracker 的方法到 windowModel 中来填充 ProcessTrackerInlines,一切都运行良好,但是如果我稍后(通过点击按钮)尝试添加一个 Inline,并调用 PropertyChanged,则新的 inline 不会显示在控件上。如果需要,我可以提供代码。 - El Bayames

6

感谢Frank提供的解决方案。我需要做一些小修改才能让它适用于我的情况。

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

5
在进行 AddRange 操作之前最好先加上 textBlock.Inlines.Clear(),以便在更改时重置 inlines。 - lelimacon
那我该怎么使用它?抱歉,我是新手。 - user11960582

6

在WPF 4版本中,您将能够绑定到一个Run对象,这可能会解决您的问题。

我过去曾通过覆盖ItemsControl并将文本显示为ItemsControl中的项来解决此问题。请查看Dr. WPF在这类问题上所做的一些教程:http://www.drwpf.com


4

如果我正确理解了您的需求,您可以手动检查即将到来的消息,并为每个消息添加一个元素到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()));
}

10
你能提供一下上面示例的XAML代码吗? - discorax

1

最近我有一个类似的任务要解决,即:在自定义消息框文本内容中插入无限数量的url链接,并将其与此文本绑定。我决定在这里发布我的实现,看到这个主题有一些不同的好主意...这是我的解决方案:

概念: xaml TextBlock内容的流程:

<TextBlock>
  ...
  <Inline>
  <Hyperlink <Inline>>
  <Inline>
  <Hyperlink <Inline>>
  ...
  1. 我的 x:Name=MixedText TextBlock 元素接收其值作为单个文本格式,如下所示:

"...一些文本在这里...[链接文本|链接地址]...一些其他文本在这里...等等."

示例:

"请访问 Microsoft [网站|https://www.microsoft.com/en-us/windows/windows-7-end-of-life-support-information],下载 Windows 7 SP1,完成 SP1 安装,然后再次运行安装程序。去 [roblox|https://www.roblox.com] 网站放松一下,就像我儿子 \u263A 那样。"

  1. 我在DataContextChanged事件中进行解析和所有元素的注入到我的MixedText TextBlock 元素中。

xaml部分:定义绑定路径(MixedText)。 ...

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

ViewModel 部分:定义绑定路径属性。

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

我实现了MultipartTextHandler类,其中包括MixedText解析和动态xaml注入模型准备。

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

代码后台的东西。
  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);
         }
     }
    

以下是我的控制台应用程序的使用方法

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

实际对话展示视图: enter image description here

1
Pavel Anhikouski的建议非常有效。以下是在MVVM中使用数据绑定的缺失部分,使用AddTrace属性在ViewModel中添加内容到窗口中的OutputBlock。 窗口中备份属性MyBindingPath不需要。
视图模型:
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()));
}

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

我认为你可能需要在PropertyChanged处理程序上添加一些额外的代码,以便在绑定的集合已经有内容时初始化textBlock.Inlines,并清除任何现有的上下文。


0
每个人都给出了好的解决方案,但我遇到了类似的问题,在寻找解决方案数小时后,我决定直接绑定到默认内容,而不使用依赖属性。 抱歉我的英语陈旧... 哈哈哈
[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>

完成了!

这是因为绑定内联元素是不必要的,你可以修改另一个控件内容中的文本部分而无需进行绑定。这个解决方案对我很有帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接