Change style of last item in ListBox
Asked Answered
L

3

9

I have listbox control which has list of colors. Here is code and Image:

<ListBox Name="FillSelections" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Center" SelectedItem="{Binding SelectedColor}" SelectionMode="Single" Style="{StaticResource HorizontalListBoxStyle}" ItemsSource="{Binding FillColors}" ItemTemplate="{StaticResource ColorsItemTemplate}"></ListBox>

 <DataTemplate x:Key="ColorsItemTemplate">
    <Border BorderBrush="Transparent">
        <Rectangle Width="20" StrokeThickness="1" Stroke="Black">
            <Rectangle.Fill>
                <SolidColorBrush Color="{Binding}" />
            </Rectangle.Fill>
        </Rectangle>
    </Border>

Image:

image

How would I change style of last item only like this:

image

Lithium answered 25/8, 2012 at 21:21 Comment(3)
You might be able to use a trigger based on RelativeSource PreviousData like this answerPagandom
if it was that easy then i would have not asked that here. Instead of giving minus vote (THE PERSON WHO GAVE ME - POINT)HE/SHE needs to verify first. My listebox item is not static. It's depends on skin color of my application and shows all diff shade of that skin color.Lithium
Instead of Binding="{Binding RelativeSource={RelativeSource PreviousData }}", is there any way i can compare with current data. I tried self but not working.Lithium
C
22

This can be achieved through converter which do the work of finding if its last item in the listbox -

Converter

public class IsLastItemInContainerConverter : IValueConverter
{
   public object Convert(object value, Type targetType,
                         object parameter, CultureInfo culture)
   {
       DependencyObject item = (DependencyObject)value;
       ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);

       return ic.ItemContainerGenerator.IndexFromContainer(item)
               == ic.Items.Count - 1;
   }

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

And using that you can set the DataTemplate in your xaml class like this -

<ListBox ItemContainerStyle="{StaticResource ColorsItemStyle}"/>

<Style x:Key="ColorsItemStyle">
  <Style.Triggers>
     <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
       Converter={StaticResource IsLastItemInContainerConverter}}" Value="False">
          <Setter Property="ContentTemplate">
             <Setter.Value>
                 <DataTemplate></DataTemplate> // Your template goes here
             </Setter.Value>
          </Setter>
      </DataTrigger>

     <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
       Converter={StaticResource IsLastItemInContainerConverter}}" Value="True">
          <Setter Property="ContentTemplate">
             <Setter.Value>
                 <DataTemplate></DataTemplate> // Your lastItem template goes here
             </Setter.Value>
          </Setter>
      </DataTrigger>
  </Style.Triggers>
</Style>
Care answered 26/8, 2012 at 15:9 Comment(4)
How can I get the binding to update whenever the ItemsControl changes? (adding/removing items)Dorina
I haven't been able to get this to work. The list is always empty when the converter is called for some reason.Longsufferance
This doesn't work if items are added to the items source one at a timeHyperspace
Self is not a Dependency Object in this case. You should have rather bind with TemplatedParent as relative source.Hypothetical
L
8

to get this to work w/ a ListBox that changes over time I ended up using a MultiBinding:

<DataTemplate x:Key="myItemTemplate">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding}"/>
        <TextBlock x:Name="dots" Text="..."/>
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger Value="False">
            <DataTrigger.Binding>
                <MultiBinding Converter="{StaticResource isLastItemInContainerConverter}">
                    <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=ListBoxItem}" />
                    <Binding Path="Items.Count" RelativeSource="{RelativeSource FindAncestor, AncestorType=ListBox}" />
                </MultiBinding>
            </DataTrigger.Binding>
            <Setter TargetName="dots" Property="Visibility" Value="Collapsed"/>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

Note: the second binding is only used to get notified when the list changes

here is the corresponding MultivalueConverter

public class IsLastItemInContainerConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        DependencyObject item = (DependencyObject)values[0];
        ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);

        return ic.ItemContainerGenerator.IndexFromContainer(item) == ic.Items.Count - 1;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Lamonica answered 11/6, 2014 at 13:15 Comment(2)
I'm not sure you needed to use the multibinding because the converter is only using the first of the two values. I assume you probably started with both and later figured out how to use just the first value...Sumptuous
@TonyT The answer says: "Note: the second binding is only used to get notified when the list changes"Expectoration
P
3

Stop Messing With The UI... It's Just Data!!!

Personally, I think the easiest way to do this is using a CompositeCollection (or a custom enumerator). The advantage of this way of thinking is it properly separates this as data, which is what it is, rather than messing with custom UI nonsense/bindings/relative sources, etc.

I'll explain.

Consider you are trying to show 'x' number of dynamically-generated colors stored in a myColors collection, followed by something that means 'no color' (your box with the line in it.)

First, define a 'no color' token somewhere in your app, like so...

class NoColorToken{}

Then define a DataTemplate targeting that class, like so...

<DataTemplate DataType="{x:Type ns:NoColorToken}">
    <TextBlock Text="Replace with template representing 'no color'" />
</DataTemplate>

You could even make it more generic calling it a NoSelectionToken to use with any type of list. Just make sure to scope the DataTemplate to that specific location's usage (i.e. no color in this example.)

Then in your code, just stuff your colors into a CompositeCollection followed by an instance of the NoColorToken class, like so:

var colorsAndToken = new CompositeCollection();
colorsAndToken.Add(new CollectionContainer(myColors));
colorsAndToken.Add(new NoColorToken());

itemsControl.ItemsSource = colorsAndToken;

Changes to MyColors (if observable) will automatically update the UI.

Things can be made even easier if they don't need to be observable (i.e. No individual adds or removals) by simply writing an enumerator function (essentially the simplified basics of what CompositeCollection does internally.)

IEnumerable ColorsWithToken(IEnumerable colors){

    foreach (var color in colors)
        yield return color;

    yield return new NoColorToken();
}

itemsControl.ItemsSource = ColorsWithToken(myColors);

Again though, the custom enumerator approach won't track changes to myColors. If myColors changes, you have to re-assign ItemsSource. However, if you go the route of the CompositeCollection, it handles updates automatically, just at the expense of a new object, the CompositeCollection, but that's what it's there for.

By the way, you can also wrap the above in a converter that handles either approach for you, returning either the enumerator, or a CompositeCollection for a pure XAML approach regardless of which ItemsControl.ItemsSource you're applying it to. I've actually done exactly that with an AddNoSelectionPlaceholder converter.)

Again, the reason I prefer this is it treats the items, including the 'no color' item as data, which is what it is. Even better, since it is data, it lets you easily change things around. Want the 'no color' item to be first? Just switch the order you added them.

colorsAndToken.Add(new NoColorToken());
colorsAndToken.Add(new CollectionContainer(myColors));

or

yield return new NoColorToken();

foreach (var color in colors)
    yield return color;

Again, it's just data now. Nothing 'clever' needs to be done in the data template or control, bindings or anywhere else. Even better, it's now also fully unit-testable. No UI needed.

Protoxylem answered 18/2, 2019 at 23:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.