6

I want to be able to maintain a list in the background that puts new items at the end of the list (to avoid Insert() pushing the items around on updates) but to be able to display it in the reverse order without "sorting".

I just want it to show up in the list view in the reverse order that it is in the list. Can I do this with a template or something similar?

Paul
  • 61
  • 1
  • 2

4 Answers4

4

You can change the ListView's ItemsPanel to be a DockPanel with LastChildFill set to false. Then in the ItemContainerStyle, set the DockPanel.Dock property to bottom. This will start filling at the bottom and work its way up to the top. I put the ListView in a grid with 2 rows, first's Height="Auto" and second's Height="*" and it acted just like a normal ListView, but with the items reversed.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <ListBox Grid.Row="0">
    <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}"
                   BasedOn="{StaticResource {x:Type ListBoxItem}}">
                <Setter Property="DockPanel.Dock"
                        Value="Bottom" />
            </Style>
        </ListBox.ItemContainerStyle>
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <DockPanel LastChildFill="False" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>

Idea taken from: https://stackoverflow.com/a/493059/2812277

Ryan West
  • 88
  • 4
3

Update
Here is an Attached Behavior which will reverse any ItemsControl. Use it like this

<ListBox behaviors:ReverseItemsControlBehavior.ReverseItemsControl="True"
         ...>

ReverseItemsControlBehavior

public class ReverseItemsControlBehavior
{
    public static DependencyProperty ReverseItemsControlProperty =
        DependencyProperty.RegisterAttached("ReverseItemsControl",
                                            typeof(bool),
                                            typeof(ReverseItemsControlBehavior),
                                            new FrameworkPropertyMetadata(false, OnReverseItemsControlChanged));
    public static bool GetReverseItemsControl(DependencyObject obj)
    {
        return (bool)obj.GetValue(ReverseItemsControlProperty);
    }
    public static void SetReverseItemsControl(DependencyObject obj, object value)
    {
        obj.SetValue(ReverseItemsControlProperty, value);
    }

    private static void OnReverseItemsControlChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if ((bool)e.NewValue == true)
        {
            ItemsControl itemsControl = sender as ItemsControl;
            if (itemsControl.IsLoaded == true)
            {
                DoReverseItemsControl(itemsControl);
            }
            else
            {
                RoutedEventHandler loadedEventHandler = null;
                loadedEventHandler = (object sender2, RoutedEventArgs e2) =>
                {
                    itemsControl.Loaded -= loadedEventHandler;
                    DoReverseItemsControl(itemsControl);
                };
                itemsControl.Loaded += loadedEventHandler;
            }
        }
    }
    private static void DoReverseItemsControl(ItemsControl itemsControl)
    {
        Panel itemPanel = GetItemsPanel(itemsControl);
        itemPanel.LayoutTransform = new ScaleTransform(1, -1);
        Style itemContainerStyle;
        if (itemsControl.ItemContainerStyle == null)
        {
            itemContainerStyle = new Style();
        }
        else
        {
            itemContainerStyle = CopyStyle(itemsControl.ItemContainerStyle);
        }
        Setter setter = new Setter();
        setter.Property = ItemsControl.LayoutTransformProperty;
        setter.Value = new ScaleTransform(1, -1);
        itemContainerStyle.Setters.Add(setter);
        itemsControl.ItemContainerStyle = itemContainerStyle;
    }
    private static Panel GetItemsPanel(ItemsControl itemsControl)
    {
        ItemsPresenter itemsPresenter = GetVisualChild<ItemsPresenter>(itemsControl);
        if (itemsPresenter == null)
            return null;
        return GetVisualChild<Panel>(itemsControl);
    }
    private static Style CopyStyle(Style style)
    {
        Style styleCopy = new Style();
        foreach (SetterBase currentSetter in style.Setters)
        {
            styleCopy.Setters.Add(currentSetter);
        }
        foreach (TriggerBase currentTrigger in style.Triggers)
        {
            styleCopy.Triggers.Add(currentTrigger);
        }
        return styleCopy;
    }

    private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
    {
        T child = default(T);

        int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < numVisuals; i++)
        {
            Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
            child = v as T;
            if (child == null)
            {
                child = GetVisualChild<T>(v);
            }
            if (child != null)
            {
                break;
            }
        }
        return child;
    }
}

Otherwise, you can follow what's outlined in the following link: WPF reverse ListView

<ListBox ...>
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel VerticalAlignment="Top"  Orientation="Vertical">
                <VirtualizingStackPanel.LayoutTransform>
                    <ScaleTransform ScaleX="1" ScaleY="-1" />
                </VirtualizingStackPanel.LayoutTransform>
            </VirtualizingStackPanel>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="LayoutTransform">
                <Setter.Value>
                    <ScaleTransform ScaleX="1" ScaleY="-1" />
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • The 2nd block of code from the link (which is for listview) makes the items appear upside down. The 1st block of code doesn't seem to work when the collection changes (in my experience). – Jim W Oct 24 '13 at 18:38
  • 2nd block worked for me (first does not work in design mode). You need to apply both ScaleTransforms. The first will display he whole stackpanel upside down (with reversed items) and with the second transform you just revert each item – Florian Mar 05 '17 at 12:03
  • The 2nd block produces incorrect behavior when the items don't fit in the visible area and a vertical scrollbar appears. – ajarov Jul 29 '22 at 13:14
