WinUI 3學習筆記(2)—— 用ListView來展示集合

樓上那個蜀黍發表於2021-08-10

在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 應用

開發 Windows 10 應用程式

編寫首個 Windows 10 應用

建立 Windows 10 應用的使用者介面 (UI)

增強 Windows 10 應用的使用者介面

在 Windows 10 應用中實現資料繫結

 

相關文章