Search code examples
wpfdata-bindingrelativesource

Binding {RelativeSource PreviousData} breaks binding in specific case


I tried to use {RelativeSource PreviousData} in a ListBox.ItemTemplate and it worked correctly.

But, when using the specific code provided below, binding stops working when scrolling up an down few times and some of the Rectangles are missing.

The issue reproduce even when using a single DataTrigger, but it does not reconstruct when ListBox.Height is more than 178.

Example GIF - Green lines are missing!:

Regular scroll works, but fast scroll cause it binding to 'loose-it'


MainWindow.Xaml source:

<Window
    x:Class="PreviousDataBindingWheelIssue.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:local="clr-namespace:PreviousDataBindingWheelIssue"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="PreviousData Issue"
    d:DataContext="{d:DesignInstance Type=local:MyModel}"
    SizeToContent="WidthAndHeight"
    mc:Ignorable="d">
    <StackPanel>

        <!--  Height must be less or equal to 178  -->
        <ListBox
            Width="300"
            Height="178"
            HorizontalContentAlignment="Stretch"
            ItemsSource="{Binding MyData}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <DockPanel Background="#FFFFFFED">
                        <Rectangle
                            Height="2"
                            Margin="0"
                            DockPanel.Dock="Top">
                            <Rectangle.Style>
                                <Style TargetType="Rectangle">
                                    <Setter Property="Fill" Value="#FF63605C" />
                                    <Style.Triggers>

                                        <!--
                                            Hide our magnificent separator if this is the first item on the list
                                            see http://stackoverflow.com/a/22705507/426315
                                            but, it seems to have some issues when using mouse wheel
                                            some of the rows does NOT have the rectangle even when PreviousData SHOULD not be null
                                        -->
                                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
                                            <Setter Property="Visibility" Value="Collapsed" />
                                        </DataTrigger>

                                        <DataTrigger Binding="{Binding}" Value="Fun Item">
                                            <Setter Property="Fill" Value="SpringGreen" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Rectangle.Style>
                        </Rectangle>

                        <TextBlock
                            Margin="5,7,5,7"
                            VerticalAlignment="Center"
                            FontSize="12"
                            Text="{Binding}" />
                    </DockPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>
</Window>

Mainwindow code behind:

using System.Windows;
namespace PreviousDataBindingWheelIssue
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MyModel();
        }
    }
}

MyModel.cs source:

using System.Collections.ObjectModel;

namespace PreviousDataBindingWheelIssue
{
    public class MyModel
    {
        public ObservableCollection<string> MyData { get; set; }

        public MyModel()
        {
            MyData = new ObservableCollection<string>()
            {
                "Lorem ipsum dolor", "sit amet, consectetur", "adipiscing elit. Sed",
                "Fun Item",
                "rhoncus leo convallis", "pulvinar tellus at",
                "Fun Item",
                "porta metus. Mauris", "sed mauris quis", "neque congue semper",
                "Fun Item",
                "vitae non leo", "Donec aliquet feugiat", "massa vitae luctus",
                "Fun Item",
                "Duis pharetra velit", "et lorem blandit"
            };
        }
    }
}

Solution

  • Since the PreviousData binding is not reliable with virtualization, you can either disable virtualization by setting VirtualizingPanel.IsVirtualizing="False" on ListBox, or make your bindings virtualization ready.

    One way to deal with such an issue is to create a custom listbox (ListBox2 in my sample code), override PrepareContainerForItemOverride and set some property that can be used for further operations. I create an attached property ItemIndex for this purpose.

    public class ListBox2 : ListBox
    {
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            SetItemIndex(element, ItemContainerGenerator.IndexFromContainer(element));
        }
    
    
        // helper attached property to indicate the index of listbox items
    
        public static int GetItemIndex(DependencyObject obj)
        {
            return (int)obj.GetValue(ItemIndexProperty);
        }
    
        protected static void SetItemIndex(DependencyObject obj, int value)
        {
            obj.SetValue(ItemIndexPropertyKey, value);
        }
    
        private static readonly DependencyPropertyKey ItemIndexPropertyKey =
            DependencyProperty.RegisterAttachedReadOnly("ItemIndex", typeof(int), typeof(ListBox2), new PropertyMetadata(-1));
    
        public static readonly DependencyProperty ItemIndexProperty = ItemIndexPropertyKey.DependencyProperty;
    }
    

    Then change the xaml to use ListBox2 and rely on the ItemIndex instead of PreviousData:

    <local:ListBox2
        Width="300"
        Height="178"
        HorizontalContentAlignment="Stretch"
        ItemsSource="{Binding MyData}">
        <local:ListBox2.ItemTemplate>
            <DataTemplate>
                <DockPanel Background="#FFFFFFED">
                    <Rectangle
                        Height="2"
                        Margin="0"
                        DockPanel.Dock="Top">
                        <Rectangle.Style>
                            <Style TargetType="Rectangle">
                                <Setter Property="Fill" Value="#FF63605C" />
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Path=(local:ListBox2.ItemIndex),RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="0">
                                        <Setter Property="Visibility" Value="Collapsed" />
                                    </DataTrigger>
    
                                    <DataTrigger Binding="{Binding}" Value="Fun Item">
                                        <Setter Property="Fill" Value="SpringGreen" />
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Rectangle.Style>
                    </Rectangle>
    
                    <TextBlock
                        Margin="5,7,5,7"
                        VerticalAlignment="Center"
                        FontSize="12"
                        Text="{Binding}" />
                </DockPanel>
            </DataTemplate>
        </local:ListBox2.ItemTemplate>
    </local:ListBox2>