如何创建上下文感知的ListBoxItem模板?

时间:2011-07-10 09:40:41

标签: c# .net wpf xaml

我想创建XAML聊天界面,根据它的邻居显示不同的消息。这是一个例子:

enter image description here

我认为ListBox控件最适合这个。我也在考虑不同的控件,例如FlowDocumentReader,但我从未使用它们。另外我需要提一下,消息的文本应该是可选的(跨多个消息),我不知道如何使用ListBox实现这一点。

更新:主要的一点是,如果一方(在这种情况下是维京)连续发送一些消息,接口应该连接那些(使用超薄消息头而不是完整消息头)。因此,带有标题的消息外观取决于先前的消息是否由同一个人发送。

4 个答案:

答案 0 :(得分:1)

假设您的ItemTemplateStackPanel TextBlock标题和TextBlock消息,您可以使用MultiBinding可见性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; 
    } 

答案 1 :(得分:1)

如果您只对标题的格式感兴趣(完整或小),那么ListBox绑定中包含PreviousData的ListView / ItemsControl / RelativeSource就是要走的路(正如 anivas 所指出的那样)。

但是既然你补充说你想支持跨多个消息的选择,那么就我所知,这几乎排除了ItemsControl以及从它派生的类。您将不得不使用类似FlowDocument的内容。

不幸的是FlowDocument没有ItemsSource属性。有一些解决方法的例子,比如Create Flexible UIs With Flow Documents And Data Binding但是这个实现几乎让我的VS2010崩溃了(我没有调查它的原因,可能是一个简单的修复)。

我会这样做

首先,您在设计器中设计FlowDocument的块,当您满意时,将它们移动到您设置x:Shared="False"的资源。这将使您能够创建资源的多个实例,而不是反复使用相同的实例。然后使用ObservableCollection作为FlowDocument的“源”并订阅CollectionChanged事件,并在eventhandler中获取资源的新实例,检查是否需要完整或小标题,然后将块添加到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,您可以创建附加行为而不是后面的代码,我在此处制作了类似方案的示例实现:Binding a list in a FlowDocument to List<MyClass>?

此外,FlowDocument的MSDN页面非常有用:http://msdn.microsoft.com/en-us/library/aa970909.aspx

答案 2 :(得分:0)

尝试提示伪代码

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中的yor bindind代码中使用XAML属性Converter 您可以在哪里指定实现IValueConverter的类引用。这是链接

网上资源:

Converter

您可以在UI / ModelView之间转换类型。

希望这有帮助。

答案 3 :(得分:0)

我认为你不能完全通过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>

同样,这个例子并不完美,可能只是其中一个有效,但至少它应该指向正确的方向。