博客園客戶端UAP開發隨筆-從9個細節說ListView的使用


前言

ListView應該算是在WP開發中最常用的一個顯示控件了,在我們的項目中,也大量的使用了ListView。很多WP上的開發者肯定也是如此。但是ListView有很多你可能沒用到的功能。這篇博客主要是結合項目中遇到的問題,從9個細節之處來介紹下ListView的全面使用。

基本

首先定義好我們准備使用的實體類,之后的代碼中將一直用到這些。

下面是MVVM中常用到的簡單基類,用於讓UI響應model的變化(雖然這個例子沒用到,但是如果有興趣的話,可以自己動手看看效果)

public abstract class Base : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // 使用CallerMemberNameAttribute可以獲得調用這個方法的成員名稱,對於屬性的set來說,就是屬性名
        public void NotifyChange([CallerMemberName]string property = null)
        {
            if (this.PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
    }

用來在ListView中顯示的Item,我們假設Time屬性在應用中是不會改變的。

public class Item : Base
    {
        private string _title = string.Empty;

        public string Title
        {
            get { return _title; }
            set
            {
                _title = value;
                NotifyChange();
            }
        }

        private string _content = string.Empty;

        public string Content
        {
            get { return _content; }
            set
            {
                _content = value;
                NotifyChange();
            }
        }

        public string Time
        {
            get;
            set;
        }
    }

再來個Helper用來生成測試數據。

public static class DataHelper
    {
        public static ObservableCollection<Item> CreateItems()
        {
            var collection = new ObservableCollection<Item>();

            for (var i = 0; i < 10; i++)
            {
                collection.Add(new Item
                {
                    Title = "Title " + i.ToString(),
                    Content = "Content " + i.ToString(),
                    Time = DateTime.Now.ToString()
                });
            }

            return collection;
        }
    }

這里為了簡單,直接把數據賦值給了頁面的上下文(DataContext),這樣在XAML中直接使用{Binding}即可綁定到當前的上下文。

public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            this.NavigationCacheMode = NavigationCacheMode.Required;

            this.DataContext = DataHelper.CreateItems();
        }
    }
<ListView x:Name="items_listview" 
                  ItemsSource="{Binding}">
            <ListView.ItemTemplate>
                <!--一個簡單的ListView項目模板,綁定到Item.Title/Content-->
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Path=Title}"></TextBlock>
                        <TextBlock Text="{Binding Path=Content}"></TextBlock>
                        <TextBlock Text="{Binding Path=Time}"></TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

這樣一個簡單ListView完成了,但是可以看到,默認的項目樣式有點丑,這個可以根據需要美化下ItemTemplate即可。。

list_base

對齊

我們的ListView現在所有的項目內容都是默認左對齊的,那么如果想要像博客園UAP那樣,把一部分內容放在右邊怎么辦呢?

sample_right 

在XAML中,控件有兩種對齊方式,HorizontalAlignment(水平對齊)和VerticalAlignment(垂直對齊),顯然我們現在需要的是水平對齊。然后利用Grid,將之分成2行,第2行用來顯示時間,並且設置TextBlock的HorizontalAlignment為Right就可以了,這樣我們的項目模板就變成了:

<DataTemplate>
                    <!--每個項目都用顯示邊框,更好的區分開-->
                    <Border BorderBrush="Blue" BorderThickness="1" Margin="0, 10, 0, 0">
                        <!--這里簡單的把每個項目的寬度都拉長,以便效果明顯-->
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"></RowDefinition>
                                <RowDefinition Height="20"></RowDefinition>
                            </Grid.RowDefinitions>
                            <StackPanel>
                                <TextBlock Text="{Binding Path=Title}"></TextBlock>
                                <TextBlock Text="{Binding Path=Content}"></TextBlock>
                            </StackPanel>
                            <TextBlock Grid.Row="1" HorizontalAlignment="Right" Text="{Binding Path=Time}"></TextBlock>
                        </Grid>
                    </Border>
                </DataTemplate>

但是運行的效果和想象的不一樣啊,時間也沒有靠右側顯示啊(我給每個項目都加了邊框,看起來明顯些)。

listview_horizontal_align_without style

這是因為ListView的項目(也就是ListViewItem)的寬度和內容是一樣的,所以看不來效果。我們把項目的寬度設置成和ListView一樣的就可以了。

在Page.Resources內部修改ListViewItem的樣式:

<Page.Resources>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </Page.Resources>

