在WPF的itemscontrol中,如何对最后一项使用不同的模板?

24
我在使用自定义模板来展示我的ItemsControl中的结果。
item 1, item 2, item3,
我想改变最后一个元素的模板,以便结果变为:
item 1, item2, item3

ItemsControl:

<ItemsControl ItemsSource="{Binding Path=MyCollection}">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>

            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Path=Name}"/>
                <TextBlock Text=", "/>
            </StackPanel>

        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>

有没有人能够给出解决我的问题的方案?谢谢!

5个回答

67

我已经通过仅使用XAML找到了解决我的问题的方法。如果有人需要做相同的事情,请使用以下代码:

<ItemsControl ItemsSource="{Binding Path=MyCollection}">

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemTemplate>
        <DataTemplate>

            <StackPanel Orientation="Horizontal">
                <TextBlock x:Name="comma" Text=", "/>
                <TextBlock Text="{Binding}"/>
            </StackPanel>

            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
                    <Setter TargetName="comma" Property="Visibility" Value="Collapsed"/>
                </DataTrigger>
            </DataTemplate.Triggers>

        </DataTemplate>
    </ItemsControl.ItemTemplate>

</ItemsControl>

4
+1 很简单明了的解决方案。然而需要注意,如果对数据绑定集合进行任何更改(如排序、筛选、添加或删除),DataTrigger 将不会再次触发,也不会相应地更新 DataTemplate - Sheridan
39
对于那些来到这里寻找基于最初问题的答案(和其他错误链接至此的类似问题),请注意,这不是你正在寻找的内容。该解决方案更改的是第一个项目的模板,而不是最后一个项目的模板。 - jpwkeeper
jpwkeeper说得很对,但这是一个非常棒的解决方案来解决我的问题! - EightyOne Unite
3
请注意,对于具有大量项目的控件,PreviousData的速度非常慢,在1000个项目上使用它是不可行的。 - Giedrius
1
这里展示了使用PreviousData的另一种替代方法:https://dev59.com/1HE85IYBdhLWcg3w_I38#34138980 - 这个答案使用AlternationIndex。 - Skully
这个解决方案只改变了第一个项目的模板,而且当有更多项目时性能较差。不过对我来说,这个解决方案还是不错的,既适用于第一个项目,也适用于最后一个项目。 https://dev59.com/mWsz5IYBdhLWcg3wn5Xa#76627293 - Gowshik

6
您可以使用DataTemplateSelector,在SelectTemplate()方法中,您可以检查项目是否为最后一个,然后返回另一个模板。
在XAML中:
<ItemsControl.ItemTemplate>     
  <DataTemplate>
      <ContentPresenter 
             ContentTemplateSelector = "{StaticResource MyTemplateSelector}">

在代码后台中:
 private sealed class MyTemplateSelector: DataTemplateSelector
 { 

    public override DataTemplate SelectTemplate(
                                      object item, 
                                      DependencyObject container)
    {
        // ...
    }
  }

2
通过提供的参数(item和container),我无法确定该项是否是集合中的最后一项。 - Bram W.
您可以使用“FindParentOfType<TParent>”方法(Google Intrawebs)查找父级ItemsControl容器,然后使用AlternationIndex属性。请参阅StackOverflow上的示例。 - sll

6
这个解决方案会影响最后一行,并根据基础集合的更改进行更新:

CodeBehind

