您可以查看FlowDocument。此类可用于自定义块(段落)的外观,类似于ItemsControl,它也可以包含 UI 控件(以备不时之需)。当然,文本选择将适用于整个文档。
很遗憾,FlowDocument 不支持绑定,因此您必须为此编写一些代码。
让我举个例子。您可以使用 System.Windows.Interactivity 命名空间中的 Behavior 为 FlowDocument 类创建可重用的功能扩展。
这是你可以开始的:
<FlowDocumentScrollViewer>
<FlowDocument ColumnWidth="400">
<i:Interaction.Behaviors>
<myApp:ChatFlowDocumentBehavior Messages="{Binding Messages}">
<myApp:ChatFlowDocumentBehavior.ItemTemplate>
<DataTemplate>
<myApp:Fragment>
<Paragraph Background="Aqua" BorderBrush="BlueViolet" BorderThickness="1"/>
</myApp:Fragment>
</DataTemplate>
</myApp:ChatFlowDocumentBehavior.ItemTemplate>
</myApp:ChatFlowDocumentBehavior>
</i:Interaction.Behaviors>
</FlowDocument>
</FlowDocumentScrollViewer>
(i 命名空间是xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity")
所以我们的ChatFlowDocumentBehavior 有一个可绑定的Messages 属性用于显示聊天消息。此外,还有一个 ItemTemplate 属性,您可以在其中定义单个聊天消息的外观。
注意Fragment 类。这只是一个简单的包装器(下面的代码)。 DataTemplate 类不会接受 Paragraph 作为其内容,但我们需要我们的项目是 Paragraphs。
您可以根据需要配置Paragraph(例如颜色、字体,可能还有其他子项或控件等)
所以,Fragment 类是一个简单的包装器:
[ContentProperty("Content")]
sealed class Fragment : FrameworkElement
{
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
nameof(Content),
typeof(FrameworkContentElement),
typeof(Fragment));
public FrameworkContentElement Content
{
get => (FrameworkContentElement)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
}
行为类的代码有点多,但并不复杂。
sealed class ChatFlowDocumentBehavior : Behavior<FlowDocument>
{
// This is our dependency property for the messages
public static readonly DependencyProperty MessagesProperty =
DependencyProperty.Register(
nameof(Messages),
typeof(ObservableCollection<string>),
typeof(ChatFlowDocumentBehavior),
new PropertyMetadata(defaultValue: null, MessagesChanged));
public ObservableCollection<string> Messages
{
get => (ObservableCollection<string>)GetValue(MessagesProperty);
set => SetValue(MessagesProperty, value);
}
// This defines how our items will look like
public DataTemplate ItemTemplate { get; set; }
// This method will be called by the framework when the behavior attaches to flow document
protected override void OnAttached()
{
RefreshMessages();
}
private static void MessagesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ChatFlowDocumentBehavior b))
{
return;
}
if (e.OldValue is ObservableCollection<string> oldValue)
{
oldValue.CollectionChanged -= b.MessagesCollectionChanged;
}
if (e.NewValue is ObservableCollection<string> newValue)
{
newValue.CollectionChanged += b.MessagesCollectionChanged;
}
// When the binding engine updates the dependency property value,
// update the flow doocument
b.RefreshMessages();
}
private void MessagesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddNewItems(e.NewItems.OfType<string>());
break;
case NotifyCollectionChangedAction.Reset:
AssociatedObject.Blocks.Clear();
break;
}
}
private void RefreshMessages()
{
if (AssociatedObject == null)
{
return;
}
AssociatedObject.Blocks.Clear();
if (Messages == null)
{
return;
}
AddNewItems(Messages);
}
private void AddNewItems(IEnumerable<string> items)
{
foreach (var message in items)
{
// If the template was provided, create an instance from the template;
// otherwise, create a default non-styled paragraph instance
var newItem = (Paragraph)(ItemTemplate?.LoadContent() as Fragment)?.Content ?? new Paragraph();
// This inserts the message text directly into the paragraph as an inline item.
// You might want to change this logic.
newItem.Inlines.Add(message);
AssociatedObject.Blocks.Add(newItem);
}
}
}
以此为起点,您可以扩展行为以满足您的需求。例如。添加用于删除或重新排序消息的事件处理逻辑,实现全面的消息模板等。
几乎总是可以用尽可能少的代码实现功能,使用 XAML 功能:样式、模板、资源等。但是,对于缺少的功能,您只需要回退到代码。但在这种情况下,请始终尽量避免视图中的代码隐藏。为此创建Behaviors 或附加属性。