這次就對了。

listview_right

多選

ListView默認是單選的,需要修改SelectionMode屬性來啟用多選的支持,這樣每個項目左側都會出現一個復選框。

listview_multiple_select

現在ListView就變成下面這個樣子了:

listview_multiple_selected

ScrollViewer的獲得及應用

如果你用過我們的應用的話,你在某個作者的文章列表頁面會發現:隨着你用手向上滑動,頁面的標題會變成作者的頭像和昵稱,方便用戶識別當前在看誰的博客列表,那么這個功能是怎么實現的呢?

滾動前:

bloger_init

滾動后:

bloger_after

其實這個功能是通過判斷ListView的ScrollViewer的滾動方向和距離來實現的。

首先我們需要找到ListView上的ScrollViewer控件,這個控件不是顯式的在XAML中定義的,我們需要在VirtualTree上來查找。

下面這是個通用的查找方法。

public static ScrollViewer GetScrollViewer(Windows.UI.Xaml.DependencyObject depObj)
        {
            if (depObj is ScrollViewer)
            {
                return depObj as ScrollViewer;
            }

            for (int i = 0; i < Windows.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                var child = Windows.UI.Xaml.Media.VisualTreeHelper.GetChild(depObj, i);
                var result = GetScrollViewer(child);
                if (result != null) return result;
            }
            return null;
        }

然后我們在ListView加載完成之后(這里一定要是加載完成之后,否則你得到可能是個null),查找這個ScrollViewer,然后添加ViewChanged事件(當滑動時觸發),並在事件內對滑動的距離和方向進行判斷

private void ListView_Loaded(object sender, RoutedEventArgs e)
        {
           this.scrollViewer = GetScrollViewer(this.ListView);
           this.scrollViewer.ViewChanged += scrollViewer_ViewChanged;
        }

        void scrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            //VerticalOffset大於0表示向上滑動
            if (this.scrollViewer.VerticalOffset > 50)
            {
                if (!isAuthorShowOnTitle)
                {
                //執行動畫
                    this.sb_AuthorMoveUp.Begin();
                }
            }
            else
            {
                if (isAuthorShowOnTitle)
                {
               // 執行動畫
                    this.sb_AuthorMoveDown.Begin();
                }
            }
        }

顯示多列

現在我們的ListView一直都是只顯示一列,那么怎么樣實現下圖的效果呢?可以不用GridView么?

listview_panel

使用ListView實現這個功能,需要自定義首先定義ItemsPanel模板(ItemsPanelTemplate),通過WrapGrid來實現的,WrapGrid是按從左到右或從上到下的順序對子元素進行定位,而這個布局的功能實際上就是GridView的,再修改MaximumRowsOrColumns來定義能夠顯示的最多列/行,這樣我們的ListView就實現了最多兩列的效果。

<ListView x:Name="items_listview" 
                  ItemsSource="{Binding}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2"></WrapGrid>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
            <ListView.ItemTemplate>
                <!--一個簡單的ListView項目模板,綁定到Item.Title/Content-->
                <DataTemplate>
                    <!--每個項目都用顯示邊框,更好的區分開-->
                    <Border BorderBrush="Blue" BorderThickness="1" Margin="0, 10, 0, 0">
                        <!--這里簡單的把每個項目的寬度都拉長,以便效果明顯-->
                        <Grid Width="150">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"></RowDefinition>
                                <RowDefinition Height="20"></RowDefinition>
                            </Grid.RowDefinitions>
                            <StackPanel>
                                <TextBlock Text="{Binding Path=Title}"></TextBlock>
                                <TextBlock Text="{Binding Path=Content}"></TextBlock>
                            </StackPanel>
                            <TextBlock VerticalAlignment="Bottom" Grid.Row="1" HorizontalAlignment="Right" Text="{Binding Path=Time}"></TextBlock>
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

效果如下:

listview_columns

Header和Footer

很多使用ListView的人可能沒有注意到,其實ListView還有Header和Footer,同時也都支持自定義模板。

在博客園UAP中,我們使用Header來作為頁面的副標題,Footer則可以用來作為增量加載時的提示,比如”加載中。。“,”沒有更多了。。“。

Header和Footer的使用很簡單,和ItemTemplate一樣,只要定義好對應的模板就可以了。

            <ListView.HeaderTemplate>
                <DataTemplate>
                    <Grid Background="Red">
                        <TextBlock FontSize="25" Text="這是一個副標題"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ListView.HeaderTemplate>
            <ListView.FooterTemplate>
                <DataTemplate>
                    <Grid Background="Green">
                        <TextBlock  FontSize="25"  Text="沒有更多內容啦。。。"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ListView.FooterTemplate>

