PLC-IoT 网关开发札记(6): Xamarin.Forms 的 CollectionView 绑定了什么?

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

相关推荐
sun0077007 小时前
android ndk编译valgrind
android
AI视觉网奇8 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空8 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet9 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin9 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo0305198710 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
御控工业物联网11 小时前
城市二次供水物联网监测管控管理平台御控解决方案:构建全链路智能水务新生态
物联网·数据采集·远程监控·物联网网关·二次供水·智能水务·泵站
电子科技圈11 小时前
芯科科技FG23L无线SoC现已全面供货,为Sub-GHz物联网应用提供最佳性价比
科技·嵌入式硬件·mcu·物联网·制造·智能硬件·交通物流
禁默12 小时前
第六届大数据、人工智能与物联网工程国际会议(ICBAIE 2025)
大数据·人工智能·物联网
字符搬运工-蓝天12 小时前
Win7环境中离线安装Visual Studio 2017的相关问题
ide·windows·visual studio