需求:使用 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
<?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 代码
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 真真良心。