Лучший способ сделать сортировку WPF ListView/GridView при нажатии на заголовок столбца?
здесь много из решений в интернете, пытающихся заполнить это, казалось бы, очень простое упущение от WPF. Я действительно смущен тем, что было бы "лучшим" способом. Например... Я хочу, чтобы в заголовке столбца было немного стрелок вверх/вниз, чтобы указать направление сортировки. Есть, по-видимому, как 3 различных способа сделать это, некоторые с помощью кода, некоторые с помощью разметки, некоторые с помощью разметки плюс код, и все они кажутся скорее как хак.
кто-нибудь сталкивался с этим проблема раньше, и нашли решение, которым они полностью довольны? Кажется странным, что такая базовая часть функциональности WinForms отсутствует в WPF и должна быть взломана.
8 ответов:
все зависит на самом деле, если вы используете DataGrid из инструментария WPF, то есть встроенная сортировка, даже многоколоночная сортировка, которая очень полезна. Проверьте больше здесь:
кроме того, если вы используете другой элемент управления, который не поддерживает сортировка, я бы рекомендовал следующие методы:
пользовательская сортировка ли Гао
затем:
Я написал набор вложенных свойств для автоматической сортировки
GridView, вы можете проверить это здесь. Он не обрабатывает стрелку вверх/вниз, но его можно легко добавить.<ListView ItemsSource="{Binding Persons}" IsSynchronizedWithCurrentItem="True" util:GridViewSort.AutoSort="True"> <ListView.View> <GridView> <GridView.Columns> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" util:GridViewSort.PropertyName="Name"/> <GridViewColumn Header="First name" DisplayMemberBinding="{Binding FirstName}" util:GridViewSort.PropertyName="FirstName"/> <GridViewColumn Header="Date of birth" DisplayMemberBinding="{Binding DateOfBirth}" util:GridViewSort.PropertyName="DateOfBirth"/> </GridView.Columns> </GridView> </ListView.View> </ListView>
MSDN имеет простой способ выполнить сортировку по столбцам С глифами вверх / вниз. Однако пример не является полным - они не объясняют, как использовать шаблоны данных для глифов. Ниже то, что я получил, чтобы работать с моим ListView. Это работает на .Net 4.
в вашем ListView вы должны указать обработчик событий для запуска для щелчка по GridViewColumnHeader. Мой ListView выглядит так:
<ListView Name="results" GridViewColumnHeader.Click="results_Click"> <ListView.View> <GridView> <GridViewColumn DisplayMemberBinding="{Binding Path=ContactName}"> <GridViewColumn.Header> <GridViewColumnHeader Content="Contact Name" Padding="5,0,0,0" HorizontalContentAlignment="Left" MinWidth="150" Name="ContactName" /> </GridViewColumn.Header> </GridViewColumn> <GridViewColumn DisplayMemberBinding="{Binding Path=PrimaryPhone}"> <GridViewColumn.Header> <GridViewColumnHeader Content="Contact Number" Padding="5,0,0,0" HorizontalContentAlignment="Left" MinWidth="150" Name="PrimaryPhone"/> </GridViewColumn.Header> </GridViewColumn> </GridView> </ListView.View> </ListView>в коде позади, настроить код для обработки сортировка:
// Global objects BindingListCollectionView blcv; GridViewColumnHeader _lastHeaderClicked = null; ListSortDirection _lastDirection = ListSortDirection.Ascending; // Header click event void results_Click(object sender, RoutedEventArgs e) { GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader; ListSortDirection direction; if (headerClicked != null) { if (headerClicked.Role != GridViewColumnHeaderRole.Padding) { if (headerClicked != _lastHeaderClicked) { direction = ListSortDirection.Ascending; } else { if (_lastDirection == ListSortDirection.Ascending) { direction = ListSortDirection.Descending; } else { direction = ListSortDirection.Ascending; } } string header = headerClicked.Column.Header as string; Sort(header, direction); if (direction == ListSortDirection.Ascending) { headerClicked.Column.HeaderTemplate = Resources["HeaderTemplateArrowUp"] as DataTemplate; } else { headerClicked.Column.HeaderTemplate = Resources["HeaderTemplateArrowDown"] as DataTemplate; } // Remove arrow from previously sorted header if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked) { _lastHeaderClicked.Column.HeaderTemplate = null; } _lastHeaderClicked = headerClicked; _lastDirection = direction; } } // Sort code private void Sort(string sortBy, ListSortDirection direction) { blcv.SortDescriptions.Clear(); SortDescription sd = new SortDescription(sortBy, direction); blcv.SortDescriptions.Add(sd); blcv.Refresh(); }а затем в XAML, вам нужно добавить два DataTemplates, которые вы указали в методе сортировки:
<DataTemplate x:Key="HeaderTemplateArrowUp"> <DockPanel LastChildFill="True" Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}}"> <Path x:Name="arrowUp" StrokeThickness="1" Fill="Gray" Data="M 5,10 L 15,10 L 10,5 L 5,10" DockPanel.Dock="Right" Width="20" HorizontalAlignment="Right" Margin="5,0,5,0" SnapsToDevicePixels="True"/> <TextBlock Text="{Binding }" /> </DockPanel> </DataTemplate> <DataTemplate x:Key="HeaderTemplateArrowDown"> <DockPanel LastChildFill="True" Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}}"> <Path x:Name="arrowDown" StrokeThickness="1" Fill="Gray" Data="M 5,5 L 10,10 L 15,5 L 5,5" DockPanel.Dock="Right" Width="20" HorizontalAlignment="Right" Margin="5,0,5,0" SnapsToDevicePixels="True"/> <TextBlock Text="{Binding }" /> </DockPanel> </DataTemplate>С помощью
DockPanelСLastChildFillзначение true сохранит глиф справа от заголовка и позволит метке заполнить остальную часть пространства. Я связалDockPanelширинойActualWidthнаGridViewColumnHeaderпотому что мои столбцы не имеют ширины, что позволяет им автоматически подстраиваться к содержимому. Я поставилMinWidths на колонках, хотя, так что глиф не скрывает название столбца. ЭлементTextBlock Textустанавливается в пустую привязку, которая отображает имя столбца, указанное в заголовке.
Я использую MVVM, поэтому я создал некоторые прикрепленные свойства самостоятельно, используя Томаса в качестве ссылки. Он выполняет сортировку по одному столбцу за раз, когда вы нажимаете на заголовок, переключаясь между восходящим и нисходящим. Он сортирует с самого начала, используя первый столбец. И он показывает глифы стиля Win7 / 8.
как правило, все, что вам нужно сделать, это установить основное свойство true (но вы должны явно объявить GridViewColumnHeaders):
<Window xmlns:local="clr-namespace:MyProjectNamespace"> <Grid> <ListView local:App.EnableGridViewSort="True" ItemsSource="{Binding LVItems}"> <ListView.View> <GridView> <GridViewColumn DisplayMemberBinding="{Binding Property1}"> <GridViewColumnHeader Content="Prop 1" /> </GridViewColumn> <GridViewColumn DisplayMemberBinding="{Binding Property2}"> <GridViewColumnHeader Content="Prop 2" /> </GridViewColumn> </GridView> </ListView.View> </ListView> </Grid> <Window>Если вы хотите сортировать на другом свойстве, чем дисплей, чем вы должны объявить, что:
<GridViewColumn DisplayMemberBinding="{Binding Property3}" local:App.GridViewSortPropertyName="Property4"> <GridViewColumnHeader Content="Prop 3" /> </GridViewColumn>вот код для вложенных свойств, мне нравится лениться и поместить их в приложение.код XAML.cs:
using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data. using System.Windows.Media; using System.Windows.Media.Media3D; namespace MyProjectNamespace { public partial class App : Application { #region GridViewSort public static DependencyProperty GridViewSortPropertyNameProperty = DependencyProperty.RegisterAttached( "GridViewSortPropertyName", typeof(string), typeof(App), new UIPropertyMetadata(null) ); public static string GetGridViewSortPropertyName(GridViewColumn gvc) { return (string)gvc.GetValue(GridViewSortPropertyNameProperty); } public static void SetGridViewSortPropertyName(GridViewColumn gvc, string n) { gvc.SetValue(GridViewSortPropertyNameProperty, n); } public static DependencyProperty CurrentSortColumnProperty = DependencyProperty.RegisterAttached( "CurrentSortColumn", typeof(GridViewColumn), typeof(App), new UIPropertyMetadata( null, new PropertyChangedCallback(CurrentSortColumnChanged) ) ); public static GridViewColumn GetCurrentSortColumn(GridView gv) { return (GridViewColumn)gv.GetValue(CurrentSortColumnProperty); } public static void SetCurrentSortColumn(GridView gv, GridViewColumn value) { gv.SetValue(CurrentSortColumnProperty, value); } public static void CurrentSortColumnChanged( object sender, DependencyPropertyChangedEventArgs e) { GridViewColumn gvcOld = e.OldValue as GridViewColumn; if (gvcOld != null) { CurrentSortColumnSetGlyph(gvcOld, null); } } public static void CurrentSortColumnSetGlyph(GridViewColumn gvc, ListView lv) { ListSortDirection lsd; Brush brush; if (lv == null) { lsd = ListSortDirection.Ascending; brush = Brushes.Transparent; } else { SortDescriptionCollection sdc = lv.Items.SortDescriptions; if (sdc == null || sdc.Count < 1) return; lsd = sdc[0].Direction; brush = Brushes.Gray; } FrameworkElementFactory fefGlyph = new FrameworkElementFactory(typeof(Path)); fefGlyph.Name = "arrow"; fefGlyph.SetValue(Path.StrokeThicknessProperty, 1.0); fefGlyph.SetValue(Path.FillProperty, brush); fefGlyph.SetValue(StackPanel.HorizontalAlignmentProperty, HorizontalAlignment.Center); int s = 4; if (lsd == ListSortDirection.Ascending) { PathFigure pf = new PathFigure(); pf.IsClosed = true; pf.StartPoint = new Point(0, s); pf.Segments.Add(new LineSegment(new Point(s * 2, s), false)); pf.Segments.Add(new LineSegment(new Point(s, 0), false)); PathGeometry pg = new PathGeometry(); pg.Figures.Add(pf); fefGlyph.SetValue(Path.DataProperty, pg); } else { PathFigure pf = new PathFigure(); pf.IsClosed = true; pf.StartPoint = new Point(0, 0); pf.Segments.Add(new LineSegment(new Point(s, s), false)); pf.Segments.Add(new LineSegment(new Point(s * 2, 0), false)); PathGeometry pg = new PathGeometry(); pg.Figures.Add(pf); fefGlyph.SetValue(Path.DataProperty, pg); } FrameworkElementFactory fefTextBlock = new FrameworkElementFactory(typeof(TextBlock)); fefTextBlock.SetValue(TextBlock.HorizontalAlignmentProperty, HorizontalAlignment.Center); fefTextBlock.SetValue(TextBlock.TextProperty, new Binding()); FrameworkElementFactory fefDockPanel = new FrameworkElementFactory(typeof(StackPanel)); fefDockPanel.SetValue(StackPanel.OrientationProperty, Orientation.Vertical); fefDockPanel.AppendChild(fefGlyph); fefDockPanel.AppendChild(fefTextBlock); DataTemplate dt = new DataTemplate(typeof(GridViewColumn)); dt.VisualTree = fefDockPanel; gvc.HeaderTemplate = dt; } public static DependencyProperty EnableGridViewSortProperty = DependencyProperty.RegisterAttached( "EnableGridViewSort", typeof(bool), typeof(App), new UIPropertyMetadata( false, new PropertyChangedCallback(EnableGridViewSortChanged) ) ); public static bool GetEnableGridViewSort(ListView lv) { return (bool)lv.GetValue(EnableGridViewSortProperty); } public static void SetEnableGridViewSort(ListView lv, bool value) { lv.SetValue(EnableGridViewSortProperty, value); } public static void EnableGridViewSortChanged( object sender, DependencyPropertyChangedEventArgs e) { ListView lv = sender as ListView; if (lv == null) return; if (!(e.NewValue is bool)) return; bool enableGridViewSort = (bool)e.NewValue; if (enableGridViewSort) { lv.AddHandler( GridViewColumnHeader.ClickEvent, new RoutedEventHandler(EnableGridViewSortGVHClicked) ); if (lv.View == null) { lv.Loaded += new RoutedEventHandler(EnableGridViewSortLVLoaded); } else { EnableGridViewSortLVInitialize(lv); } } else { lv.RemoveHandler( GridViewColumnHeader.ClickEvent, new RoutedEventHandler(EnableGridViewSortGVHClicked) ); } } public static void EnableGridViewSortLVLoaded(object sender, RoutedEventArgs e) { ListView lv = e.Source as ListView; EnableGridViewSortLVInitialize(lv); lv.Loaded -= new RoutedEventHandler(EnableGridViewSortLVLoaded); } public static void EnableGridViewSortLVInitialize(ListView lv) { GridView gv = lv.View as GridView; if (gv == null) return; bool first = true; foreach (GridViewColumn gvc in gv.Columns) { if (first) { EnableGridViewSortApplySort(lv, gv, gvc); first = false; } else { CurrentSortColumnSetGlyph(gvc, null); } } } public static void EnableGridViewSortGVHClicked( object sender, RoutedEventArgs e) { GridViewColumnHeader gvch = e.OriginalSource as GridViewColumnHeader; if (gvch == null) return; GridViewColumn gvc = gvch.Column; if(gvc == null) return; ListView lv = VisualUpwardSearch<ListView>(gvch); if (lv == null) return; GridView gv = lv.View as GridView; if (gv == null) return; EnableGridViewSortApplySort(lv, gv, gvc); } public static void EnableGridViewSortApplySort( ListView lv, GridView gv, GridViewColumn gvc) { bool isEnabled = GetEnableGridViewSort(lv); if (!isEnabled) return; string propertyName = GetGridViewSortPropertyName(gvc); if (string.IsNullOrEmpty(propertyName)) { Binding b = gvc.DisplayMemberBinding as Binding; if (b != null && b.Path != null) { propertyName = b.Path.Path; } if (string.IsNullOrEmpty(propertyName)) return; } ApplySort(lv.Items, propertyName); SetCurrentSortColumn(gv, gvc); CurrentSortColumnSetGlyph(gvc, lv); } public static void ApplySort(ICollectionView view, string propertyName) { if (string.IsNullOrEmpty(propertyName)) return; ListSortDirection lsd = ListSortDirection.Ascending; if (view.SortDescriptions.Count > 0) { SortDescription sd = view.SortDescriptions[0]; if (sd.PropertyName.Equals(propertyName)) { if (sd.Direction == ListSortDirection.Ascending) { lsd = ListSortDirection.Descending; } else { lsd = ListSortDirection.Ascending; } } view.SortDescriptions.Clear(); } view.SortDescriptions.Add(new SortDescription(propertyName, lsd)); } #endregion public static T VisualUpwardSearch<T>(DependencyObject source) where T : DependencyObject { return VisualUpwardSearch(source, x => x is T) as T; } public static DependencyObject VisualUpwardSearch( DependencyObject source, Predicate<DependencyObject> match) { DependencyObject returnVal = source; while (returnVal != null && !match(returnVal)) { DependencyObject tempReturnVal = null; if (returnVal is Visual || returnVal is Visual3D) { tempReturnVal = VisualTreeHelper.GetParent(returnVal); } if (tempReturnVal == null) { returnVal = LogicalTreeHelper.GetParent(returnVal); } else { returnVal = tempReturnVal; } } return returnVal; } } }
я сделал адаптацию способа Microsoft, где я переопределяю
ListViewуправление сделатьSortableListView:public partial class SortableListView : ListView { private GridViewColumnHeader lastHeaderClicked = null; private ListSortDirection lastDirection = ListSortDirection.Ascending; public void GridViewColumnHeaderClicked(GridViewColumnHeader clickedHeader) { ListSortDirection direction; if (clickedHeader != null) { if (clickedHeader.Role != GridViewColumnHeaderRole.Padding) { if (clickedHeader != lastHeaderClicked) { direction = ListSortDirection.Ascending; } else { if (lastDirection == ListSortDirection.Ascending) { direction = ListSortDirection.Descending; } else { direction = ListSortDirection.Ascending; } } string sortString = ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path; Sort(sortString, direction); lastHeaderClicked = clickedHeader; lastDirection = direction; } } } private void Sort(string sortBy, ListSortDirection direction) { ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ItemsSource != null ? this.ItemsSource : this.Items); dataView.SortDescriptions.Clear(); SortDescription sD = new SortDescription(sortBy, direction); dataView.SortDescriptions.Add(sD); dataView.Refresh(); } }строку
((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Pathbit обрабатывает случаи, когда имена столбцов не совпадают с их путями привязки, чего не делает метод Microsoft.я хотел перехватить
GridViewColumnHeader.Clickсобытие, чтобы мне больше не нужно было думать об этом, но я не мог найти способ сделать это. В результате я добавляю следующее в XAML для каждогоSortableListView:GridViewColumnHeader.Click="SortableListViewColumnHeaderClicked"а потом на любой
Window, который содержит любое количествоSortableListViews, просто добавьте следующий код:private void SortableListViewColumnHeaderClicked(object sender, RoutedEventArgs e) { ((Controls.SortableListView)sender).GridViewColumnHeaderClicked(e.OriginalSource as GridViewColumnHeader); }здесь
Controls- это просто идентификатор XAML для пространства имен, в котором вы сделалиSortableListViewуправление.таким образом, это предотвращает дублирование кода на стороне сортировки, вам просто нужно помнить, чтобы обрабатывать событие, как указано выше.
Если у вас есть listview и превратить его в gridview вы можете легко сделать ваши заголовки столбцов gridview кликабельны, сделав это.
<Style TargetType="GridViewColumnHeader"> <Setter Property="Command" Value="{Binding CommandOrderBy}"/> <Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self},Path=Content}"/> </Style>затем просто установите команду делегата в коде.
public DelegateCommand CommandOrderBy { get { return new DelegateCommand(Delegated_CommandOrderBy); } } private void Delegated_CommandOrderBy(object obj) { throw new NotImplementedException(); }Я собираюсь предположить, что вы все знаете, как сделать ICommand DelegateCommand здесь. это позволило мне сохранить все мое представление, щелкнув в ViewModel.
Я только добавил Это так, что есть несколько способов сделать то же самое. Я не писал код для добавления кнопок со стрелками в заголовок, но это будет сделано в стиле XAML, вам нужно будет перепроектировать весь заголовок, который JanDotNet имеет в своем коде.
решение, которое суммирует все рабочие части существующих ответов и комментариев, включая шаблоны заголовков столбцов:
View:
<ListView x:Class="MyNamspace.MyListView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" ItemsSource="{Binding Items}" GridViewColumnHeader.Click="ListViewColumnHeaderClick"> <ListView.Resources> <Style TargetType="Grid" x:Key="HeaderGridStyle"> <Setter Property="Height" Value="20" /> </Style> <Style TargetType="TextBlock" x:Key="HeaderTextBlockStyle"> <Setter Property="Margin" Value="5,0,0,0" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style TargetType="Path" x:Key="HeaderPathStyle"> <Setter Property="StrokeThickness" Value="1" /> <Setter Property="Fill" Value="Gray" /> <Setter Property="Width" Value="20" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="Margin" Value="5,0,5,0" /> <Setter Property="SnapsToDevicePixels" Value="True" /> </Style> <DataTemplate x:Key="HeaderTemplateDefault"> <Grid Style="{StaticResource HeaderGridStyle}"> <TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" /> </Grid> </DataTemplate> <DataTemplate x:Key="HeaderTemplateArrowUp"> <Grid Style="{StaticResource HeaderGridStyle}"> <Path Data="M 7,3 L 13,3 L 10,0 L 7,3" Style="{StaticResource HeaderPathStyle}" /> <TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" /> </Grid> </DataTemplate> <DataTemplate x:Key="HeaderTemplateArrowDown"> <Grid Style="{StaticResource HeaderGridStyle}"> <Path Data="M 7,0 L 10,3 L 13,0 L 7,0" Style="{StaticResource HeaderPathStyle}" /> <TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" /> </Grid> </DataTemplate> </ListView.Resources> <ListView.View> <GridView ColumnHeaderTemplate="{StaticResource HeaderTemplateDefault}"> <GridViewColumn Header="Name" DisplayMemberBinding="{Binding NameProperty}" /> <GridViewColumn Header="Type" Width="45" DisplayMemberBinding="{Binding TypeProperty}"/> <!-- ... --> </GridView> </ListView.View> </ListView>Code Behinde:
public partial class MyListView : ListView { GridViewColumnHeader _lastHeaderClicked = null; public MyListView() { InitializeComponent(); } private void ListViewColumnHeaderClick(object sender, RoutedEventArgs e) { GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader; if (headerClicked == null) return; if (headerClicked.Role == GridViewColumnHeaderRole.Padding) return; var sortingColumn = (headerClicked.Column.DisplayMemberBinding as Binding)?.Path?.Path; if (sortingColumn == null) return; var direction = ApplySort(Items, sortingColumn); if (direction == ListSortDirection.Ascending) { headerClicked.Column.HeaderTemplate = Resources["HeaderTemplateArrowUp"] as DataTemplate; } else { headerClicked.Column.HeaderTemplate = Resources["HeaderTemplateArrowDown"] as DataTemplate; } // Remove arrow from previously sorted header if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked) { _lastHeaderClicked.Column.HeaderTemplate = Resources["HeaderTemplateDefault"] as DataTemplate; } _lastHeaderClicked = headerClicked; } public static ListSortDirection ApplySort(ICollectionView view, string propertyName) { ListSortDirection direction = ListSortDirection.Ascending; if (view.SortDescriptions.Count > 0) { SortDescription currentSort = view.SortDescriptions[0]; if (currentSort.PropertyName == propertyName) { if (currentSort.Direction == ListSortDirection.Ascending) direction = ListSortDirection.Descending; else direction = ListSortDirection.Ascending; } view.SortDescriptions.Clear(); } if (!string.IsNullOrEmpty(propertyName)) { view.SortDescriptions.Add(new SortDescription(propertyName, direction)); } return direction; } }
попробуйте это:
using System.ComponentModel; youtItemsControl.Items.SortDescriptions.Add(new SortDescription("yourFavoritePropertyFromItem",ListSortDirection.Ascending);
Comments