需求:使用 CollectionView 呈现数据列表和按钮动作
项目开发中不可避免地会遇到在一个页面中呈现列表的情况,使用 CollectionView 作为容器是很方便的。CollectionView 中显示的数据对应于后台的一个 IEnumerable 派生的列表,常用的是 List<T> 和 Vector<T>,我习惯于使用 List<T> 作为后台的数据表。
CollectionView 的每一项对应后台的 List<T> 的一条记录。在网关应用中,有一个页面要列出所有的场景,单击(不论是鼠标还是手指单点一下)执行这个场景,单击条目右侧的“配置...”按钮对这个场景进行配置。
CollectionView 的 SelectionMode=“Single”,SelectionChanged 事件响应对这个条目的单击。在这个页面中,CollectionView 的每一条用一个 Grid 包装,包括了一个引导图标,一个主条目 Label 显示这个场景的名称,一个付条目 Labe 显示这个场景的类型,右侧的装填了一个“配置”按钮。 两个 Label 的 Text 可以在 XAML 中用显示绑定的方式显示对应的属性,但问题来了,“配置”按钮应该绑定什么呢?也就是说,对这个条目中包含的无绑定控件,怎么判断是哪一个条目的“配置”按钮被点击了呢?
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="I2oT.Views.ScenesPage" Title="场景"> <ContentPage.Resources> <Style TargetType="Button"> <Setter Property="VisualStateManager.VisualStateGroups"> <VisualStateGroupList> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal" > <VisualState.Setters> <Setter Property="Scale" Value="1" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Pressed"> <VisualState.Setters> <Setter Property="Scale" Value="0.9" /> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateGroupList> </Setter> <Setter Property="TextColor" Value="{StaticResource AppForegroundColor}"/> <Setter Property="BackgroundColor" Value="{StaticResource AppBackgroundColor}"/> <Setter Property="FontSize" Value="Caption"/> <Setter Property="HeightRequest" Value="32"/> <Setter Property="MinimumHeightRequest" Value="10"/> <Setter Property="CornerRadius" Value="2"/> <Setter Property="Padding" Value="4"/> <Setter Property="HorizontalOptions" Value="Start"/> </Style> <Style x:Key="ItemButtonStyle" TargetType="Button"> <Setter Property="VisualStateManager.VisualStateGroups"> <VisualStateGroupList> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal" > <VisualState.Setters> <Setter Property="Scale" Value="1" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Pressed"> <VisualState.Setters> <Setter Property="Scale" Value="0.8" /> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateGroupList> </Setter> <Setter Property="TextColor" Value="{StaticResource AppTextCommonColor}"/> <Setter Property="BackgroundColor" Value="Transparent"/> <Setter Property="FontSize" Value="Caption"/> <Setter Property="HeightRequest" Value="32"/> <Setter Property="MinimumHeightRequest" Value="10"/> <Setter Property="BorderColor" Value="{StaticResource AppTextCommonColor}"/> <Setter Property="BorderWidth" Value="0.5"/> <Setter Property="CornerRadius" Value="2"/> <Setter Property="Padding" Value="4"/> <Setter Property="VerticalOptions" Value="Center"/> <Setter Property="HorizontalOptions" Value="Start"/> <Setter Property="Margin" Value="4,0"/> <Setter Property="CharacterSpacing" Value="1"/> </Style> </ContentPage.Resources> <ContentPage.ToolbarItems> <ToolbarItem Text="刷新" Clicked="RefreshSubsetList"/> <ToolbarItem Text="添加" Clicked="OnAddSceneClicked"/> </ContentPage.ToolbarItems> <CollectionView x:Name="collectionView" Margin="{StaticResource PageMargin}" SelectionMode="Single" SelectionChanged="OnSelectionChanged"> <CollectionView.Header> <ScrollView Orientation="Horizontal"> <StackLayout Orientation="Horizontal" > <Button x:Name="btnInstantScene" Text="即时场景" Clicked="DisplayInstantScenes"/> <Button x:Name="btnTimingScene" Text="定时场景" Clicked="DisplayTimingScenes"/> <Button x:Name="btnSensorScene" Text="自动化场景" Clicked="DisplaySensorScenes"/> </StackLayout> </ScrollView> </CollectionView.Header> <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="8" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <StackLayout> <Grid ColumnDefinitions="0.15*,*,0.4*"> <Image Grid.RowSpan="2" Source="scene.png" Aspect="AspectFit" VerticalOptions="Start" HeightRequest="20" BackgroundColor="Transparent"/> <Label Grid.Row ="0" Grid.Column="1" Text="{Binding Name}" FontSize="Small" TextColor="{Binding ViewColor}" BackgroundColor="Transparent"/> <Label Grid.Row ="1" Grid.Column="1" Text="{Binding Descriptive}" TextColor="{StaticResource DescriptiveTextColor}" FontSize="Caption" BackgroundColor="Transparent"/> <Button Grid.RowSpan="2" Grid.Row="0" Grid.Column="2" Text="配置..." Style="{StaticResource ItemButtonStyle}" Clicked="OnDefineScene"/> </Grid> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> <CollectionView.Footer> <Label x:Name="lbMessage" Text="Status" FontSize="Caption" TextColor="{StaticResource AppTipIconColor}" VerticalOptions="EndAndExpand" HorizontalOptions="FillAndExpand" HorizontalTextAlignment="Center"/> </CollectionView.Footer> </CollectionView> </ContentPage>
Xamarin.Forms 的 CollectionView 中的子控件的 BindingContext
一开始我也对这个“绑定”感到手足无措,后来突然想到了一个办法:使用 Debug 模式,断点运行到 OnDefineScene 函数中,用 Shift+F9 查看一下是否有可用的线索。果然找到了!原来,在 CollectionView 条目中定义的子控件,不论是否显示地使用 {Binding xxxProperty} 进行绑定,这些子控件的 BindingContext 竟然就是被绑定列表的对应记录!
cs 代码
using I2oT.Data; using I2oT.Models; using I2oT.Views.Scenes; using I2oT.Views.Subsets; using I2oT.Views.SystemSettings; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace I2oT.Views { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class ScenesPage : ContentPage { private List<SceneModel> sceneList = null; private List<SceneModel> instantSceneList = null; private List<SceneModel> timingSceneList = null; private List<SceneModel> sensorSceneList = null; public ScenesPage() { InitializeComponent(); } protected override void OnAppearing() { base.OnAppearing(); RefreshSceneList(this, new EventArgs()); lbMessage.Text = ""; } private void RefreshSceneList(object sender, EventArgs e) { collectionView.ItemsSource = null; sceneList = (new SceneModel()).GetAll(); collectionView.ItemsSource = sceneList; instantSceneList = new List<SceneModel>(); timingSceneList = new List<SceneModel>(); sensorSceneList = new List<SceneModel>(); foreach (var sx in sceneList) { if (sx.Type == 1) { instantSceneList.Add(sx); } else if (sx.Type == 2) { timingSceneList.Add(sx); } else if (sx.Type == 3) { sensorSceneList.Add(sx); } } btnInstantScene.Text = "即时场景 " + instantSceneList.Count().ToString(); btnTimingScene.Text = "定时场景 " + timingSceneList.Count().ToString(); btnSensorScene.Text = "自动化场景 " + sensorSceneList.Count().ToString(); } private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender == null || e == null) return; SceneModel scene = (SceneModel)e.CurrentSelection.FirstOrDefault(); if (scene == null) return; if (scene.Type != 1) return; // Only instant scene can be performed directly. if (scene.Type == 1) { App.Gateway.PerformScene(scene.ID); RefreshSubsetList(null, new EventArgs()); } } private void OnDefineScene(object sender, EventArgs e) { var sx = (SceneModel)(((Button)sender).BindingContext); switch (sx.Type) { case 1: case 3: Shell.Current.GoToAsync($"{nameof(InstantSceneDefinePage)}?{nameof(InstantSceneDefinePage.SceneID)}={sx.ID}"); break; case 2: string uri = ""; uri += $"{nameof(TimingSceneDefinePage)}?"; uri += $"{nameof(TimingSceneDefinePage.SceneID)}={sx.ID}&"; uri += $"{nameof(TimingSceneDefinePage.SceneName)}={sx.Name}"; Shell.Current.GoToAsync(uri); break; default: break; } } private void OnAddSceneClicked(object sender, EventArgs e) { Shell.Current.GoToAsync($"{nameof(AddNewScenePage)}"); } private void DisplayInstantScenes(object sender, EventArgs e) { collectionView.ItemsSource = instantSceneList; } private void DisplayTimingScenes(object sender, EventArgs e) { collectionView.ItemsSource = timingSceneList; } private void DisplaySensorScenes(object sender, EventArgs e) { collectionView.ItemsSource = sensorSceneList; } } }
上述代码中,在 OnAppearing 方法中调用 RefreshSceneList 方法获取已定义的场景列表,列表中的每一个元素是一个 SceneModel (场景的数据模型),默认将全部场景列出,通过 ItemsSource 属性将 sceneList 绑定到 CollectionView。
断点观察
在 OnDefineScene 事件的第一条语句上设置断点,运行到此处暂停,然后 Shift+F9 打开快速监视,输入sender,(Button)sender,再输入((Button)sender).BindingContext,得到的计算值如下图所示。也就是说,这个配置按钮的 BindingContext 是 CollectionView 绑定的列表的当前元素!
哦吼,这下好办啦!直接将这个 SceneModel 的 ID 传递给下级页面就可以啦~
总结
一旦 CollectionView 的 ItemsSource 被赋值为一个类的列表,那么这个 CollectionView 的每一个条目中的任何控件的默认 BindingContext 就是这个列表的当前元素。
Xamarin.Forms 的 CollectionView 真真良心。