How can a separator be added between items in an ItemsControl
Asked Answered
B

5

65

I'm needing to display a list of numbers from a collection in an Items Control. So the items are: "1", "2", "3".

When they are rendered, I need them separated by a comma (or something similar). So the above 3 items would look like this: "1, 2, 3".

How can I add a separator to the individual items, without having one tacked on the end of the list?

I am not stuck on using an ItemsControl, but that's what I had started to use.

Byword answered 24/3, 2010 at 20:40 Comment(0)
B
118
<ItemsControl ItemsSource="{Binding Numbers}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- could use a WrapPanel if more appropriate for your scenario -->
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock x:Name="commaTextBlock" Text=", "/>
                <TextBlock Text="{Binding .}"/>
            </StackPanel>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
                    <Setter Property="Visibility" TargetName="commaTextBlock" Value="Collapsed"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>

    </ItemsControl.ItemTemplate>
</ItemsControl>

I arrived at your question because I was looking for a solution in Silverlight, which does not have a previous data relative source.

Barozzi answered 28/7, 2010 at 9:52 Comment(6)
@foson: I never found one. I ended up using a negative margin to "cut off" the trailing ", " in the text. It felt dirty, but it worked.Barozzi
What about using the index of the item and comparing to zero?Maryellen
That is great, simple and works fine. With this i can detect if an item is the first one of a collection (inside the data template). But there is any way for detect if is the last item? Maybe a NextItem or some thing? Please see this question: #13613553.Covalence
@GONeale - It must be something with declarative languages, I know XSLT and HTML to some degree feel the exact same way to me.Cain
That's a neat trick. Thanks. I was able to use this principle to add space between my items but not above the top one or below the bottom one by setting a margin of 0,20,0,0 and making the trigger set the margin to 0.Kneehigh
Chris' answer below that uses the "AlternationCount" is a more reliable solution, especially when list counts change. https://mcmap.net/q/298615/-how-can-a-separator-be-added-between-items-in-an-itemscontrolWomble
M
34

The current accepted answer gave me a xaml binding error for every template, which I was concerned could be affecting performance. Instead, I did the below, using the AlternationIndex to hide the first separator. (Inspired by this answer.)

<ItemsControl ItemsSource="{Binding Numbers}" AlternationCount="{Binding RelativeSource={RelativeSource Self}, Path=Items.Count}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock x:Name="SeparatorTextBlock" Text=", "/>
                <TextBlock Text="{Binding .}"/>
            </StackPanel>
        <DataTemplate.Triggers>
            <Trigger Property="ItemsControl.AlternationIndex" Value="0">
                <Setter Property="Visibility" TargetName="SeparatorTextBlock" Value="Collapsed" />
            </Trigger>
         </DataTemplate.Triggers>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
Maduro answered 7/12, 2015 at 16:55 Comment(3)
Funny. I came back here after having problems with my commas sticking around after the list count changes using the accepted answer. Your solution solved my problem, thank you!Womble
For me works without dot on this line <TextBlock Text="{Binding }"/> instead of <TextBlock Text="{Binding .}"/>Palma
The AlternationIndex solution also works with <ControlTemplate /> as well, unlike other solutions which means it can be used without data binding (where your items are specified in XAML). Simply awesome idea.Sheaff
B
5

For a more generalized Silverlight-compatible solution, I derived a control from ItemsControl (SeperatedItemsControl). Each item is wrapped in a SeperatedItemsControlItem, just like ListBox's ListBoxItem. The template for SeperatedItemsControlItem contains a seperator and a ContentPresenter. The seperator for the first element in the collection is hidden. You can easily modify this solution to make a horizontal bar seperator between items, which is what I created it for.

MainWindow.xaml:

<Window x:Class="ItemsControlWithSeperator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:local="clr-namespace:ItemsControlWithSeperator"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
    <local:ViewModel x:Key="vm" />

</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" DataContext="{StaticResource vm}">

    <local:SeperatedItemsControl ItemsSource="{Binding Data}">
        <local:SeperatedItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </local:SeperatedItemsControl.ItemsPanel>
        <local:SeperatedItemsControl.ItemContainerStyle>
            <Style TargetType="local:SeperatedItemsControlItem">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="local:SeperatedItemsControlItem" >
                            <StackPanel Orientation="Horizontal">
                                <TextBlock x:Name="seperator">,</TextBlock>
                                <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"/>
                            </StackPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </local:SeperatedItemsControl.ItemContainerStyle>
    </local:SeperatedItemsControl>
