在WPF的時代,我們多是使用ListBox和ListView來展示,縱向滾動條顯示的集合資料。這兩個控制元件的預設樣式,以及對觸控的支援,已完全落後於時代。他們兩個分別長這樣,和Win10及Win11的風格完全不沾邊。
今天我來看下WinUI 3中適用於Desktop App的ListView,我們略過ListBox是因為ListBox使用較為簡單,通常用於將少量資料同時顯示在頁面上。
而顯示需要滾動條的大量資料,會推薦使用ListView,同時ListView的表現形式也較為豐富。例如分組,拖拽,縮放,以及頁頭頁尾的自定義。
本篇我們將試著應用以上這些特性到Desktop App中,通過WinUI 3庫,我們不再需要Xaml Islands這樣曲線救國的做法。而是真正在Desktop App中用上原生的新ListView。
首先我們來看分組,WinUI 3中的ListView通過CollectionViewSource這個元件來實現分組功能。當然我們也可以通過巢狀集合的方式來實現,例如將ListView的ListViewItem同樣也包含一個ListView/ItemsControl。但是這樣做有一些缺陷,第一是會破壞UI虛擬化(UI virtualization),因為用於虛擬化的容器(Item Container)在面對重複的Item才會起到效果,用ListView/ItemsControl這樣的可變集合作為Item,在數量增多後會有明顯的效能問題。第二是巢狀集合的話,SeletedItem和ItemClick處理起來會比較困難。所以我更推薦使用CollectionViewSource。
ListView的分組並不複雜,在XAML中我們需要於Resources節點中放置CollectionViewSource物件,該物件是真正資料來源的一個檢視。我們可以對這個檢視做分組和排序,但不會自動影響到真正的資料來源。
假設我們有一個Person類:
public class Person { public string Name { get; set; } }
並構建了PersionList作為資料來源,同時建立了分組檢視PersonGroup。
this.PersonList = new List<Person> { new Person{ Name = "Abe"}, new Person{ Name = "Alice"}, new Person{ Name = "Bell"}, new Person{ Name = "Ben"}, new Person{ Name = "Bob"}, new Person{ Name = "Fox"}, new Person{ Name = "Gray"}, new Person{ Name = "James"}, new Person{ Name = "Jane"}, new Person{ Name = "Roy"}, new Person{ Name = "Vincent"} }; PersonGroup = PersonList.GroupBy(p => p.Name.First().ToString());
那麼我們在XAML中,將通過如下形式的binding來使用該分組檢視:
<Grid> <Grid.Resources> <CollectionViewSource x:Name="personListCVS" IsSourceGrouped="True" Source="{x:Bind PersonGroup}"/> </Grid.Resources> <ListView ItemsSource="{x:Bind personListCVS.View}"> <ListView.GroupStyle> <GroupStyle> <GroupStyle.HeaderTemplate> <DataTemplate> <TextBlock Text="{Binding Key}"/> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </ListView.GroupStyle> <ListView.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}"></TextBlock> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid>
上面的XAML中定義簡單的僅TextBlock的HeaderTemplate和ItemTemplate,實際生產中可按需編寫更復雜的模板。
接下來我們看拖拽的實現,這裡我們建立一個DragDropPage.xaml,假設ListView作為被拖走資料的一方,在DataTemplate的子節點上,將CanDrag設定為True,因為接受資料的需要,我們要明確被拖拽走的是個什麼東西,通過DragStarting事件可以獲得被操作的UIElement。
<ListView Grid.Column="0" ItemsSource="{x:Bind PersonList}"> <ListView.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}" CanDrag="True" DragStarting="TextBlock_DragStarting"></TextBlock> </DataTemplate> </ListView.ItemTemplate> </ListView>
作為接受拖拽物件的ComboxBox,除了設定AllowDrop=True以外,還需要通過DragOver來響應圖示的變化(假設在滑鼠移過來後,圖示從“無效”的樣式轉變成了“複製”,表示可以接受該資料),以及通過Drop來處理接受資料。
<ComboBox Grid.Column="1" ItemsSource="{x:Bind PersonList}" SelectedItem="{x:Bind SelectedPerson,Mode=TwoWay}" AllowDrop="True" DragOver="ComboBox_DragOver" Drop="ComboBox_Drop"> <ComboBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Name}"></TextBlock> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>
在建立本篇的Sample Code時,WinUI 3 的SDK版本已經從0.5更新到0.8了(穩定版)。但是在Drop事件裡依然遇到了bug,無法訪問DragEventArgs中的DataView來獲取DragStarting時存放的資料。這個問題已經有大量的issue開給了某軟,並不是WinUI 3庫本身的bug,鍋已丟給CsWinRT專案,但什麼時候修好就不知道了。
Unable to drop files onto Grid in WinUI3 Desktop · Issue #2715 · microsoft/microsoft-ui-xaml (github.com)
DataView無法使用,直接影響了應用間的資料傳遞。對於本篇的APP內部的拖拽,倒是可以寫個Property繞過去。拖拽操作的流程先是通過DragStarting來獲取和存放要傳遞的資料。DragOver負責更改圖示,反饋給使用者某個控制元件可以接受Drop操作。最後由Drop來接受資料和更新UI。
private void ComboBox_Drop(object sender, DragEventArgs e) { //if (e.DataView.Contains(StandardDataFormats.Text)) //{ // var name = await e.DataView.GetTextAsync(); // this.SelectedPerson = this.PersonList.FirstOrDefault(p => p.Name == name); //} this.SelectedPerson = DragPerson; } private void ComboBox_DragOver(object sender, DragEventArgs e) { e.AcceptedOperation = DataPackageOperation.Copy; } private void TextBlock_DragStarting(UIElement sender, DragStartingEventArgs args) { DragPerson = (sender as TextBlock).DataContext as Person; //args.Data.SetData(StandardDataFormats.Text, DragPerson.Name); }
本篇最後我想討論下<ListView.Header>和<ListView.Footer>,通常認為額外做一個<StackPanel>或<Grid>放置在ListView的上下方即可。不過我想說的是,如果遇到Layout變化,通過VisaulState等方式來位移控制元件的情況,一個整體的ListView會比較方便。在ListViewHeaderFooterPage中,假設ListView上方存在MenuBar,下部需要CommandBar。如果這兩個控制元件一直和ListView保持緊密的聯絡,那就可以放到Header和Footer中作為一個整體。
以上就是本篇對WinUI 3中ListView的一些討論。縮放檢視打算後面結合SemanticZoom 再來演示。感謝閱讀到這裡的同學們!
Sample Code:
https://github.com/manupstairs/WinUI3Samples/tree/main/WinUI3Samples/ListViewSample
以下連結,是MS Learn上Windows開發的入門課程,單個課程三十分鐘到60分鐘不等,如需補充基礎知識的同學點這裡:
開始使用 Visual Studio 開發 Windows 10 應用