WPF 视图 ListView 倒序显示

6
我希望能够在后台维护一个列表,使新项目加入到列表的末尾(以避免在更新时使用Insert()将项目推开),但是可以按相反的顺序显示它而不需要“排序”。
我只希望它按照与列表中相反的顺序显示在列表视图中。我是否可以使用模板或类似的东西来实现这个功能?
4个回答

4

您可以将ListView的ItemsPanel更改为具有LastChildFill设置为false的DockPanel。然后在ItemContainerStyle中,将DockPanel.Dock属性设置为bottom。这将从底部开始填充并向上工作。我将ListView放在一个具有2行的网格中,第一行的Height =“ Auto”,第二行的Height =“ *”,它就像普通的ListView一样,但项目反转了。

<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>

灵感来自于:https://dev59.com/6XRB5IYBdhLWcg3w26t3#493059

1
整洁简明。网格内容实际上并不需要。 - Florian
这个想法还不完整。当列表框内容所占区域超过窗口大小时,没有滚动条。 - Grigoriy

3

更新
这里提供了一个附加行为,可以将任何ItemsControl反转。使用方法如下:

<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;
    }
}

否则,您可以按照以下链接中概述的步骤操作:WPF 反向 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>

链接中的第二个代码块(用于列表视图)会使项目出现倒置。根据我的经验,第一个代码块在集合更改时似乎无法正常工作。 - Jim W says reinstate Monica
第二个块对我有用(第一个在设计模式下不起作用)。您需要应用两个ScaleTransforms。第一个将倒置整个StackPanel(带有反转的项),而第二个变换只需还原每个项。 - Florian
当项目不适合可见区域并且出现垂直滚动条时,第二个块会产生错误的行为。 - ajarov

0

假设ItemsSource是一个ObservableCollection,我的解决方案是实现一个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
}

然后,您只需向ViewModel添加一个新属性并绑定它:

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);
}

0
在搜索并尝试了半天后,我找到了@ryan-west的答案,并稍微修改了一下以适应我的需求。我成功地得到了一个滚动查看器中的列表框,以通常所见的相反顺序显示列表中的内容。
      <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>

如果你将ListBox放在ScrollViewer中,那么它的内容就不会被虚拟化,对于大型列表来说,这会影响性能。 - Grigoriy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接