关于排序ItemsControl的文章(附带一些额外内容)






4.56/5 (47投票s)
一篇关于 ItemsControl 排序的文章(带一些额外内容)。
引言
当我开始这个应用程序时,我想要创建一个简单的应用程序,它演示了如何在 WPF 中的 ItemsControl
中进行排序。我不得不说,我在这方面有点走火入魔,我创造的比我最初设想的要多一点(不多,但确实多了一点)。在这篇文章中,我们将介绍以下内容:
- 如何创建可滚动设计区域
- 设计自定义面板
- 如何巧妙地使用 Adorners
- 如何使用
CollectionView
对ItemsControl
进行排序
我将在此过程中更详细地解释每一个。
目录
展示所附演示的视频
自从我发现如何制作视频并且我的主机也不是很差之后,我觉得如果视频对文章有意义,那么添加视频是件好事。所以这篇文章也有一个视频。
我建议等待整个视频流式传输完成,然后再观看。这样会最有意义。 |
架构概述
我不会解释这些类中的每一个,而是会突出重点。我认为上面目录中列出的主要要点值得介绍,所以我会花一点时间在这些方面,但除此之外,如果你感兴趣,只需深入研究代码即可。
创建可滚动设计表面(带摩擦)
你是否曾有过这样的需求,需要用户能够在一个大对象(例如图表)周围滚动?嗯,这正是我为本文所设定的需求。我想创建一个自定义的 Panel
,我不知道它会有多大,所以我希望用户能够使用鼠标在周围滚动。
我们可能都知道 WPF 有一个 ScrollViewer
控件,它允许用户使用滚动条滚动,这很好,但它看起来很丑。我希望用户根本不会真正意识到存在滚动区域;我希望他们只使用鼠标在大区域内平移。
为此,我开始四处寻找,并拼凑出了标准 ScrollViewer
控件的以下子类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace ScrollControl
{
/// <summary>
/// Provides a scrollable ScrollViewer which
/// allows user to apply friction, which in turn
/// animates the ScrollViewer position, giving it
/// the appearance of sliding into position
/// </summary>
public class FrictionScrollViewer : ScrollViewer
{
#region Data
// Used when manually scrolling.
private DispatcherTimer animationTimer = new DispatcherTimer();
private Point previousPoint;
private Point scrollStartOffset;
private Point scrollStartPoint;
private Point scrollTarget;
private Vector velocity;
#endregion
#region Ctor
/// <summary>
/// Overrides metadata
/// </summary>
static FrictionScrollViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(FrictionScrollViewer),
new FrameworkPropertyMetadata(typeof(FrictionScrollViewer)));
}
/// <summary>
/// Initialises all friction related variables
/// </summary>
public FrictionScrollViewer()
{
Friction = 0.95;
animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
animationTimer.Tick += HandleWorldTimerTick;
animationTimer.Start();
}
#endregion
#region DPs
/// <summary>
/// The ammount of friction to use. Use the Friction property to set a
/// value between 0 and 1, 0 being no friction 1 is full friction
/// meaning the panel won’t "auto-scroll".
/// </summary>
public double Friction
{
get { return (double)GetValue(FrictionProperty); }
set { SetValue(FrictionProperty, value); }
}
// Using a DependencyProperty as the backing store for Friction.
public static readonly DependencyProperty FrictionProperty =
DependencyProperty.Register("Friction", typeof(double),
typeof(FrictionScrollViewer), new UIPropertyMetadata(0.0));
#endregion
#region overrides
/// <summary>
/// Get position and CaptureMouse
/// </summary>
/// <param name="e"></param>
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
if (IsMouseOver)
{
// Save starting point, used later when determining how much to scroll.
scrollStartPoint = e.GetPosition(this);
scrollStartOffset.X = HorizontalOffset;
scrollStartOffset.Y = VerticalOffset;
// Update the cursor if can scroll or not.
Cursor = (ExtentWidth > ViewportWidth) ||
(ExtentHeight > ViewportHeight) ?
Cursors.ScrollAll : Cursors.Arrow;
CaptureMouse();
}
base.OnPreviewMouseDown(e);
}
/// <summary>
/// If IsMouseCaptured scroll to correct position.
/// Where position is updated by animation timer
/// </summary>
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (IsMouseCaptured)
{
Point currentPoint = e.GetPosition(this);
// Determine the new amount to scroll.
Point delta = new Point(scrollStartPoint.X -
currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
scrollTarget.X = scrollStartOffset.X + delta.X;
scrollTarget.Y = scrollStartOffset.Y + delta.Y;
// Scroll to the new position.
ScrollToHorizontalOffset(scrollTarget.X);
ScrollToVerticalOffset(scrollTarget.Y);
}
base.OnPreviewMouseMove(e);
}
/// <summary>
/// Release MouseCapture if its captured
/// </summary>
/// <param name="e"></param>
protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
{
if (IsMouseCaptured)
{
Cursor = Cursors.Arrow;
ReleaseMouseCapture();
}
base.OnPreviewMouseUp(e);
}
#endregion
#region Animation timer Tick
/// <summary>
/// Animation timer tick, used to move the scrollviewer incrementally
/// to the desired position. This also uses the friction setting
/// when determining how much to move the scrollviewer
/// </summary>
private void HandleWorldTimerTick(object sender, EventArgs e)
{
if (IsMouseCaptured)
{
Point currentPoint = Mouse.GetPosition(this);
velocity = previousPoint - currentPoint;
previousPoint = currentPoint;
}
else
{
if (velocity.Length > 1)
{
ScrollToHorizontalOffset(scrollTarget.X);
ScrollToVerticalOffset(scrollTarget.Y);
scrollTarget.X += velocity.X;
scrollTarget.Y += velocity.Y;
velocity *= Friction;
}
}
}
#endregion
}
}
我可以在 XAML 中按如下方式使用它
<UserControl.Resources>
<!-- scroll viewer -->
<Style x:Key="ScrollViewerStyle" TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="VerticalScrollBarVisibility" Value="Hidden" />
</Style>
</UserControl.Resources>
....
....
<!-- Content Start-->
<local:FrictionScrollViewer x:Name="ScrollViewer"
Style="{StaticResource ScrollViewerStyle}">
<ItemsControl x:Name="itemsControl"
Style="{StaticResource mainPanelStyle}">
....
....
</ItemsControl>
</local:FrictionScrollViewer>
我们在这里所做的只是创建我的 FrictionScrollViewer
的一个实例,它承载了一个单独的 ItemsControl
。您可能会注意到这个类的名称是 FrictionScrollViewer
,所以很自然地,滚动运动会涉及到一些摩擦。实际发生的是,用户的鼠标移动被跟踪,并且每次时间间隔滴答时,DispatchTimer
用于更新子类化的 ScrollViewer
水平/垂直偏移。它看起来相当不错。
当用户滚动时,鼠标光标会变为滚动光标。
你仍然需要为 FrictionScrollViewer
设置 Style
,以便隐藏滚动条。我可以在代码中完成此操作,但我不知道人们会想要什么,所以将该选项留作一个可通过 Style
修复的东西。我个人不想要滚动条,但有些人可能会,所以只需更改 XAML 中显示的 Style
。
在 WPF 中创建自定义面板
在 WPF 中创建自定义面板实际上非常酷。假设您不喜欢当前的布局选项(StackPanel
/Grid
/Canvas
/DockPanel
);我们将自己编写。
你知道,有时候你想要一份定制的工作。虽然使用现有布局组合创建大多数作品可能是真的,但有时将其包装到自定义面板中会更方便。
我的一位 WPF 同门 Rudi Grobler 最近发布了一个包含来自许多不同来源的许多不同自定义面板的单一应用程序,在一个包含的演示应用程序中。这可以在 Rudi 的博客上找到。如果你想要更多面板,可以去那里看看。
现在,在创建自定义面板时,您只需要重写两个方法,它们是
Size MeasureOverride(Size constraint)
Size ArrangeOverride(Size arrangeBounds)
我见过关于创建自定义面板的最佳文章之一是 Paul Tallett 在 CodeProject 上的文章 Fisheye Panel;引用 Paul 的精彩文章:
要启动您自己的自定义 Panel
,您需要从 System.Windows.Controls.Panel
派生并实现两个重写:MeasureOverride
和 LayoutOverride
。这些实现了两遍布局系统,其中在测量阶段,您的父级会调用您以查看您想要多少空间。您通常会询问您的子级想要多少空间,然后将结果返回给父级。在第二遍中,有人决定所有内容的大小,并将最终大小传递给您的 ArrangeOverride
方法,您在该方法中告诉子级它们的大小并布置它们。请注意,每次您做一些影响布局的事情(例如,调整窗口大小),所有这些都会以新的大小再次发生。
我希望为本文实现的是一个基于列的 Panel
,当当前列空间不足时,它会换到新的一列。现在,我本可以直接使用一个包含大量垂直 StackPanel
的 DockPanel
,但这违背了我的目的。我希望面板能够根据可用空间计算出一列中有多少项。
于是我开始探索,并在 Mathew McDonald 的优秀著作 Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5 中找到了一个绝佳的起点,所以我的代码很大程度上基于 Mathew 书中的示例。
我的 ColumnedPanel
最重要的方法如下所示
#region Measure Override
// From MSDN : When overridden in a derived class, measures the
// size in layout required for child elements and determines a
// size for the FrameworkElement-derived class
protected override Size MeasureOverride(Size constraint)
{
Size currentColumnSize = new Size();
Size panelSize = new Size();
foreach (UIElement element in base.InternalChildren)
{
element.Measure(constraint);
Size desiredSize = element.DesiredSize;
if (GetColumnBreakBefore(element) ||
currentColumnSize.Height + desiredSize.Height > constraint.Height)
{
// Switch to a new column (either because
// the element has requested it
// or space has run out).
panelSize.Height =
Math.Max(currentColumnSize.Height, panelSize.Height);
panelSize.Width += currentColumnSize.Width;
currentColumnSize = desiredSize;
// If the element is too high to fit
// using the maximum height of the line,
// just give it a separate column.
if (desiredSize.Height > constraint.Height)
{
panelSize.Height =
Math.Max(desiredSize.Height, panelSize.Height);
panelSize.Width += desiredSize.Width;
currentColumnSize = new Size();
}
}
else
{
// Keep adding to the current column.
currentColumnSize.Height += desiredSize.Height;
// Make sure the line is as wide as its widest element.
currentColumnSize.Width =
Math.Max(desiredSize.Width, currentColumnSize.Width);
}
}
// Return the size required to fit all elements.
// Ordinarily, this is the width of the constraint, and the height
// is based on the size of the elements.
// However, if an element is higher than the height given to the panel,
// the desired width will be the height of that column.
panelSize.Height = Math.Max(currentColumnSize.Height, panelSize.Height);
panelSize.Width += currentColumnSize.Width;
return panelSize;
}
#endregion
#region Arrange Override
//From MSDN : When overridden in a derived class, positions child
//elements and determines a size for a FrameworkElement derived
//class.
protected override Size ArrangeOverride(Size arrangeBounds)
{
int firstInLine = 0;
Size currentColumnSize = new Size();
double accumulatedWidth = 0;
UIElementCollection elements = base.InternalChildren;
for (int i = 0; i < elements.Count; i++)
{
Size desiredSize = elements[i].DesiredSize;
if (GetColumnBreakBefore(elements[i]) || currentColumnSize.Height
+ desiredSize.Height > arrangeBounds.Height)
//need to switch to another column
{
arrangeColumn(accumulatedWidth, currentColumnSize.Width,
firstInLine, i, arrangeBounds);
accumulatedWidth += currentColumnSize.Width;
currentColumnSize = desiredSize;
//the element is higher than
// the constraint - give it a separate column
if (desiredSize.Height > arrangeBounds.Height)
{
arrangeColumn(accumulatedWidth, desiredSize.Width, i, ++i,
arrangeBounds);
accumulatedWidth += desiredSize.Width;
currentColumnSize = new Size();
}
firstInLine = i;
}
else //continue to accumulate a column
{
currentColumnSize.Height += desiredSize.Height;
currentColumnSize.Width =
Math.Max(desiredSize.Width, currentColumnSize.Width);
}
}
if (firstInLine < elements.Count)
arrangeColumn(accumulatedWidth, currentColumnSize.Width,
firstInLine, elements.Count, arrangeBounds);
return arrangeBounds;
}
#endregion
#region Private Methods
/// <summary>
/// Arranges a single column of elements
/// </summary>
private void arrangeColumn(double x, double columnWidth,
int start, int end, Size arrangeBounds)
{
double y = 0;
double totalChildHeight = 0;
double widestChildWidth = 0;
double xOffset = 0;
UIElementCollection children = InternalChildren;
UIElement child;
for (int i = start; i < end; i++)
{
child = children[i];
totalChildHeight += child.DesiredSize.Height;
if (child.DesiredSize.Width > widestChildWidth)
widestChildWidth = child.DesiredSize.Width;
}
//work out y start offset within a given column
y = ((arrangeBounds.Height - totalChildHeight) / 2);
for (int i = start; i < end; i++)
{
child = children[i];
if (child.DesiredSize.Width < widestChildWidth)
{
xOffset = ((widestChildWidth - child.DesiredSize.Width) / 2);
}
child.Arrange(new Rect(x + xOffset, y, child.DesiredSize.Width, columnWidth));
y += child.DesiredSize.Height;
xOffset = 0;
}
}
#endregion
然后我可以在任何可以使用标准 Panel
的地方使用此面板。我实际上将其用作 ItemsControl
面板,如下所示,其中 ItemsControl
是我上面提到的 FrictionScrollViewer
所使用的面板。
<ItemsControl x:Name="itemsControl"
Style="{StaticResource mainPanelStyle}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:ColumnedPanel IsItemsHost="True"
Loaded="OnPanelLoaded"
MinHeight="360" Height="360"
VerticalAlignment="Center"
Background="CornflowerBlue"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
使用 Adorners
如果你来自 WinForms 世界(就像我一样),Adorners 的概念可能有点陌生。以下是 MSDN 对 Adorners 的简要介绍:
Adorners 是一种特殊的 FrameworkElement
类型,用于向用户提供视觉提示。除了其他用途,Adorners 可以用于向元素添加功能句柄或提供有关控件的状态信息。
Adorner 是一种绑定到 UIElement
的自定义 FrameworkElement
。Adorners 在 AdornerLayer
中渲染,AdornerLayer
是一个始终位于被修饰元素或被修饰元素集合上方的渲染表面。Adorner 的渲染独立于 Adorner 绑定到的 UIElement
的渲染。Adorner 通常相对于其绑定的元素定位,使用位于被修饰元素左上角的标准 2D 坐标原点。
Adorners 的常见应用包括
- 向
UIElement
添加功能手柄,使用户能够以某种方式操纵元素(调整大小、旋转、重新定位等) - 提供视觉反馈以指示各种状态,或响应各种事件
- 在
UIElement
上叠加视觉装饰 - 在视觉上遮蔽或覆盖
UIElement
的部分或全部
所附项目实际上使用了两个 Adorner,一个用于主 ScrollerControl
(即 ScrollerControlAdorner
),另一个用于 ItemHolder
(ItemHolderAdorner
)。这些将在下面进行描述。
ScrollerControlAdorner
提供了两个额外的按钮,它们在 AdornerLayer
上创建和管理。这些按钮允许 Adorner 调用 AdornedElement
(ScrollerControl
) 中的 Scroll(Point delta)
方法。有一个 DispatchTimer
用于在鼠标悬停在 Adorner 中的按钮上时调用 Scroll(Point delta)
。这个 Adorner 最重要的部分是构造函数/计时器,如下所示:
#region Constructor
public ScrollerControlAdorner(ScrollerControl sc)
: base(sc)
{
timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
timer.IsEnabled = false;
timer.Tick += new EventHandler(timer_Tick);
this.sc = sc;
host.Width = (double)this.AdornedElement.GetValue(ActualWidthProperty);
host.Height = (double)this.AdornedElement.GetValue(ActualHeightProperty);
host.VerticalAlignment = VerticalAlignment.Center;
host.HorizontalAlignment = HorizontalAlignment.Left;
host.Margin = new Thickness(0);
Button btnLeft = new Button();
Style styleLeft = sc.TryFindResource("leftButtonStyle") as Style;
if (styleLeft != null)
btnLeft.Style = styleLeft;
btnLeft.MouseEnter += new System.Windows.Input.MouseEventHandler(btnLeft_MouseEnter);
btnLeft.MouseLeave += new System.Windows.Input.MouseEventHandler(MouseLeave);
Point parentTopleft = sc.TranslatePoint(new Point(0, 0), sc.Parent as UIElement);
double top = ((host.Height / 2) - (GLOBALS.leftRightButtonScrollSize / 2) -
(parentTopleft.Y / 2) - (GLOBALS.footerPanelHeight/2));
btnLeft.SetValue(Canvas.TopProperty, top);
btnLeft.SetValue(Canvas.LeftProperty, (double)spacer);
Button btnRight = new Button();
Style styleRight = sc.TryFindResource("rightButtonStyle") as Style;
if (styleRight != null)
btnRight.Style = styleRight;
btnRight.MouseEnter +=
new System.Windows.Input.MouseEventHandler(btnRight_MouseEnter);
btnRight.MouseLeave += new System.Windows.Input.MouseEventHandler(MouseLeave);
btnRight.SetValue(Canvas.TopProperty, top);
btnRight.SetValue(Canvas.LeftProperty,
(double)(host.Width - (GLOBALS.leftRightButtonScrollSize + spacer)));
host.Children.Add(btnLeft);
host.Children.Add(btnRight);
base.AddLogicalChild(host);
base.AddVisualChild(host);
}
#endregion // Constructor
#region Private Methods
private void timer_Tick(object sender, EventArgs e)
{
switch (currentDirection)
{
case ScrollDirection.Left:
sc.Scroll(new Point(10, 0));
break;
case ScrollDirection.Right:
sc.Scroll(new Point(-10, 0));
break;
}
}
/// <summary>
/// Sets currentDirection to None and disables scroll timer
/// </summary>
new private void MouseLeave(object sender,
System.Windows.Input.MouseEventArgs e)
{
currentDirection = ScrollDirection.None;
timer.IsEnabled = false;
}
/// <summary>
/// Sets currentDirection to Left and enables scroll timer
/// </summary>
private void btnLeft_MouseEnter(object sender,
System.Windows.Input.MouseEventArgs e)
{
currentDirection = ScrollDirection.Left;
timer.IsEnabled = true;
}
/// <summary>
/// Sets currentDirection to Right and enables scroll timer
/// </summary>
private void btnRight_MouseEnter(object sender,
System.Windows.Input.MouseEventArgs e)
{
currentDirection = ScrollDirection.Right;
timer.IsEnabled = true;
}
#endregion
ItemHolderAdorner
提供 AdornedElement
(ItemHolder
) 的视觉副本,然后将其放大一些。视觉副本是通过制作 AdornedElement
的 VisualBrush
来实现的。此 Adorner 最重要的部分是构造函数,如下所示:
public ItemHolderAdorner(ItemHolder adornedElement, Point position)
: base(adornedElement)
{
host.Width = adornedElement.Width;
host.Height = adornedElement.Height;
host.HorizontalAlignment = HorizontalAlignment.Left;
host.VerticalAlignment = VerticalAlignment.Top;
host.IsHitTestVisible = false;
this.IsHitTestVisible = false;
Border outerBorder = new Border();
outerBorder.Background = Brushes.White;
outerBorder.Margin = new Thickness(0);
outerBorder.CornerRadius = new CornerRadius(5);
outerBorder.Width = adornedElement.Width;
outerBorder.Height = adornedElement.Height;
Border innerBorder = new Border();
innerBorder.Background = Brushes.CornflowerBlue;
innerBorder.Margin = new Thickness(1);
innerBorder.CornerRadius = new CornerRadius(5);
outerBorder.Child = innerBorder;
innerBorder.Child = new Grid
{
Background = new VisualBrush(adornedElement as Visual),
Margin = new Thickness(1)
};
double scale = 1.5;
host.RenderTransform = new ScaleTransform(scale, scale, -0.5, -0.5);
double hostWidth = host.Width * scale;
double diff = (double)(adornedElement.Width / 4);
Thickness margin = new Thickness();
margin.Top = diff * -1;
margin.Left = diff * -1;
host.Margin = margin;
host.Children.Add(outerBorder);
base.AddLogicalChild(host);
base.AddVisualChild(host);
}
使用 CollectionView 排序 ItemsControl
正如我在本文开头所说,这实际上是撰写本文的主要原因... 但我跑题了,写了其余部分。
在 WPF 中使用 ItemsControl
(或其子类,如 ListBox
)进行排序可以通过 XAML 或代码隐藏来实现。我正在使用代码隐藏,但我也会演示一个 XAML 示例。
您首先需要了解的是您正在与 ItemsControl
一起使用的模式类型;如果您正在使用 ItemsSource
,那么您处于我称之为非直接模式;如果您正在使用 Items.Add()
添加项目,那么您处于我称之为直接模式。
如果您有一个带有内容的 ItemsControl
(例如 ListBox
),您可以使用 Items
属性访问 ItemCollection
,这是一个视图。因为它是一个视图,所以您可以使用与视图相关的功能,例如排序、筛选和分组。请注意,当设置 ItemsSource
时,视图操作会委托给 ItemsSource
集合上的视图。因此,ItemCollection
仅在委托视图支持排序、筛选和分组时才支持这些功能。
以下示例演示了如何对名为 myListBox
的 ListBox
的内容进行排序。在此示例中,Content
是要排序的属性名称。
myListBox.Items.SortDescriptions.Add(new SortDescription("Content",
ListSortDirection.Descending));
当您这样做时,视图可能是也可能不是默认视图,具体取决于您的 ItemsControl
上数据的设置方式。例如,当 ItemsSource
属性绑定到 CollectionViewSource
时,您使用 Items
属性获取的视图不是默认视图。
如果您的 ItemsControl
已绑定(您正在使用 ItemsSource
属性),那么您可以执行以下操作以获取默认视图:
CollectionView myView myView =
(CollectionView)CollectionViewSource.GetDefaultView(myItemsControl.ItemsSource);
在所附的演示项目中,我正在根据 ObservableCollection<ItemHolder>
(ItemList
) 中存储的 ItemHolder
控件上可用的公共属性设置新的排序,该 ObservableCollection<ItemHolder>
用作 ItemsControl
的 ItemsSource
。
public void SortItems(SortingType sort)
{
CollectionView defaultView =
(CollectionView)CollectionViewSource.
GetDefaultView(itemsControl.ItemsSource);
switch (sort)
{
case SortingType.Normal:
defaultView.SortDescriptions.Clear();
SetItemsCurrentSort(sort);
break;
case SortingType.ByDate:
defaultView.SortDescriptions.Add(new SortDescription("FileDate",
ListSortDirection.Descending));
SetItemsCurrentSort(sort);
break;
case SortingType.ByExtension:
defaultView.SortDescriptions.Add(new SortDescription("FileExtension",
ListSortDirection.Descending));
SetItemsCurrentSort(sort);
break;
case SortingType.BySize:
defaultView.SortDescriptions.Add(new SortDescription("FileSize",
ListSortDirection.Descending));
SetItemsCurrentSort(sort);
break;
default:
defaultView.SortDescriptions.Clear();
SetItemsCurrentSort(SortingType.Normal);
break;
}
}
要在 XAML 中执行类似操作,我们可以简单地执行类似操作:
以下示例创建了一个数据集合的视图,该视图按城市名称排序,并按州分组
<Window.Resources>
<src:Places x:Key="places"/>
<CollectionViewSource Source="{StaticResource places}" x:Key="cvs">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="CityName"/>
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<dat:PropertyGroupDescription PropertyName="State"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
该视图然后可以作为绑定源,如以下示例所示
<ListBox ItemsSource="{Binding Source={StaticResource cvs}}"
DisplayMemberPath="CityName" Name="lb">
<ListBox.GroupStyle>
<x:Static Member="GroupStyle.Default"/>
</ListBox.GroupStyle>
</ListBox>
结论
我在等待上一篇文章的答案时开始了这篇文章,但你知道,我对这篇文章的结果感到非常满意。我认为它展示了一系列不错的东西,例如:
- 如何创建可滚动设计区域
- 设计自定义面板
- 如何巧妙地使用 Adorners
- 如何使用
CollectionView
对列表进行排序
所以,希望这里仍然有你可以使用的东西。