效果如下圖。

分組

當我第一次使用Windows phone時,覺得應用列表頁面向上滑動時的效果很cool(如下圖,當沒有更多以b開頭的應用之后,手指再向上滑動,c會慢慢把b頂上去),要在Windows phone上實現這一效果,只需要使用ListView顯示分組數據就可以了,動畫效果是自帶的。

app list screen

先定義一個簡單的分組類:

public class Group : Base
    {
        private string _name = string.Empty;

        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                this.NotifyChange();
            }
        }

        public ObservableCollection<Item> Items
        {
            get;
            private set;
        }

        public Group()
        {
            this.Items = new ObservableCollection<Item>();
        }
    }

然后再DataHelper中添加一個生成分組數據的方法(請無視循環中可能存在的性能問題-_-)。

        public static ObservableCollection<Group> CreateGroups()
        {
            var groups = new ObservableCollection<Group>();

            for (var i = 0; i < 13; i++)
            {
                var group = new Group
                {
                    Name = "Group " + i.ToString()
                };

                for (var j = 0; j < 10; j++)
                {
                    var item = new Item
                    {
                        Time = DateTime.Now.ToString(),
                        Title = "Title " + j.ToString(),
                        Content = "Content" + j.ToString()
                    };

                    group.Items.Add(item);
                }

                groups.Add(group);
            }

            return groups;
        }

在頁面上,ListView的ItemsSource和之前的有點不一樣了,我們需要告訴ListView該怎么顯示數據,每個分組中項目列表是哪個屬性。這時候我們需要定義一個數據集視圖(CollectionView),具體請看下面代碼里的注釋。

<Page
    x:Class="ListView_Group_Sample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ListView_Group_Sample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Page.Resources>
        <!--通過XAML創建數據集視圖,並通過視圖定義分組(IsSourceGrouped)
        ,每個分組中項目對應的路徑(ItemsPath,對應的就是我們Group.Items)
        ,Source表示當前視圖的源,{Binding}表示綁定的是當前的上下文(DataContext)-->
        <CollectionViewSource x:Name="cv_items"
                              IsSourceGrouped="True"
                              ItemsPath="Items"
                              Source="{Binding}"></CollectionViewSource>
    </Page.Resources>

    <Grid>
        <!--這里ItemsSource和普通的有點不一樣了,要用到在Page.Resources中定義的視圖來顯示-->
        <ListView x:Name="items_listview"
                  ItemsSource="{Binding Source={StaticResource cv_items}}">
            <ListView.GroupStyle>
                <GroupStyle >
                    <!--分組的頭部顯示的模板,這里我們用背景色來高亮,文字綁定到Group.Name-->
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Grid Background="BlueViolet">
                                <TextBlock Text="{Binding Path=Name}" FontSize="20"></TextBlock>
                            </Grid>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
            <ListView.ItemTemplate>
                <!--一個簡單的ListView項目模板,綁定到Item.Title/Content-->
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Path=Title}"></TextBlock>
                        <TextBlock Text="{Binding Path=Content}"></TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>

這樣一個簡單的分組功能就實現了

開始的時候:

listview_group_beginning

被推上去了:

listview_group_push

但是遺憾的是,ListView分組顯示之后,就不能通過ISupportIncrementalLoading來實現增量加載了(增量加載組?)。

SemanticZoom控件

前面提到了WP的應用列表界面的分組顯示,那么這個頁面的另一個更cool的效果就是點擊任意分組的Header之后,會顯示一個縮小的索引視圖,這個就是SemanticZoom控件的效果。

這個控件實際上是通過控制內部的兩個ListView/GridView來實現這種效果的,一個顯示縮小的索引視圖(ZoomOutView),另一個顯示具體的分組列表(ZoomInView)。前面我們已經實現了分組列表,這樣我們只需要再用GridView實現個縮小的視圖,然后放在SemanticZoom空間內部就完成了。

