【问题标题】:How to create context-aware ListBoxItem template?如何创建上下文感知 ListBoxItem 模板?
【发布时间】:2011-07-10 09:40:40
【问题描述】:

我想创建 XAML 聊天界面,该界面将根据其邻居以不同方式显示消息。这是一个例子:

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

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

【问题讨论】:

  • 不太清楚“取决于它的邻居”。你是什​​么意思 ?为什么不只传递模型视图/模型属性?

标签: c# .net wpf xaml


【解决方案1】:

如果您只是对标头的格式(完整或小)感兴趣,那么 ListBox/ListView/ItemsControlRelativeSource 绑定中带有 PreviousData 是要走的路(正如指出的那样动画)。

但是,由于您添加了希望支持跨多条消息进行选择,因此据我所知,这几乎排除了ItemsControl 和派生自它的类。您必须改用FlowDocument 之类的东西。

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

我会这样做

首先您在设计器中设计FlowDocument 的块,当您满意时将它们移动到您设置x:Shared="False" 的资源中。这将使您能够创建资源的多个实例,而不是一遍又一遍地使用同一个。然后您使用ObservableCollection 作为FlowDocument 的“源”并订阅CollectionChanged 事件,在事件处理程序中您将获得资源的新实例,检查您是否需要完整或小标题,然后然后将块添加到FlowDocument。您还可以为 Remove 等添加逻辑。

示例实现

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

这将使您能够在消息中选择您喜欢的文本

如果您使用 MVVM,您可以创建附加行为而不是背后的代码,我在这里做了一个类似场景的示例实现:Binding a list in a FlowDocument to List<MyClass>?

另外,FlowDocument 的 MSDN 页面也很有帮助:http://msdn.microsoft.com/en-us/library/aa970909.aspx

【讨论】:

  • 你不需要设置 x:Shared="False" 以确保每次都创建一个新的资源实例吗?
  • @crazyarabian: 完全正确:) blocksTemplate 资源具有 x:Shared="False" 以确保每次都创建一个新实例。因此,当触发 CollectionChanged 事件时,会创建一个新的 ArrayList 块
【解决方案2】:

假设您的 ItemTemplateStackPanelTextBlock 标头和 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; 
    } 

【讨论】:

    【解决方案3】:

    尝试给出提示类似的伪代码

    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 的类引用。以下是链接

    网络资源:

    Converter

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

    希望这会有所帮助。

    【讨论】:

    • 我知道可以为单个消息设置样式。我也知道如何为来自不同发件人的消息设置不同的样式。我不知道如何让来自同一发件人的两条相邻消息使用纤细的标头。
    • 嗯,实际上我可能不明白你的意思。但简而言之,我的帖子说:使用您的数据模型来定义它。不要依靠 XAML 来创造奇迹,因为在 ViewModel 或 Model 中更容易做到。我向 ypu 展示了如何在不同来源之间进行延迟,但没有什么可以阻止您向消息类添加新属性以定义更准确的关系/分组,并在使用 Converter 将其绑定到 UI 之后。
    【解决方案4】:

    我认为你不能纯粹通过 XAML 来做到这一点,你需要在某处编写代码来确定每条消息之间的关系,即消息 n - 1 的作者是否与 n 相同?

    我写了一个非常简单的示例,它产生了所需的输出。我的示例和生成的代码 sn-ps 绝不是生产级代码,但它至少应该为您指明正确的方向。

    首先,我首先创建了一个非常简单的对象来表示消息:

    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>
    

    同样,这个例子并不完美,可能只是众多可行的例子之一,但至少它应该为您指明正确的方向。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-28
      • 1970-01-01
      • 1970-01-01
      • 2019-04-12
      • 2018-12-22
      • 2013-09-17
      相关资源
      最近更新 更多