我的WPF应用程序从后端服务接收一系列消息,我需要在UI中显示这些消息。这些消息的内容各不相同,而我希望每个消息都有不同的视觉布局(例如字符串格式、颜色、字体、图标等)。
我希望能够为每条消息创建一个内联元素(例如Run、TextBlock、Italic等),然后以某种方式将它们全部放入ObservableCollection<>
中,并通过WPF数据绑定在UI中使用TextBlock.Inlines来展示。但我找不到如何实现这一点,这是否可能?
我的WPF应用程序从后端服务接收一系列消息,我需要在UI中显示这些消息。这些消息的内容各不相同,而我希望每个消息都有不同的视觉布局(例如字符串格式、颜色、字体、图标等)。
我希望能够为每条消息创建一个内联元素(例如Run、TextBlock、Italic等),然后以某种方式将它们全部放入ObservableCollection<>
中,并通过WPF数据绑定在UI中使用TextBlock.Inlines来展示。但我找不到如何实现这一点,这是否可能?
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);
}
}
}
这是不可能的,因为TextBlock.Inlines
属性不是依赖属性。只有依赖属性可以成为数据绑定的目标。
根据您确切的布局要求,您可以使用ItemsControl
,将其ItemsPanel
设置为WrapPanel
,并将其ItemsSource
设置为您的集合。(可能需要进行一些实验,因为Inline
不是UIElement
,所以它的默认呈现可能是使用ToString()
而不是显示。)
或者,您可能需要构建一个新控件,例如MultipartTextBlock
,它具有可绑定的PartsSource
属性和一个TextBlock
作为其默认模板。当设置PartsSource
时,您的控件将附加一个CollectionChanged
事件处理程序(直接或通过CollectionChangedEventManager),并在PartsSource
集合更改时从代码更新TextBlock.Inlines
集合。
在任何情况下,如果您的代码直接生成Inline
元素(因为Inline
无法同时在两个位置使用),则可能需要谨慎。您还可以考虑公开文本、字体等的抽象模型(即视图模型),并通过DataTemplate
创建实际的Inline
对象。这可能还可以提高可测试性,但显然会增加复杂性和工作量。
这是一种使用 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,在这种情况下,您需要订阅集合更改。
感谢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);
}
}
AddRange
操作之前最好先加上 textBlock.Inlines.Clear()
,以便在更改时重置 inlines。 - lelimacon在WPF 4版本中,您将能够绑定到一个Run对象,这可能会解决您的问题。
我过去曾通过覆盖ItemsControl并将文本显示为ItemsControl中的项来解决此问题。请查看Dr. WPF在这类问题上所做的一些教程:http://www.drwpf.com
如果我正确理解了您的需求,您可以手动检查即将到来的消息,并为每个消息添加一个元素到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()));
}
最近我有一个类似的任务要解决,即:在自定义消息框文本内容中插入无限数量的url链接,并将其与此文本绑定。我决定在这里发布我的实现,看到这个主题有一些不同的好主意...这是我的解决方案:
概念: xaml TextBlock内容的流程:
<TextBlock>
...
<Inline>
<Hyperlink <Inline>>
<Inline>
<Hyperlink <Inline>>
...
"...一些文本在这里...[链接文本|链接地址]...一些其他文本在这里...等等."
示例:
"请访问 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 那样。"
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 ☀."
/// </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);
}
}
Inside the view constructor:
this.DataContextChanged += MessageBoxView_DataContextChanged;
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!");
}
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()));
}
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,并清除任何现有的上下文。
[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>
完成了!
这是因为绑定内联元素是不必要的,你可以修改另一个控件内容中的文本部分而无需进行绑定。这个解决方案对我很有帮助。
<ItemsControl>
中使用这个子类时,我的绑定的内联元素一直显示为空。就像@LawMan已经提到的那样,在这种情况下,必须调整OnPropertyChanged
方法,例如像这里所示。 - SourceSeeker