转换器需要三个参数才能正常运行-当前项、项控件、项计数,并在当前项也是最后一项时返回true:

  class LastItemConverter : IMultiValueConverter
    {

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            int count = (int)values[2];

            if (values != null && values.Length == 3 && count>0)
            {
                System.Windows.Controls.ItemsControl itemsControl = values[0] as System.Windows.Controls.ItemsControl;
                var itemContext = (values[1] as System.Windows.Controls.ContentPresenter).DataContext;
            
                var lastItem = itemsControl.Items[count-1];

                return Equals(lastItem, itemContext);
            }

            return DependencyProperty.UnsetValue;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

XAML

数据触发器用于包含名为“PART_TextBox”的文本框的 DataTemplate:

  <DataTemplate.Triggers>
            <DataTrigger Value="True" >
                <DataTrigger.Binding>
                    <MultiBinding Converter="{StaticResource LastItemConverter}">
                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
                        <Binding RelativeSource="{RelativeSource Self}"/>
                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count"/>
                    </MultiBinding>
                </DataTrigger.Binding>
                <Setter Property="Foreground" TargetName="PART_TextBox" Value="Red" />
            </DataTrigger>
 </DataTemplate.Triggers>      

Xaml中的转换器作为静态资源。

<Window.Resources>
     <local:LastItemConverter x:Key="LastItemConverter" />
</Window.Resources>

快照

下方是它运行时的截图:

enter image description here此代码已添加到“codeproject”网站的“itemscontrol”中。 https://www.codeproject.com/Articles/242628/A-Simple-Cross-Button-for-WPF

请注意最后一项文本为红色。


0
你可以使用转换器来找到这种集合控件的第一个或最后一个项目,它的效果非常好。 在我的代码中它运行得非常顺畅。
我编写了一个可以用于ItemsControl、ListView和ListBox的First/Last项目转换器。
public class FirstItemConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length <= 1 || values[1] is null)
        {
            return false;
        }

        if (values[0] is ItemsControl itemsControl)
        {
            return Equals(itemsControl.Items[0], values[1]);
        }

        if (values[0] is ListBox listBox)
        {
            return Equals(listBox.Items[0], values[1]);
        }

        if (values[0] is ListBox listView)
        {
            return Equals(listView.Items[0], values[1]);
        }

        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

public class LastItemConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length <= 1 || values[1] is null)
        {
            return false;
        }

        if (values[0] is ItemsControl itemsControl)
        {
            return Equals(itemsControl.Items[^1], values[1]);
        }

        if (values[0] is ListBox listBox)
        {
            return Equals(listBox.Items[^1], values[1]);
        }

        if (values[0] is ListBox listView)
        {
            return Equals(listView.Items[^1], values[1]);
        }

        return false;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

在你的XAML中,你可以像这样使用它来显示/隐藏你想要的控件。
<ItemsControl ItemsSource="{Binding YourItemCollection}">
                <ItemsControl.Resources>
                    <converters:FirstItemConverter x:Key="firstItemConverter" />
                    <converters:LastItemConverter x:Key="lastItemConverter" />
                </ItemsControl.Resources>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Button>
                                <!--Icon button-->
                                <Button.Style>
                                    <Style TargetType="Button">
                                        <Style.Triggers>
                                            <DataTrigger Value="True">
                                                <DataTrigger.Binding>
                                                    <MultiBinding Converter="{StaticResource firstItemConverter}">
                                                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
                                                        <Binding />
                                                    </MultiBinding>
                                                </DataTrigger.Binding>
                                                <Setter Property="Visibility" Value="Visible" />
                                            </DataTrigger>
                                            <DataTrigger Value="True">
                                                <DataTrigger.Binding>
                                                    <MultiBinding Converter="{StaticResource lastItemConverter}">
                                                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
                                                        <Binding />
                                                    </MultiBinding>
                                                </DataTrigger.Binding>
                                                <Setter Property="Visibility" Value="Visible" />
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Button.Style>
                            </Button>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

-1
一个问题...我看到你正在使用ItemsControl而不是ListBox,它似乎绑定到一个字符串集合,并且你只想显示结果文本而不格式化各个部分,这让我想知道你所需的输出实际上是否就是问题中提到的字符串本身,而不是真正的ItemsControl
如果我的理解是正确的,你考虑过只是使用一个简单的TextBlock绑定到项目集合,但通过转换器进行处理吗?然后在转换器内部,你将把value强制转换为一个字符串数组,然后在Convert方法中,只需使用逗号作为分隔符Join它们,这将自动仅在元素之间添加它们,如下所示...
var strings = (IEnumerable<String>)value;

return String.Join(", ", strings);

我也同意这个(或类似的)解决方案是对OP实际使用情况最好的解决方案,因为仅仅使用ItemsControl生成逗号分隔的字符串列表感觉完全过度。然而,问题是关于如何对ItemsControl中的最后一个元素应用不同的样式,所以很遗憾,我不能给这个点赞。 - undefined
你可以始终为那些真正希望得到我回答中所描述的结果的人点赞,他们很可能正试图实现我猜测他们真正想要的东西。记住,有一个被接受的答案确实能够做到他想要的,所以你也可以为它投票(尽管如果你看一下那个被接受的答案,他是在“处理”第一个而不是最后一个项目,但这只是语义问题而已…)最终,你投票是因为某个东西是否有帮助,而不仅仅是“是否完全匹配”。这就是这个网站的目的! :) - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接