實現一個只顯示Group.Name的Grid很簡單。這里需要注意的是,我們直接把分組集合綁定到了GridView.ItemsSource,這樣對於每個GridViewItem而言,其上下文就變成了Group,而不是Item,所以我們在TextBlock中綁定的是Group.Name。

        <GridView ItemsSource="{Binding}">
            <GridView.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Blue" BorderThickness="2" Margin="10, 10, 0, 0">
                        <Grid Height="150" Width="150" Background="Black">
                            <TextBlock Text="{Binding Path=Name}"></TextBlock>
                        </Grid>
                    </Border>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>

顯示如下,效果差不多-_-..

 

現在兩個視圖都有了,是時候放在SemanticZoom控件里了。現在把ListView放在ZoomInView用來顯示詳細信息,把GridView放在ZoomOutView顯示縮略信息。

        <SemanticZoom>
            <SemanticZoom.ZoomedInView>
                <!--這里ItemsSource和普通的有點不一樣了,要用到在Page.Resources中定義的視圖來顯示-->
                <ListView x:Name="items_listview"
                  ItemsSource="{Binding Source={StaticResource cv_items}}">
                    <ListView.GroupStyle>
                        <GroupStyle >
                            <!--分組的頭部顯示的模板,這里我們用背景色來高亮,文字綁定到Group.Name-->
                            <GroupStyle.HeaderTemplate>
                                <DataTemplate>
                                    <Grid Background="BlueViolet">
                                        <TextBlock Text="{Binding Path=Name}" FontSize="20"></TextBlock>
                                    </Grid>
                                </DataTemplate>
                            </GroupStyle.HeaderTemplate>
                        </GroupStyle>
                    </ListView.GroupStyle>
                    <ListView.ItemTemplate>
                        <!--一個簡單的ListView項目模板,綁定到Item.Title/Content-->
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Text="{Binding Path=Title}"></TextBlock>
                                <TextBlock Text="{Binding Path=Content}"></TextBlock>
                            </StackPanel>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </SemanticZoom.ZoomedInView>

            <SemanticZoom.ZoomedOutView>
                <GridView ItemsSource="{Binding}">
                    <GridView.ItemTemplate>
                        <DataTemplate>
                            <Border BorderBrush="Blue" BorderThickness="2" Margin="10, 10, 0, 0">
                                <Grid Height="150" Width="150" Background="Black">
                                    <TextBlock Text="{Binding Path=Name}"></TextBlock>
                                </Grid>
                            </Border>
                        </DataTemplate>
                    </GridView.ItemTemplate>
                </GridView>
            </SemanticZoom.ZoomedOutView>
        </SemanticZoom>

這時候你如果運行程序,默認顯示的ListView,當你點擊分組的Header后,GridView會自動彈出來了,這樣一個簡單SemanticZoom就是實現了,切換工作都是系統幫忙實現的。

更新項目

在博客園UAP這個應用中,在博客列表頁面上,如果點擊文章標題的話,會運行自定義動畫把博客的summary隱藏起來,並顯示“朕無視”來表示忽略此文章。雖然我們可以使用綁定狀態來隱藏/顯示控件,但是這樣卻不能執行自定義動畫,所以我們是在ListView每個項目的DataContextChanged事件和OnApplyTemplate事件中進行狀態判斷的,其中每個項目都是一個自定義控件,在控件中判斷當前綁定數據的狀態來執行對應的邏輯。

下面這個PostControl自定義控件在兩個事件中通過GetTemplateChild得到子控件,然后對子控件進行對應的設置,如動畫,是否顯示等。

    public sealed class PostControl : Control
    {
        public PostControl()
        {
            this.DefaultStyleKey = typeof(PostControl);
            this.DataContextChanged += PostControl_DataContextChanged;
        }

        void PostControl_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
        {
            this.UpdateUI(false);
        }

        protected override void OnApplyTemplate()
        {
            this.UpdateUI(false);
        }
private void UpdateUI(bool showAnimation = true)
        {
        //更新邏輯
        var tbSummary = this.GetTemplateChild("tb_Summary") as TextBlock;
      }
  }

這里需要注意,一定在這兩個事件中都要進行更新,因為有的時候,其中某個事件還得不到子控件,這個暫時還不知道原因,可能和調用的順序有關吧。

 

 

分享代碼,改變世界!

Windows Phone Store App link:

http://www.windowsphone.com/zh-cn/store/app/博客園-uap/500f08f0-5be8-4723-aff9-a397beee52fc

Windows Store App link:

http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059

GitHub open source link:

https://github.com/MS-UAP/cnblogs-UAP

MSDN Sample Code:

https://code.msdn.microsoft.com/CNBlogs-Client-Universal-477943ab


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM