相信你們對GridView都不陌生,是很是有用的控件,用於平鋪有序的顯示多個內容項。打開任何WinRT應用或者是微軟合做商的網站,都會在APP中發現GridView的使用。「Tiles」提供了一個簡單易用,平鋪方式來組織內容顯示。Windows8的開始菜單是最典型的GridView 示例。「開始菜單」顯示了系統中安裝的全部應用程序,並且支持從新排列。服務器
本文源於咱們項目的開發人員,他們想在項目中提供與GridView相同的用戶體驗,想要建立類GridView控件。session
GridView 能夠顯示大小不定的內容項,而且以有序的方式顯示。若是各個內容項無序,而且內容尺寸大小相同,GirdView還支持拖拽操做。然而,這些功能並非默認提供的,須要編寫必定的代碼才能實現。app
本文主要介紹了擴展GridView控件——稱爲GridViewEx, GridViewEx主要實現GridView在不一樣大小的內容項中的拖拽功能。async
首先了解GridView的基本屬性和功能,GridView包含一些屬性集和 ItemTemplate。爲了實現經過拖拽操做執行重排列功能,必須完成如下三件事:ide
1. 設置AllowDrop屬性爲true;佈局
2. 設置CanReorderItems 屬性值爲True;網站
3. 綁定數據源,該數據源必須支持數據修改或支持重排序。例如,使用ObservableCollection或IList數據源。ui
<GridView ItemsSource="{Binding}" AllowDrop="True" CanReorderItems="True"> <GridView.ItemTemplate> <DataTemplate> <Border BorderBrush="Aqua" BorderThickness="1" Background="Peru"> <Grid Margin="12"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Text="{Binding}"/> <TextBlock Grid.Row="1">item</TextBlock> </Grid> </Border> </DataTemplate> </GridView.ItemTemplate> </GridView>
擴展後的GridView使用拖拽操做將會很是方便快捷。this
GridViewEx控件彌補了GridView,功能以下:spa
咱們也爲GridViewEx增長了新建分組的功能,若是用戶將內容項拖到控件左邊或右邊時會觸發新建分組操做。
1: public class GridViewEx : GridView
2: {
3: /// <summary>
4: /// Initializes a new instance of the <see cref="GridViewEx"/> control.
5: /// </summary>
6: public GridViewEx()
7: {
8: // see attached sample
9: }
10:
11: private void GridViewEx_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
12: {
13: // see attached sample
14: }
15:
16: /// <summary>
17: /// Stores dragged items into DragEventArgs.Data.Properties["Items"] value.
18: /// Override this method to set custom drag data if you need to.
19: /// </summary>
20: protected virtual void OnDragStarting(DragItemsStartingEventArgs e)
21: {
22: // see attached sample
23: }
該控件包含幾個變量,用來存儲拖放內容的索引。OnDragStarting 事件在DragEventArgs.Data.Properties[「Items」] 中存儲拖拽的內容。OnDragStarting 須要根據本身的需求重寫。
當用戶拖拽某一項內容時,須要給用戶提示來引導用戶將內容放在合適的位置上。標準的GriView對象是經過滑動相鄰的內實項來實現的。本文將在GridViewEx中完善此操做。
1: /// <summary>
2: /// Shows reoder hints while custom dragging.
3: /// </summary>
4: protected override void OnDragOver(DragEventArgs e)
5: {
6: // see attached sample }
7:
8: private int GetDragOverIndex(DragEventArgs e)
9: {
10: // see attached sample
11: }
首先須要重寫GridView.OnDrop方法,該方法會當用戶釋放某一項內容時觸發。重寫Ondrop方法,代碼以下:
1: /// <summary>
2: /// Handles drag and drop for cases when it is not supported by the Windows.UI.Xaml.Controls.GridView control
3: /// </summary>
4: protected override async void OnDrop(DragEventArgs e)
5: {
6: // see attached sample
7: }
OnDrop方法主要實現了內容項從源分組移到目標分組的邏輯代碼,以及建立新分組的功能。
若是GrideView經過將IsSourceGrouped值爲True來綁定CollectionViewSource狀況下,GridView提供分組功能,這就意味着分組必須對數據源進行分組,但GridView沒有訪問數據的權限。所以本文在執行拖放操做時,實現添加新分組功能。GridViewEx.BeforeDrop事件處理此需求,而且提供更多的數據信息,如DragEventArgs數據。
當用戶執行拖放操做時,觸發BeforeDrop 事件。
1: /// <summary>
2: /// Occurs before performing drop operation,
3: /// </summary>
4: public event EventHandler<BeforeDropItemsEventArgs> BeforeDrop;
5: /// <summary>
6: /// Rises the <see cref="BeforeDrop"/> event.
7: /// </summary>
8: /// <param name="e">Event data for the event.</param>
9: protected virtual void OnBeforeDrop(BeforeDropItemsEventArgs e)
10: {
11: // see attached sample
12: }
BeforeDropItemEventArgs包含關於被拖拽的內容項的重要信息,該信息在OnDrop事件中可以使用的。
1: /// <summary>
2: /// Provides data for the <see cref="GridViewEx.BeforeDrop"/> event.
3: /// </summary>
4: public sealed class BeforeDropItemsEventArgs : System.ComponentModel.CancelEventArgs
5: {
6: /// <summary>
7: /// Gets the item which is being dragged.
8: /// </summary>
9: public object Item
10: {
11: get;
12: }
13: /// <summary>
14: /// Gets the current item index in the underlying data source.
15: /// </summary>
16: public int OldIndex
17: {
18: get;
19: }
20: /// <summary>
21: /// Gets the index in the underlying data source where
22: /// the item will be inserted by the drop operation.
23: /// </summary>
24: public int NewIndex
25: {
26: get;
27: }
28: /// <summary>
29: /// Gets the bool value determining whether end-user actions requested
30: /// creation of the new group in the underlying data source.
31: /// This property only makes sense if GridViewEx.IsGrouping property is true.
32: /// </summary>
33: /// <remarks>
34: /// If this property is true, create the new data group and insert it into
35: /// the groups collection at the positions, specified by the
36: /// <see cref="BeforeDropItemsEventArgs.NewGroupIndex"/> property value.
37: /// Then the <see cref="GridViewEx"/> will insert dragged item
38: /// into the newly added group.
39: /// </remarks>
40: public bool RequestCreateNewGroup
41: {
42: get;
43: }
44: /// <summary>
45: /// Gets the current item data group index in the underlying data source.
46: /// This property only makes sense if GridViewEx.IsGrouping property is true.
47: /// </summary>
48: public int OldGroupIndex
49: {
50: get;
51: }
52: /// <summary>
53: /// Gets the data group index in the underlying data source
54: /// where the item will be inserted by the drop operation.
55: /// This property only makes sense if GridViewEx.IsGrouping property is true.
56: /// </summary>
57: public int NewGroupIndex
58: {
59: get;
60: }
61: /// <summary>
62: /// Gets the original <see cref="DragEventArgs"/> data.
63: /// </summary>
64: public DragEventArgs DragEventArgs
65: {
66: get;
67: }
68: }
AllowNewGroup屬性肯定用戶拖拽某一內容項到控件邊界時,是否建立新組。GridView並無提供此功能,在GridViewEX添加此功能。
1: /// <summary>
2: /// Gets or sets the value determining whether new group should be created at
3: /// dragging the item to the empty space.
4: /// </summary>
5: public bool AllowNewGroup
6: {
7: get { return (bool)GetValue(AllowNewGroupProperty); }
8: set { SetValue(AllowNewGroupProperty, value); }
9: }
10:
11: /// <summary>
12: /// Identifies the <see cref="AllowNewGroup"/> dependency property.
13: /// </summary>
14: public static readonly DependencyProperty AllowNewGroupProperty =
15: DependencyProperty.Register("AllowNewGroup", typeof(bool),
16: typeof(GridViewEx), new PropertyMetadata(false));
爲了在拖拽過程當中添加分組,須要將AllowNewGroup屬性設置爲True。處理GridViewEx.BeforeDrop事件,該事件的參數可以幫助決定單項內容的起始位置和目的位置。在BeforeDrop事件的Handler中,使用 NewGroupIndex 建立新的數據組,並插入到已有組集合。最後,須要實現的擴展GridView控件模板。在用戶可拖拽的項目的位置建立新分組,並使用佔位符來代替。一旦用戶拖某一內容放置到控件的邊界時,觸發建立新分組,ItemsPresenter的兩個邊界元素是新組的佔位符。
GridViewEx控件模板generic.xaml,以下:
1: <Style TargetType="local:GridViewEx">
2: <Setter Property="Padding" Value="0,0,0,10" />
3: <Setter Property="IsTabStop" Value="False" />
4: <Setter Property="TabNavigation" Value="Once" />
5: <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
6: <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
7: <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Enabled" />
8: <Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" />
9: <Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled" />
10: <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="False" />
11: <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" />
12: <Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False" />
13: <Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True" />
14: <Setter Property="IsSwipeEnabled" Value="True" />
15: <Setter Property="Template">
16: <Setter.Value>
17: <ControlTemplate TargetType="local:GridViewEx">
18: <Border BorderBrush="{TemplateBinding BorderBrush}"
19: Background="{TemplateBinding Background}"
20: BorderThickness="{TemplateBinding BorderThickness}">
21: <ScrollViewer x:Name="ScrollViewer"
22: TabNavigation="{TemplateBinding TabNavigation}"
23: HorizontalScrollMode="
24: {TemplateBinding ScrollViewer.HorizontalScrollMode}"
25: HorizontalScrollBarVisibility=
26: "{TemplateBinding
27: ScrollViewer.HorizontalScrollBarVisibility}"
28: IsHorizontalScrollChainingEnabled=
29: "{TemplateBinding
30: ScrollViewer.IsHorizontalScrollChainingEnabled}"
31: VerticalScrollMode="
32: {TemplateBinding ScrollViewer.VerticalScrollMode}"
33: VerticalScrollBarVisibility=
34: "{TemplateBinding
35: ScrollViewer.VerticalScrollBarVisibility}"
36: IsVerticalScrollChainingEnabled=
37: "{TemplateBinding
38: ScrollViewer.IsVerticalScrollChainingEnabled}"
39: IsHorizontalRailEnabled="
40: {TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
41: IsVerticalRailEnabled="
42: {TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
43: ZoomMode="{TemplateBinding
44: ScrollViewer.ZoomMode}"
45: IsDeferredScrollingEnabled="
46: {TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
47: BringIntoViewOnFocusChange="
48: {TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}">
49: <StackPanel Orientation="Horizontal">
50: <Border Width="60"
51: x:Name="NewGroupPlaceHolderFirst"
52: Background="Transparent"
53: Padding="{TemplateBinding Padding}"
54: Visibility="{Binding AllowNewGroup,
55: Converter={StaticResource
56: VisibilityConverter},
57: RelativeSource={RelativeSource TemplatedParent}}"/>
58: <ItemsPresenter
59: Header="{TemplateBinding Header}"
60: HeaderTemplate="{TemplateBinding HeaderTemplate}"
61: HeaderTransitions="{TemplateBinding HeaderTransitions}"
62: Padding="{TemplateBinding Padding}"/>
63: <Border Width="60"
64: x:Name="NewGroupPlaceHolderLast"
65: Background="Transparent"
66: Padding="{TemplateBinding Padding}"
67: Visibility="{Binding AllowNewGroup,
68: Converter={StaticResource
69: VisibilityConverter},
70: RelativeSource={RelativeSource TemplatedParent}}"/>
71: </StackPanel>
72: </ScrollViewer>
73: </Border>
74: </ControlTemplate>
75: </Setter.Value>
76: </Setter>
77: </Style>
如上所示,咱們已經實現了基本的拖拽操做,與Windows8 開始菜單相似的功能,接下來討論如何實現如下功能:
Windows8展現了不一樣大小的Tiles,可是目前GridView或GridViewEx還不支持此功能。由於GridView使用WrapGrid做爲默認的ItemsPanel,WrapPanel只能建立一種佈局,即全部的條目尺寸相同的。所以微軟提供了VariableSizedWrapGrid,支持不一樣大小塊的佈局建立。
GridViewEx控件的優點在於可以使用VariableSizedWrapGrid,而且很好的支持拖放操做。爲了使用VariableSizedWrapGrid 並顯示不一樣大小的內容項,必須實現如下功能:
將GridViewEx.ItemsPanel 設置爲VariableSizedWrapGrid
在GridView中重寫GridView 的PrepareContainerForItemOverride 方法。在該方法中,能夠設置Item的RowSpan或ColumnSpan屬性來識別內容項的大小。
即生成繼承GridViewEx的新控件MyGridView。爲何須要擴展GridViewEx控件而不是重寫GridViewEx的PrepareContainerForItemOverride方法?由於指定Item尺寸的邏輯必須放在數據模型中,而不是控件內部。
如想將某一項顯示較大一點,須要在數據項中建立一個屬性返回比1大的整型數值,來設置RowSpanhuoColumnSpan屬性。
1: public class Item
2: {
3: public int Id { get; set; }
4: public int ItemSize { get; set; }
5: /* */
6: }
所以,當建立新的內容項,咱們要指定ItemSize屬性。若是值爲1則代表常規尺寸,若是值爲2則代表大尺寸,ColumnSpan屬性則設置爲2。
1: /// <summary>
2: /// This class sets VariableSizedWrapGrid.ColumnSpanProperty for GridViewItem controls,
3: /// so that every item can have different size in the VariableSizedWrapGrid.
4: /// </summary>
5: public class MyGridView : GridViewSamples.Controls.GridViewEx
6: {
7: // set ColumnSpan according to the business logic
8: // (maybe some GridViewSamples.Samples.Item or group properties)
9: protected override void PrepareContainerForItemOverride(
10: Windows.UI.Xaml.DependencyObject element, object item)
11: {
12: try
13: {
14: GridViewSamples.Samples.Item it = item as GridViewSamples.Samples.Item;
15: if (it != null)
16: {
17: element.SetValue(
18: Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, it.ItemSize);
19: }
20: }
21: catch
22: {
23: element.SetValue(Windows.UI.Xaml.Controls.VariableSizedWrapGrid.ColumnSpanProperty, 1);
24: }
25: finally
26: {
27: base.PrepareContainerForItemOverride(element, item);
28: }
29: }
30: }
建立MyGridView實例,並綁定到數據集合。
1: <local:MyGridView AllowDrop="True" CanReorderItems="True"
2: CanDragItems="True" IsSwipeEnabled="True"
3: ItemsSource="{Binding}"
4: ItemTemplate="{StaticResource ItemTemplate}" >
5: <GridView.ItemsPanel>
6: <ItemsPanelTemplate>
7: <VariableSizedWrapGrid ItemHeight="160"
8: ItemWidth="160" />
9: </ItemsPanelTemplate>
10: </GridView.ItemsPanel>
11: <GridView.ItemContainerStyle>
12: <Style TargetType="GridViewItem">
13: <Setter Property="HorizontalContentAlignment"
14: Value="Stretch"/>
15: <Setter Property="VerticalContentAlignment"
16: Value="Stretch"/>
17: </Style>
18: </GridView.ItemContainerStyle>
19: </local:MyGridView>
如上所示,咱們將指定內容項的ItemSize屬性設置爲2,效果如圖所示:
使用GridViewEx控件,可以實現添加新分組和拖拽等功能,也是在App中最爲常見的功能,實現分組必須完成如下設置:
在GridViewEx中添加支持不一樣大小的內容項,邏輯代碼:
1: <local:MyGridView AllowDrop="True" CanReorderItems="True"
2: CanDragItems="True" IsSwipeEnabled="True"
3: ItemsSource="{Binding}"
4: ItemTemplate="{StaticResource ItemTemplate}" >
5: <GridView.ItemsPanel>
6: <ItemsPanelTemplate>
7: <VirtualizingStackPanel Orientation="Horizontal"/>
8: </ItemsPanelTemplate>
9: </GridView.ItemsPanel>
10: <GridView.GroupStyle>
11: <GroupStyle>
12: <GroupStyle.HeaderTemplate>
13: <DataTemplate>
14: <Grid Background="LightGray"
15: Margin="0">
16: <TextBlock Foreground="Black"
17: Margin="10"
18: Style="{StaticResource
19: GroupHeaderTextStyle}">
20: <Run Text="{Binding Id}"/>
21: <Run Text=" group"/>
22: </TextBlock>
23: </Grid>
24: </DataTemplate>
25: </GroupStyle.HeaderTemplate>
26:
27: <GroupStyle.ContainerStyle>
28: <Style TargetType="GroupItem">
29: <Setter Property="BorderBrush"
30: Value="DarkGray"/>
31: <Setter Property="BorderThickness"
32: Value="2"/>
33: <Setter Property="Margin"
34: Value="3,0"/>
35: </Style>
36: </GroupStyle.ContainerStyle>
37:
38: <GroupStyle.Panel>
39: <ItemsPanelTemplate>
40: <VariableSizedWrapGrid ItemHeight="160"
41: ItemWidth="160" />
42: </ItemsPanelTemplate>
43: </GroupStyle.Panel>
44: </GroupStyle>
45: </GridView.GroupStyle>
46:
47: <GridView.ItemContainerStyle>
48: <Style TargetType="GridViewItem">
49: <Setter Property="HorizontalContentAlignment"
50: Value="Stretch"/>
51: <Setter Property="VerticalContentAlignment"
52: Value="Stretch"/>
53: </Style>
54: </GridView.ItemContainerStyle>
55: </local:MyGridView>
運行演示:
自定義的GridViewEx控件支持新分組的建立,所以須要設置AllowNewGroup爲True。其次處理添加新分組的數據層,處理GridViewEx.BeforeDrop 事件。
1: /// <summary>
2: /// Creates new CollectionViewSource and updates page DataContext.
3: /// </summary>
4: private void UpdateDataContext()
5: {
6: CollectionViewSource source = new CollectionViewSource();
7: source.Source = _groups;
8: source.ItemsPath = new PropertyPath("Items");
9: source.IsSourceGrouped = true;
10: this.DataContext = source;
11: }
12: // creates new group in the data source,
13: // if end-user drags item to the new group placeholder
14: private void MyGridView_BeforeDrop(object sender, Controls.BeforeDropItemsEventArgs e)
15: {
16: if (e.RequestCreateNewGroup)
17: {
18: // create new group and re-assign datasource
19: Group group = Group.GetNewGroup();
20: if (e.NewGroupIndex == 0)
21: {
22: _groups.Insert(0, group);
23: }
24: else
25: {
26: _groups.Add(group);
27: }
28: UpdateDataContext();
29: }
30: }
也可使用Drop事件刪除空分組
1: // removes empty groups (except the last one)
2: private void MyGridView_Drop(object sender, DragEventArgs e)
3: {
4: bool needReset = false;
5: for (int i = _groups.Count - 1; i >= 0; i--)
6: {
7: if (_groups[i].Items.Count == 0 && _groups.Count > 1)
8: {
9: _groups.RemoveAt(i);
10: needReset = true;
11: }
12: }
13: if (needReset)
14: {
15: UpdateDataContext();
16: }
17: }
Windows8支持掛起或終止功能,爲了提供更好的用戶體驗,咱們繼續改善此前實現的功能,當用戶離開當前頁面,將當前的佈局暫存。在本示例中,咱們使用JSON 字符串簡化數據序列化。根據已有的數據、數據的大小及需求,以其餘格式來保存數據。咱們主要將「業務對象集合」保存。
爲了節省佈局空間。重寫LayoutAwarePage方法:
1: /// <summary>
2: /// Populates the page with content passed during navigation. Any saved state is also
3: /// provided when recreating a page from a prior session.
4: /// </summary>
5: /// <param name="navigationParameter">The parameter value passed to
6: /// <see cref="Frame.Navigate(Type,
7: /// Object)"/> when this page was initially requested.
8: /// </param>
9: /// <param name="pageState"
10: /// >A dictionary of state preserved by this page during an earlier
11: /// session. This will be null the first time a page is visited.</param>
12: protected override void LoadState(Object navigationParameter,
13: Dictionary<String, Object> pageState)
14: {
15: base.LoadState(navigationParameter, pageState);
16: if (pageState != null && pageState.Count > 0
17: && pageState.ContainsKey("Groups"))
18: {
19: // restore groups and items from the previously serialized state
20: System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer =
21: new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List<Group>));
22: var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes
23: ((string)pageState["Groups"]));
24: _groups = (List<Group>)rootSer.ReadObject(stream);
25: }
26: else
27: {
28: // if we get here for the first time and don't have
29: // serialized content, fill groups and items from scratch
30: for (int j = 1; j <= 12; j++)
31: {
32: Group group = Group.GetNewGroup();
33: for (int i = 1; i <= 7 + j % 3; i++)
34: {
35: group.Items.Add(new Item()
36: {
37: Id = i,
38: GroupId = group.Id
39: });
40: }
41: _groups.Add(group);
42: }
43: }
44: UpdateDataContext();
45: }
46:
47: /// <summary>
48: /// Preserves state associated with this page in case the application is suspended or the
49: /// page is discarded from the navigation cache. Values must conform to the serialization
50: /// requirements of <see cref="SuspensionManager.SessionState"/>.
51: /// </summary>
52: /// <param name="pageState">
53: /// An empty dictionary to be populated with serializable state.</param>
54: protected override void SaveState(Dictionary<String, Object> pageState)
55: {
56: // save groups and items to JSON string so that
57: // it's possible to restore page state later
58: base.SaveState(pageState);
59: System.Runtime.Serialization.Json.DataContractJsonSerializer rootSer =
60: new System.Runtime.Serialization.Json.DataContractJsonSerializer
61: (typeof(List<Group>));
62: var stream = new MemoryStream();
63: rootSer.WriteObject(stream, _groups);
64: string str = System.Text.Encoding.UTF8.GetString(stream.ToArray(),
65: 0, (int)stream.Length);
66: pageState.Add("Groups", str);
67: }
68:
69: /// <summary>
70: /// Invoked when this page is about to be displayed in a Frame.
71: /// </summary>
72: /// <param name="e">Event data that describes
73: /// how this page was reached. The Parameter
74: /// property is typically used to configure the page.</param>
75: protected override void OnNavigatedTo(NavigationEventArgs e)
76: {
77: // restore page state
78: var frameState =
79: GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
80: if (frameState.ContainsKey("TilePageData"))
81: {
82: this.LoadState(e.Parameter,
83: (Dictionary<String, Object>)frameState["TilePageData"]);
84: }
85: else
86: {
87: this.LoadState(e.Parameter, null);
88: }
89: }
90:
91: protected override void OnNavigatedFrom(NavigationEventArgs e)
92: {
93: // save page state with "TilePageData" key
94: var frameState =
95: GridViewSamples.Common.SuspensionManager.SessionStateForFrame(this.Frame);
96: var pageState = new Dictionary<String, Object>();
97: this.SaveState(pageState);
98: frameState["TilePageData"] = pageState;
99: }
GridViewEx控件豐富了GirdView控件功能,改進了基礎功能,提高用戶體驗。到此已經實現了GridView項與Windows8開始菜單具備的相同用戶體驗,若是你想了解如何在Windows10平臺下開發UWP引用,請持續關注下篇文章:如何在Windows10中開發UWP應用
除了 GirdView之外,具有觸摸和鍵盤導航操做的自動或手動平鋪佈局的控件還有ComponentOne的TileControl爲WinForms ,它不但提供自適應Windows8的樣式佈局,還具備相似Windows8風格的交互體驗和靈活便捷的定製能力。
原文連接:http://www.codeproject.com/Articles/536519/Extending-GridView-with-Drag-and-Drop-for-Grouping