Как создать контекстно-зависимый шаблон ListBoxItem? - PullRequest
1 голос
/ 10 июля 2011

Я хочу создать интерфейс чата XAML, который будет отображать сообщения по-разному в зависимости от соседей.Вот пример:

enter image description here

Я думаю, что элемент управления ListBox наиболее подходит для этого.Я также думаю о различных элементах управления, таких как FlowDocumentReader, но я никогда не использовал их.Также я должен упомянуть, что текст сообщения должен быть доступен для выбора (между несколькими сообщениями), и я не знаю, как этого добиться с помощью ListBox.

Обновление: Главное, чтоодна сторона (в данном случае викинг) отправляет несколько сообщений подряд, интерфейс должен объединять их (используйте тонкий заголовок сообщения вместо полного).Таким образом, внешний вид сообщения с заголовком зависит от того, было ли предыдущее сообщение отправлено тем же лицом.

Ответы [ 4 ]

1 голос
/ 15 июля 2011

Если вас интересовало только форматирование заголовков (полное или маленькое), тогда ListBox / ListView / ItemsControl с PreviousData в привязке RelativeSource - это путь, на который указывает anivas ).

Но так как вы добавили, что хотите поддерживать выбор между несколькими сообщениями, то это в значительной степени исключает ItemsControl и классы, которые вытекают из него, насколько я знаю. Вместо этого вам придется использовать что-то вроде FlowDocument.

К сожалению, FlowDocument не обладает свойством ItemsSource. Для этого есть примеры обходных путей, например Создание гибких пользовательских интерфейсов с документооборотом и привязкой данных , но эта реализация во многом делает мой сбой VS2010 (я не исследовал причину этого, может быть легко исправить) .

Вот как бы я это сделал

Сначала вы проектируете блоки FlowDocument в конструкторе, а когда вы удовлетворены, вы перемещаете их в ресурс, где вы устанавливаете x:Shared="False". Это позволит вам создавать несколько экземпляров ресурса, а не использовать один и тот же снова и снова. Затем вы используете ObservableCollection в качестве «источника» для FlowDocument и подписываетесь на событие CollectionChanged, а в обработчике событий вы получаете новый экземпляр ресурса, проверяете, хотите ли вы полный или маленький заголовок, и затем добавьте блоки к FlowDocument. Вы также можете добавить логику для удаления и т. Д.

Пример реализации

<!-- xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" -->

<Window.Resources>
    <Collections:ArrayList x:Key="blocksTemplate" x:Shared="False">
        <!-- Full Header -->
        <Paragraph Name="fullHeader" Margin="5" BorderBrush="LightGray" BorderThickness="1" TextAlignment="Right">
            <Figure HorizontalAnchor="ColumnLeft" BaselineAlignment="Center" Padding="0" Margin="0">
                <Paragraph>
                    <Run Text="{Binding Sender}"/>
                </Paragraph>
            </Figure>
            <Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>
        </Paragraph>
        <!-- Small Header -->
        <Paragraph Name="smallHeader" Margin="5" TextAlignment="Right">
            <Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>          
        </Paragraph>
        <!-- Message -->
        <Paragraph Margin="5">
            <Run Text="{Binding Message}"/>
        </Paragraph>
    </Collections:ArrayList>
</Window.Resources>
<Grid>
    <FlowDocumentScrollViewer>
        <FlowDocument Name="flowDocument"
                      FontSize="14" FontFamily="Georgia"/>
    </FlowDocumentScrollViewer>
</Grid>

И код может быть следующим:

public ObservableCollection<ChatMessage> ChatMessages
{
    get;
    set;
}

public MainWindow()
{
    InitializeComponent();
    ChatMessages = new ObservableCollection<ChatMessage>();
    ChatMessages.CollectionChanged += ChatMessages_CollectionChanged;
}

void ChatMessages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    ArrayList itemTemplate = flowDocument.TryFindResource("blocksTemplate") as ArrayList;
    if (e.Action == NotifyCollectionChangedAction.Add)
    {
        foreach (ChatMessage chatMessage in e.NewItems)
        {
            foreach (Block block in itemTemplate)
            {
                bool addBlock = true;
                int index = ChatMessages.IndexOf(chatMessage);
                if (block.Name == "fullHeader" &&
                    (index > 0 && ChatMessages[index].Sender == ChatMessages[index - 1].Sender))
                {
                    addBlock = false;
                }
                else if (block.Name == "smallHeader" &&
                         (index == 0 || ChatMessages[index].Sender != ChatMessages[index - 1].Sender))
                {
                    addBlock = false;
                }
                if (addBlock == true)
                {
                    block.DataContext = chatMessage;
                    flowDocument.Blocks.Add(block);
                }
            }
        }
    }
}

И в моем примере ChatMessage это просто

public class ChatMessage
{
    public string Sender
    {
        get;
        set;
    }
    public string Message
    {
        get;
        set;
    }
    public DateTime TimeSent
    {
        get;
        set;
    }
}

Это позволит вам выбрать текст в сообщениях, которые вам нравятся

enter image description here

Если вы используете MVVM, вы можете создать прикрепленное поведение вместо кода позади, я сделал пример реализации аналогичного сценария здесь: Привязка списка в FlowDocument к списку ?

Также очень полезна страница MSDN для FlowDocument: http://msdn.microsoft.com/en-us/library/aa970909.aspx

1 голос
/ 13 июля 2011

Предполагая, что ItemTemplate представляет собой StackPanel из TextBlock заголовка и TextBlock сообщения, вы можете использовать MultiBinding Visibility Converter, чтобы скрыть заголовок как:

<TextBlock Text="{Binding UserName}">  
   <TextBlock.Visibility> 
       <MultiBinding Converter="{StaticResource headerVisibilityConverter}"> 
       <Binding RelativeSource="{RelativeSource PreviousData}"/> 
       <Binding/> 
    </MultiBinding>                             
   </TextBlock.Visibility> 
</TextBlock> 

И логика IMultiValueConverter выглядит примерно так:

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 
    { 
        var previousMessage = values[0] as MessageItem; 
        var currentMessage = values[1] as MessageItem; 
        if ((previousMessage != null) && (currentMessage != null)) 
        { 
            return previousMessage.UserName.Equals(currentMessage.UserName) ? Visibility.Hidden : Visibility.Visible; 
        }           

        return Visibility.Visible; 
    } 
0 голосов
/ 15 июля 2011

Я не думаю, что вы можете сделать это исключительно через 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>

Опять же, этот пример не идеален и, вероятно, является лишь одним из многих, которые работают, но, по крайней мере, он должен указать вам правильное направление.

0 голосов
/ 10 июля 2011

Попробуйте дать подсказку псевдокод, подобный :

public abstract class Message {/*Implementation*/

      public enum MessageTypeEnum {Client, Viking, None};          

      public abstract MessageTypeEnum MessageType {get;}   
}

public class ClientMessage : Message {

      /*Client message concrete implementation.*/
       public override MessageTypeEnum MessageType 
       {
           get { 
               return MessageTypeEnum.Client;
           } 
       }
}

public class VikingMessage : Message 
{     
       / *Viking message concrete implementation*/
       public override MessageTypeEnum MessageType 
       {
           get { 
               return MessageTypeEnum.Viking;
           } 
       }

}

После этого в вашем коде привязки в XAML для управления связыванием используйте атрибут XAML Converter Где можно назначить ссылку на класскоторый реализует IValueConverter.Вот ссылки

Ресурс в сети:

Конвертер

Там вы можете конвертировать тип между вашим UI / ModelView.

Надеюсь, это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...