• 博客园客户端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

  • 相关阅读:
    初识RabbitMQ
    ThreadPoolExecutor中execute和submit的区别
    MYSQL bin_log 开启及数据恢复
    MYSQL 悲观锁和乐观锁简单介绍及实现
    linux php多版本
    easyui汉化啊!
    虚化技术的额外开销
    拍脑袋空想不可能有创新
    大规模WEB服务技术
    xunsearch bsd 10.1安装心酸路。。。
  • 原文地址:https://www.cnblogs.com/ms-uap/p/4218535.html
Copyright © 2020-2023  润新知