0

After googling and trying things for half of a day I came across @ryan-west answer, and slightly modified it to fit my needs. I was able to get exactly what I wanted a listbox in a scrollviewer that showed a list exactly as normally seen, but in reverse order.

      <ScrollViewer>
            <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                     VerticalAlignment="Top"
                     ItemsSource="{Binding MyList, Mode=TwoWay}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <DockPanel>
                            <Image DockPanel.Dock="Left"
                                   Source="MyIcon.png"
                                   Width="16" />
                            <Label DockPanel.Dock="Left"
                                   Content="{Binding MyName, Mode=TwoWay}"/>
                        </DockPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemContainerStyle>
                    <Style TargetType="{x:Type ListBoxItem}"
                           BasedOn="{StaticResource {x:Type ListBoxItem}}">
                        <Setter Property="DockPanel.Dock"
                                Value="Bottom" />
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <DockPanel LastChildFill="False" />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
            </ListBox>
        </ScrollViewer>
dvdhns
  • 3,026
  • 2
  • 15
  • 11
  • If you place ListBox inside ScrollViewer, then it's content won't be virtualized and it'll hit your performance in case of big list – Grigoriy Feb 02 '23 at 14:06
0

Assuming that the ItemsSource is an ObservableCollection, my solution was to implement a ReverseObservableCollection:

public class ReverseObservableCollection<T> : IReadOnlyList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    #region Private fields

    private readonly ObservableCollection<T> _observableCollection;

    #endregion Private fields

    #region Constructor

    public ReverseObservableCollection(ObservableCollection<T> observableCollection)
    {
        _observableCollection = observableCollection;
        observableCollection.CollectionChanged += ObservableCollection_CollectionChanged;
    }

    #endregion

    #region Event handlers

    private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (new[] { NotifyCollectionChangedAction.Add, NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Reset }.Contains(e.Action))
        {
            OnPropertyChanged(nameof(Count));
        }
        OnPropertyChanged(Binding.IndexerName); // ObservableCollection does this to improve WPF performance.

        var newItems = Reverse(e.NewItems);
        var oldItems = Reverse(e.OldItems);
        int newStartingIndex = e.NewItems != null ? _observableCollection.Count - e.NewStartingIndex - e.NewItems.Count : -1;
        //int oldCount = _observableCollection.Count - (e.NewItems?.Count ?? 0) + (e.OldItems?.Count ?? 0);
        //int oldStartingIndex = e.OldItems != null ? oldCount - e.OldStartingIndex - e.OldItems.Count : -1;
        int oldStartingIndex = e.OldItems != null ? _observableCollection.Count - e.OldStartingIndex - (e.NewItems?.Count ?? 0) : -1;
        var eventArgs = e.Action switch
        {
            NotifyCollectionChangedAction.Add => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, newStartingIndex),
            NotifyCollectionChangedAction.Remove => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems, oldStartingIndex),
            NotifyCollectionChangedAction.Replace => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, oldStartingIndex),
            NotifyCollectionChangedAction.Move => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, oldItems, newStartingIndex, oldStartingIndex),
            NotifyCollectionChangedAction.Reset => new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset),
            _ => throw new ArgumentException("Unexpected Action", nameof(e)),
        };
        OnCollectionChanged(eventArgs);
    }

    #endregion

    #region IReadOnlyList<T> implementation

    public T this[int index] => _observableCollection[_observableCollection.Count - index - 1];

    public int Count => _observableCollection.Count;

    public IEnumerator<T> GetEnumerator()
    {
        for (int i = _observableCollection.Count - 1; i >= 0; --i)
        {
            yield return _observableCollection[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    #region INotifyCollectionChanged implementation

    public event NotifyCollectionChangedEventHandler? CollectionChanged;

    private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        CollectionChanged?.Invoke(this, args);
    }

    #endregion

    #region INotifyPropertyChanged implementation

    public event PropertyChangedEventHandler? PropertyChanged;

    private void OnPropertyChanged(string? propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion

    #region Private methods

    private IList? Reverse(IList? list)
    {
        if (list == null) return null;

        object[] result = new object[list.Count];
        for (int i = 0; i < list.Count; ++i)
        {
            result[i] = list[list.Count - i - 1];
        }
        return result;
    }

    #endregion
}

Then, you just add a new property to the ViewModel and bind to it:

public class ViewModel
{
    // Your old ItemsSource:
    public ObservableCollection<string> Collection { get; } = new ObservableCollection<string>();

    // New ItemsSource:
    private ReverseObservableCollection<string>? _reverseCollection = null;
    public ReverseObservableCollection<string> ReverseCollection => _reverseCollection ??= new ReverseObservableCollection<string>(Collection);
}
ajarov
  • 149
  • 2
  • 5