Ask Learn
Preview
Please sign in to use this experience.
Sign inThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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即可。。
我们的ListView现在所有的项目内容都是默认左对齐的,那么如果想要像博客园UAP那样,把一部分内容放在右边怎么办呢?
在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的项目(也就是ListViewItem)的宽度和内容是一样的,所以看不来效果。我们把项目的宽度设置成和ListView一样的就可以了。
在Page.Resources内部修改ListViewItem的样式:
<Page.Resources>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</Page.Resources>
这次就对了。
ListView默认是单选的,需要修改SelectionMode属性来启用多选的支持,这样每个项目左侧都会出现一个复选框。
现在ListView就变成下面这个样子了:
如果你用过我们的应用的话,你在某个作者的文章列表页面会发现:随着你用手向上滑动,页面的标题会变成作者的头像和昵称,方便用户识别当前在看谁的博客列表,那么这个功能是怎么实现的呢?
滚动前:
滚动后:
其实这个功能是通过判断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实现这个功能,需要自定义首先定义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的人可能没有注意到,其实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显示分组数据就可以了,动画效果是自带的。
先定义一个简单的分组类:
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="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:ListView_Group_Sample" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://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分组显示之后,就不能通过ISupportIncrementalLoading来实现增量加载了(增量加载组?)。
前面提到了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:
https://www.windowsphone.com/zh-cn/store/app/博客园-uap/500f08f0-5be8-4723-aff9-a397beee52fc
Windows Store App link:
https://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
Please sign in to use this experience.
Sign in