</Grid>

C# Code:

using System;
using System.Windows;
using System.Windows.Controls;

namespace ItemsControlWithSeperator
{

    public class ViewModel
    {
        public string[] Data { get { return new[] { "Amy", "Bob", "Charlie" }; } }
    }

    public class SeperatedItemsControl : ItemsControl
    {

        public Style ItemContainerStyle
        {
            get { return (Style)base.GetValue(SeperatedItemsControl.ItemContainerStyleProperty); }
            set { base.SetValue(SeperatedItemsControl.ItemContainerStyleProperty, value); }
        }

        public static readonly DependencyProperty ItemContainerStyleProperty =
            DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(SeperatedItemsControl), null);

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new SeperatedItemsControlItem();
        }
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is SeperatedItemsControlItem;
        }
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            //begin code copied from ListBox class

            if (object.ReferenceEquals(element, item))
            {
                return;
            }

            ContentPresenter contentPresenter = element as ContentPresenter;
            ContentControl contentControl = null;
            if (contentPresenter == null)
            {
                contentControl = (element as ContentControl);
                if (contentControl == null)
                {
                    return;
                }
            }
            DataTemplate contentTemplate = null;
            if (this.ItemTemplate != null && this.DisplayMemberPath != null)
            {
                throw new InvalidOperationException();
            }
            if (!(item is UIElement))
            {
                if (this.ItemTemplate != null)
                {
                    contentTemplate = this.ItemTemplate;
                }

            }
            if (contentPresenter != null)
            {
                contentPresenter.Content = item;
                contentPresenter.ContentTemplate = contentTemplate;
            }
            else
            {
                contentControl.Content = item;
                contentControl.ContentTemplate = contentTemplate;
            }

            if (ItemContainerStyle != null && contentControl.Style == null)
            {
                contentControl.Style = ItemContainerStyle;
            }

            //end code copied from ListBox class

            if (this.Items.Count > 0)
            {
                if (object.ReferenceEquals(this.Items[0], item))
                {
                    var container = element as SeperatedItemsControlItem;
                    container.IsFirstItem = true;
                }
            }
        }
        protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);
            if (Items.Count > 1)
            {
                var container = (ItemContainerGenerator.ContainerFromIndex(1) as SeperatedItemsControlItem);
                if (container != null) container.IsFirstItem = false;
            }
            if (Items.Count > 0)
            {
               var container = (ItemContainerGenerator.ContainerFromIndex(0) as SeperatedItemsControlItem);
               if (container != null) container.IsFirstItem = true;
           }
       }

    }

    public class SeperatedItemsControlItem : ContentControl
    {
        private bool isFirstItem;
        public bool IsFirstItem 
        {
            get { return isFirstItem; }
            set 
            {
                if (isFirstItem != value)
                {
                    isFirstItem = value;
                    var seperator = this.GetTemplateChild("seperator") as FrameworkElement;
                    if (seperator != null)
                    {
                        seperator.Visibility = isFirstItem ? Visibility.Collapsed : Visibility.Visible;
                    }
                }
            }
        }    
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (IsFirstItem)
            {
                var seperator = this.GetTemplateChild("seperator") as FrameworkElement;
                if (seperator != null)
                {
                    seperator.Visibility = Visibility.Collapsed;
                }
            }
        }
    }
}
Ballarat answered 12/11, 2011 at 18:0 Comment(0)
C
4

You can also multibind to ItemsControl.AlternationIndex and ItemsControl.Count and compare the AlternationIndex to Count to see if you are the last item.

Set the AlternationIndex high enough to accomodate all your items then make a LastItemConverter with a Convert method looking something like this:

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var alterationCount = (int)values[0];
        var itemCount = (int)values[1];
        if (itemCount > 1)
        {
            return alterationCount == (itemCount - 1) ? Visibility.Collapsed : Visibility.Visible;
        }

        return Visibility.Collapsed;
    }
Chancelor answered 21/10, 2010 at 12:31 Comment(1)
One can place divider before the contents of an item in template. Then it's sufficient to bind to AlternationIndex only and hide divider in the first item (AlternationIndex == 0).Aerography
B
1

I figured I should give the solution I ended up with.

I ended up binding my collection of items to the Text of a TextBlock, and using a value converter to change the bound collection of items into the formatted string.

Byword answered 25/3, 2010 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.