Я не думаю, что вы можете сделать это исключительно через XAML, вам понадобится где-нибудь написать код, чтобы определить связь между каждым сообщением, т. Е. Является ли автор сообщения n - 1 тем же, что и n?
Я написал очень быстрый пример, который привел к желаемому результату.Мой пример и полученные фрагменты кода никоим образом не являются кодом производственного уровня, но он должен, по крайней мере, указывать вам правильное направление.
Для начала я сначала создал очень простой объект для представления сообщений:
public class ChatMessage
{
public String Username { get; set; }
public String Message { get; set; }
public DateTime TimeStamp { get; set; }
public Boolean IsConcatenated { get; set; }
}
Далее я извлек коллекцию из ObservableCollection для обработки определения отношений между каждым сообщением по мере его добавления:
public class ChatMessageCollection : ObservableCollection<ChatMessage>
{
protected override void InsertItem(int index, ChatMessage item)
{
if (index > 0)
item.IsConcatenated = (this[index - 1].Username == item.Username);
base.InsertItem(index, item);
}
}
Теперь эта коллекция может быть открыта вашим ViewModel и привязана к ListBoxна ваш взгляд.
Существует много способов отображения шаблонных элементов в XAML.Исходя из вашего примера интерфейса, единственным аспектом каждого изменения элемента является заголовок, поэтому я решил, что он наиболее послал, чтобы каждый ListBoxItem отображал HeaderedContentControl, который будет показывать правильный заголовок на основе значения IsConcatenated:
<ListBox ItemsSource="{Binding Path=Messages}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type m:ChatMessage}">
<HeaderedContentControl Header="{Binding}">
<HeaderedContentControl.HeaderTemplateSelector>
<m:ChatHeaderTemplateSelector />
</HeaderedContentControl.HeaderTemplateSelector>
<Label Content="{Binding Path=Message}" />
</HeaderedContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Вы заметите, что я указываю HeaderTemplateSelector, который отвечает за выбор между одним из двух шаблонов заголовков:
public sealed class ChatHeaderTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var chatItem = item as ChatMessage;
if (chatItem.IsConcatenated)
return ((FrameworkElement)container).FindResource("CompactHeader") as DataTemplate;
return ((FrameworkElement)container).FindResource("FullHeader") as DataTemplate;
}
}
И, наконец, вот два шаблона заголовков, которые определены как ресурсы представления:
<DataTemplate x:Key="FullHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="{Binding Path=Username}" />
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="CompactHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Right"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
Опять же, этот пример не идеален и, вероятно, является лишь одним из многих, которые работают, но, по крайней мере, он должен указать вам правильное направление.