最好的方法来使WPF ListView / GridViewsorting列标题点击?
互联网上有很多解决scheme试图填补WPF这个看似非常基本的遗漏。 我真的很困惑什么是“最好”的方式。 例如…我想要在列标题中有很less的上/下箭头来指示sorting方向。 显然,有三种不同的方法可以做到这一点,一些使用代码,一些使用标记,一些使用标记加代码,所有的看起来就像一个黑客。
有没有人遇到过这个问题,并find了解决scheme,他们完全满意? 看起来奇怪的是,WPF中缺less这样一个基本的WinFormsfunction,并且需要被黑客入侵。
这一切都取决于,如果你使用WPF工具包中的DataGrid,那么就有一个内置的sorting,即使是多列sorting,这是非常有用的。 检查更多这里:
文森特Sibals博客
或者,如果您使用不支持sorting的不同控件,我会推荐以下方法:
李高的自定义sorting
其次是:
李高更快的sorting
我写了一组附加的属性来自动sortingGridView
,你可以在这里查看 。 它不处理向上/向下的箭头,但它可以很容易地添加。
<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有一个简单的方法来使用上/下字形对列进行sorting 。 但是这个例子并不完整 – 他们没有解释如何使用字形的数据模板。 下面是我的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>
在你的代码后面,设置代码来处理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; } } 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中,您需要添加您在sorting方法中指定的两个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>
将LastChildFill
设置为true的DockPanel
将使字形保持在标题的右侧,并让标签填充空格的其余部分。 我将DockPanel
宽度绑定到GridViewColumnHeader
的ActualWidth
,因为我的列没有宽度,可以让它们自动适应内容。 尽pipe我在列上设置了MinWidth
,所以这个字形没有掩盖列标题。 TextBlock Text
被设置为一个空的绑定,显示标题中指定的列名。
我使用MVVM,所以我创build了一些我自己的附属属性,使用Thomas作为参考。 它一次对一列进行sorting,当你点击标题时,在升序和降序之间切换。 它从一开始就使用第一列进行分类。 它显示Win7 / 8风格的字形。
通常,你所要做的就是将main属性设置为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>
如果您想对不同的显示属性进行sorting,则必须声明:
<GridViewColumn DisplayMemberBinding="{Binding Property3}" local:App.GridViewSortPropertyName="Property4"> <GridViewColumnHeader Content="Prop 3" /> </GridViewColumn>
下面是附加属性的代码,我喜欢懒惰,并把它们放在提供的App.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; } } }
我做了微软方式的改编,我重写了ListView
控件来创build一个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.Path
位处理列名与Microsoft绑定path不相同的情况,Microsoft方法不会这样做。
我想拦截GridViewColumnHeader.Click
事件,这样我就不必再考虑了,但是我找不到办法。 因此,我在每个SortableListView
XAML中添加以下内容:
GridViewColumnHeader.Click="SortableListViewColumnHeaderClicked"
然后在包含任何数量的SortableListView
的任何Window
上,只需添加以下代码:
private void SortableListViewColumnHeaderClicked(object sender, RoutedEventArgs e) { ((Controls.SortableListView)sender).GridViewColumnHeaderClicked(e.OriginalSource as GridViewColumnHeader); }
其中Controls
只是您在其中创build了SortableListView
控件的命名空间的XAML ID。
所以,这确实防止了sorting方面的代码重复,你只需要记住如上处理事件。
总结现有答案和评论的所有工作部分(包括列标题模板)的解决scheme:
视图:
<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>
代码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; } }
如果你有一个列表视图,并把它变成一个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中所有View的点击。
我只是加了这个,所以有多种方法来完成同样的事情。 我没有编写在头文件中添加箭头button的代码,但是这将在XAML样式中完成,您需要重新deviseJanDotNet在其代码中的整个头部。
尝试这个:
using System.ComponentModel; youtItemsControl.Items.SortDescriptions.Add(new SortDescription("yourFavoritePropertyFromItem",ListSortDirection.Ascending);