Meilleure façon de sortinger WPF ListView / GridView en cliquant sur l’en-tête de colonne?

Il existe de nombreuses solutions sur Internet qui tentent de combler cette omission apparemment très simple de WPF. Je suis vraiment confus quant à ce qui serait le “meilleur” moyen. Par exemple … Je veux qu’il y ait peu de flèches haut / bas dans l’en-tête de la colonne pour indiquer la direction du sorting. Il y a apparemment 3 façons différentes de faire ceci, certaines utilisant du code, d’autres utilisant du balisage, d’autres utilisant du balisage-plus-code, et toutes semblant plutôt être un hack.

Quelqu’un at-il déjà rencontré ce problème et trouvé une solution avec laquelle il est complètement satisfait? Il semble étrange qu’une fonctionnalité aussi simple de WinForms soit absente de WPF et qu’elle doive être piratée.

Tout dépend vraiment, si vous utilisez le DataGrid du WPF Toolkit, il existe un sorting intégré, même un sorting à plusieurs colonnes, ce qui est très utile. Vérifiez plus ici:

Blog Vincent Sibals

Si vous utilisez un autre contrôle qui ne prend pas en charge le sorting, je vous recommande les méthodes suivantes:

Le sorting sur mesure de Li Gao

Suivi par:

Tri plus rapide de Li Gao

J’ai écrit un ensemble de propriétés jointes pour sortinger automatiquement un GridView , vous pouvez le vérifier ici . Il ne gère pas la flèche haut / bas, mais il pourrait facilement être ajouté.

            

MSDN offre un moyen simple d’effectuer un sorting sur des colonnes comportant des glyphes haut / bas. L’exemple n’est pas complet, cependant, ils n’expliquent pas comment utiliser les modèles de données pour les glyphes. Voici ce que je dois travailler avec mon ListView. Cela fonctionne sur .Net 4.

Dans votre ListView, vous devez spécifier un gestionnaire d’événement à activer pour un clic sur GridViewColumnHeader. Mon ListView ressemble à ceci:

                 

Dans votre code, configurez le code pour gérer le sorting:

 // 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; } } ssortingng header = headerClicked.Column.Header as ssortingng; 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(ssortingng sortBy, ListSortDirection direction) { blcv.SortDescriptions.Clear(); SortDescription sd = new SortDescription(sortBy, direction); blcv.SortDescriptions.Add(sd); blcv.Refresh(); } 

Et puis, dans votre XAML, vous devez append deux DataTemplates que vous avez spécifiés dans la méthode de sorting:

             

L’utilisation de DockPanel avec LastChildFill définie sur true conservera le glyphe à droite de l’en-tête et laissera l’étiquette remplir le rest de l’espace. J’ai lié la largeur de DockPanel à la ActualWidth du GridViewColumnHeader car mes colonnes n’ont pas de largeur, ce qui leur permet de s’adapter automatiquement au contenu. J’ai toutefois défini MinWidth s sur les colonnes pour que le glyphe ne couvre pas le titre de la colonne. Le TextBlock Text est défini sur une liaison vide qui affiche le nom de colonne spécifié dans l’en-tête.

J’utilise MVVM, j’ai donc créé des propriétés jointes, en utilisant Thomas comme référence. Il effectue le sorting sur une colonne à la fois lorsque vous cliquez sur l’en-tête, en basculant entre croissant et décroissant. Il sortinge depuis le début en utilisant la première colonne. Et il montre des glyphes de style Win7 / 8.

Normalement, tout ce que vous avez à faire est de définir la propriété principale sur true (mais vous devez déclarer explicitement GridViewColumnHeaders):

                 

Si vous souhaitez sortinger sur une propriété différente de celle affichée, vous devez déclarer que:

    

Voici le code pour les propriétés jointes, j’aime être paresseux et les mettre dans le App.xaml.cs fourni:

 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(ssortingng), typeof(App), new UIPropertyMetadata(null) ); public static ssortingng GetGridViewSortPropertyName(GridViewColumn gvc) { return (ssortingng)gvc.GetValue(GridViewSortPropertyNameProperty); } public static void SetGridViewSortPropertyName(GridViewColumn gvc, ssortingng 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(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; ssortingng propertyName = GetGridViewSortPropertyName(gvc); if (ssortingng.IsNullOrEmpty(propertyName)) { Binding b = gvc.DisplayMemberBinding as Binding; if (b != null && b.Path != null) { propertyName = b.Path.Path; } if (ssortingng.IsNullOrEmpty(propertyName)) return; } ApplySort(lv.Items, propertyName); SetCurrentSortColumn(gv, gvc); CurrentSortColumnSetGlyph(gvc, lv); } public static void ApplySort(ICollectionView view, ssortingng propertyName) { if (ssortingng.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(DependencyObject source) where T : DependencyObject { return VisualUpwardSearch(source, x => x is T) as T; } public static DependencyObject VisualUpwardSearch( DependencyObject source, Predicate 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; } } } 

J’ai fait une adaptation de Microsoft, où je remplace le contrôle ListView pour créer un 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; } } ssortingng sortSsortingng = ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path; Sort(sortSsortingng, direction); lastHeaderClicked = clickedHeader; lastDirection = direction; } } } private void Sort(ssortingng 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(); } } 

La ligne ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path bit ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path gère les cas où vos noms de colonne ne sont pas les mêmes que leurs chemins de liaison, ce que la méthode Microsoft ne fait pas.

Je voulais intercepter l’événement GridViewColumnHeader.Click pour que je n’aie plus à y penser, mais je ne pouvais pas trouver le moyen de le faire. En conséquence, j’ajoute ce qui suit dans XAML pour chaque SortableListView :

 GridViewColumnHeader.Click="SortableListViewColumnHeaderClicked" 

Et puis, sur toute Window contenant un nombre quelconque de SortableListView , ajoutez simplement le code suivant:

 private void SortableListViewColumnHeaderClicked(object sender, RoutedEventArgs e) { ((Controls.SortableListView)sender).GridViewColumnHeaderClicked(e.OriginalSource as GridViewColumnHeader); } 

Where Controls est simplement l’ID XAML de l’espace de noms dans lequel vous avez créé le contrôle SortableListView .

Donc, cela empêche la duplication de code du côté du sorting, vous devez juste vous rappeler de gérer l’événement comme ci-dessus.

Solution qui résume toutes les parties actives des réponses et commentaires existants, y compris les modèles d’en-tête de colonne:

Vue:

                                

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, ssortingng 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 (!ssortingng.IsNullOrEmpty(propertyName)) { view.SortDescriptions.Add(new SortDescription(propertyName, direction)); } return direction; } } 

Si vous avez une vue de liste et la transformez en vue de grid, vous pouvez facilement faire en sorte que vos en-têtes de colonnes de vue de grid soient cliquables.

   

Ensuite, définissez simplement une commande de délégué dans votre code.

  public DelegateCommand CommandOrderBy { get { return new DelegateCommand(Delegated_CommandOrderBy); } } private void Delegated_CommandOrderBy(object obj) { throw new NotImplementedException(); } 

Je vais supposer que vous savez tous comment créer ICommand DelegateCommand ici. Cela m’a permis de garder toutes mes vues en cliquant dans le ViewModel.

J’ai seulement ajouté ceci pour qu’il y ait plusieurs façons d’accomplir la même chose. Je n’ai pas écrit de code pour append des boutons fléchés dans l’en-tête, mais cela se ferait dans le style XAML.

Essaye ça:

 using System.ComponentModel; youtItemsControl.Items.SortDescriptions.Add(new SortDescription("yourFavoritePropertyFromItem",ListSortDirection.Ascending);