Search code examples
wpfxamllistviewdata-bindingdatatrigger

Collapse an WPF button if and only if this is in the last ListViewItem (row) from the ListView


In a WPF ListView I have 3 columns (GridViewColumns). The first column is auto-incremental (1, 2, 3, and so on). The second one is simply a string and the last column contains 3 buttons, delete, move up and move down (all in the same column). Let's focus on the last one which is the one I am interested in.

What do I try to achieve?

I want to collapse the move up button (Up Arrow) from the first row of ListView and also collapse the move down button (Down Arrow) from the last row of ListView. For me, first row is that which start from 1. See below image.

A Grid view with the last column showing 3 buttons next to each other (move, up, down). The up button is hidden in the first row and the down button in the last row circled in red is shown, but should be hidden.

What did I do? I applied a style to the buttons to collapse the move up button in the first row and it worked, but now I do not know how to do the same for the move down button in the last row (see button marked with a red circle in the above image).

Here is my code:

<Style x:Key="myButtonStyle" TargetType="Button">
    <Style.Triggers>
        <!-- Collapse move up button from the first row of the ListView -->
        <DataTrigger Binding="{Binding (ItemsControl.AlternationIndex),
                                   RelativeSource={RelativeSource AncestorType=ListViewItem}}" Value="1">
            <Setter Property="Visibility" Value="Collapsed"/>
        </DataTrigger>

        <!-- HOWTO: Collapse move down button from the last row of the ListView here??? -->

    </Style.Triggers>
</Style>

As you see above I use ItemsControl.AlternationIndex which I am using it to number the first column of the ListView.


Solution

  • What you would need to do is the compare the current alternation index to the alternation count. If Index == Alternation Count - 1 then it is the last row and you can hide the button. However, the Value property of DataTrigger is not a dependency property, so you cannot bind the alternation count as value. Moreover, you would need to compare the alternation count minus one to the alternation index.

    What you can do instead is use a MultiBinding to bind the alternation index and the alternation count. Then you can create a custom value converter that checks for the condition above and returns true or false. As Value of the DataTrigger you would only have to assign True.

    Let's create the converter, which returns if there are two values that are off by one. You could make this less abstract, but this way it does not matter in which order you bind index and count.

    public class OffByOneToBoolConverter : IMultiValueConverter
    {
       public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
       {
          if (values == null || values.Length != 2 || !(values[0] is int value1) || !(values[1] is int value2))
             return Binding.DoNothing;
    
          return Math.Abs(value1 - value2) == 1;
       }
    
       public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
       {
          throw new InvalidOperationException();
       }
    }
    

    For the down button, create a new style with the MultiBinding.

    <Style x:Key="myDownButtonStyle" TargetType="Button">
       <Style.Triggers>
          <DataTrigger Value="True">
             <DataTrigger.Binding>
                <MultiBinding Converter="{StaticResource OffByOneToBoolConverter}">
                   <Binding Path="(ItemsControl.AlternationIndex)"
                            RelativeSource="{RelativeSource AncestorType=ListViewItem}"/>
                   <Binding Path="(ItemsControl.AlternationCount)" RelativeSource="{RelativeSource AncestorType={x:Type ListView}}"/>
                </MultiBinding>
             </DataTrigger.Binding>
             <Setter Property="Visibility" Value="Collapsed"/>
          </DataTrigger>
       </Style.Triggers>
    </Style>
    

    Do not forget to create an instance of the OffByOneToBoolConverter in any resource dictionary in scope. By the way, you need separate button styles for the Up and Down buttons, since both have exclusive conditions. For the Up button consider Visibility set to Hidden, then the button down button stays in place even if the Up button is not shown.