开发者

How to create context-aware ListBoxItem template?

开发者 https://www.devze.com 2023-03-18 23:39 出处:网络
I want to create XAML chat interface that will display messages differently depending on it\'s neighbours. Here\'s an example:

I want to create XAML chat interface that will display messages differently depending on it's neighbours. Here's an example:

How to create context-aware ListBoxItem template?

I think ListBox control is most suit开发者_运维百科able for this. I'm also thinking about different controls such as FlowDocumentReader but I've never used them. Also I need to mention that message's text should be selectable (across multiple messages) and I don't know how to achieve this with ListBox.

Update: The main point there is that if one side (viking in this case) send some messages in a row, the interface should concatenate those (use slim message header instead of full one). So, the look of message with header depends on whether previous message was sent by the same person.


If you were just interested in the formatting of the Headers (full or small) then a ListBox/ListView/ItemsControl with PreviousData in the RelativeSource binding is the way to go (as pointed out by anivas).

But since you added that you wanted to support for selection across multiple messages then this pretty much rules out ItemsControl and the classes that derives from it as far as I know. You'll have to use something like a FlowDocument instead.

Unfortunately FlowDocument doesn't have the ItemsSource property. There are examples of workarounds for this, like Create Flexible UIs With Flow Documents And Data Binding but this implementation pretty much makes my VS2010 crash (I didn't investigate the reason for this, might be an easy fix).

Here is how I would do it

First you design the Blocks of the FlowDocument in the designer and when you're satisfied you move them to a resource where you set x:Shared="False". This will enable you to create multiple instances of the resource instead of using the same one over and over. Then you use an ObservableCollection as the "source" for the FlowDocument and subscribe to the CollectionChanged event, and in the eventhandler you get a new instance of the resource, check if you want the full or small header, and then add the blocks to the FlowDocument. You could also add logic for Remove etc.

Example implementation

<!-- 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>

And the code behind could be along the following lines

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

And in my sample, ChatMessage is just

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

This will enable you to select text however you like in the messages

How to create context-aware ListBoxItem template?

If you're using MVVM you can create an attached behavior instead of the code behind, I made a sample implementation of a similar scenario here: Binding a list in a FlowDocument to List<MyClass>?

Also, the MSDN page for FlowDocument is very helpful: http://msdn.microsoft.com/en-us/library/aa970909.aspx


Assuming your ItemTemplate is a StackPanel of TextBlock header and TextBlock message you can use a MultiBinding Visibility Converter to hide the header as:

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

And the IMultiValueConverter logic goes something like:

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


Try to give a hint pseudocode like:

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

}

After this in yor bindind code in XAML on binding control use XAML attribute Converter Where you can assign a class reference which implements IValueConverter. Here are the links

Resource on web:

Converter

There you can converts the type between your UI/ModelView.

Hope this helps.


I don't think you can do this purely through XAML, you're going to need to have code written somewhere to determine the relationship between each message, i.e., is the the author of message n - 1 the same as n?

I wrote a very quick example that resulted in the desired output. My example and the resulting code snippets are in no way production level code, but it should at least point you in the right direction.

To start, I first created a very simple object to represent the messages:

public class ChatMessage
{
  public String Username { get; set; }
  public String Message { get;  set; }
  public DateTime TimeStamp { get; set; }
  public Boolean IsConcatenated { get; set; }
}

Next I derived a collection from ObservableCollection to handle determining relationships between each message as they're added:

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

This collection can now be exposed by your ViewModel and bound to the ListBox in your view.

There are many ways to display templated items in XAML. Based on your example interface, the only aspect of each item changing is the header so I figured it made the most sent to have each ListBoxItem display a HeaderedContentControl that would show the correct header based on the IsConcatenated value:

<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>

You'll notice that I am specifying a HeaderTemplateSelector which is responsible for choosing between one of two header templates:

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

And finally, here are the two header templates which are defined as resources of the view:

<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>

Again, this example is not perfect and is probably just one of many that works, but at least it should point you in the right direction.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号