PLC-IoT 网关开发札记(4):Xamarin Forms 实现自定义控件(一个开关)

news2025/2/25 18:20:55

1. 需求

物联网项目中要集成大量的设备,作为一种简单的数字孪生手段,每一型号的设备都需要一个对应的虚拟实现,也就是用界面把这个设备呈现出来。设备有多个可管理的“属性”,对这个设备的监测对应获取这个设备“属性”的值,对这个设备的控制对应改变某些“属性”的值。

非常多的设备都具备“开关(Switch)”这个属性,开关可以是电源开关(Power Switch),双态开关(Enable/Disable)等,对这个开关清零(“0”),一般表示关闭这个开关,设备的某些功能处于禁用状态;对这个开关置1,一般表示打开这个开关,设备的对应功能处于启用状态。

对于单一的,简单的 Form,只需要几个 Label + Switch 控件就可以实现“开关”的功能了。但是这种直白的控件组合无法满足哪怕是稍微复杂一些的设备的虚拟化要求。这些设备除了开关属性以外,往往还具备其它属性,把这些属性都以”散装“的原生控件堆积在一起,XAML 文件和 XAML.cs 都会非常巨大。每一个原生控件在同一个页面中都是”本地全局“变量,不论从命名规则还是从赋值上都存在错误风险。不言而喻,这种出错风险随着控件的数量是呈指数增长的。因此,绝对有必要将一些特定的属性进行封装,只要对其进行完整的严格的测试就可以放心使用了。

这种封装是必须的,必要的,是面向对象编程的基本要求。

现在我们就利用 Xamarin.Forms 来构建一个 ”开关“ 类型的控件。

2. 实现

我已经构建好了I2oT应用项目,编码在 I2oT,发布在 I2oT.Android 和 I2oT.IOS,看Android部分。下图是按照步骤 2.1/2.2 执行完成后的自定义控件文件结构,图中已经定义了7个自定义控件,这里以 BoolProperty 为例说明怎么来构建一个“开关”控件。

2.1 为自定义控件创建一个命名空间

在 I2oT 项目中,新建文件夹 UserControls。按照 Visual Studio 的命名规则,新建文件夹后,在这个文件夹下所生成的类都会归类到 I2oT.UserControls 命名空间里。

2.2 为自定义控件创建视图和代码文件

右键点击 “UserControls” 文件夹,选择“添加➡新建项➡Xamarin.Forms➡内容视图(ContentView)➡输入BoolProperty”,在 UserControls 文件夹创建一个 BoolProperty 视图。之所以以 BoolProperty 命名,是因为开关量就其数据本质而言就是布尔类型的。

2.3 构造视图

双击 BoolProperty.xaml 文件,将模板原文替换成以下内容。

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Name="this"
             x:Class="I2oT.UserControls.BoolProperty">
  <ContentView.Content>
      <StackLayout Margin="0"
                   Orientation="Horizontal"
                   VerticalOptions="StartAndExpand"
                   HorizontalOptions="FillAndExpand">
          <Label x:Name="lvCaption" Text="Switch" Style="{StaticResource PropertyCaptionStyle}"/>
          
          <Switch x:Name="vControl" ThumbColor="Green"
                  HorizontalOptions="StartAndExpand"
                  Toggled="OnSwitchToggled"/>
          
          <Label x:Name="lvComment" Text="ON/OFF" Style="{StaticResource PropertyCommentStyle}"/>
        </StackLayout>
  </ContentView.Content>
</ContentView>

在这个描述文件中,定义了三个控件:

  • 一个 Label 控件,取名为lvCaption,用以显示这个属性的名称,例如,应用中可能会有“电源开关”,“是否启用”,“通断”等名称。
  • 一个 Switch 控件,取名为vControl,用以呈现这个布尔对象的状态,IsToggle 属性可以表示 True/False,对应这个开关的开/关,通/断状态。
  • 一个 Label 控件, 取名为lvComment,用以说明这个状态的当前值,以避免使用者的困扰。

这三个控件以水平方向封装到一个 StackLayout 中,在被调用时,StackLayout 作为一个整体显示,而内部的三个控件的相对位置不会发生紊乱。

下一步讨论前,展开 BoolProperty.xaml,双击 BoolProperty.xaml.cs 文件,将原有的模板代码替换为如下代码。

using I2oT.Views.Scenes;
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.UserControls
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class BoolProperty : ContentView
    {
        private bool initLoad = false;

        public InstantSceneSubsetPage HostPage { get; set; }

        public int SIID { get; set; } = 0x0001;
        public int CIID { get; set; } = 0x0001;
        public string Property { get; set; } = "Switch";
        public int Type { get => 0x0002; }
        public int Len { get; set; } = 0x0001;
        public int Min { get => 0x0000; }
        public int Max { get => 0x0001; }
        public int Step { get => 0x0001; }
        public bool DefaultValue { get; set; } = false;
        public bool Value
        {
            get => vControl.IsToggled;
            set => vControl.IsToggled = value;
        }

        public string Caption
        {
            set => lvCaption.Text = value.Trim();
        }

        public BoolProperty()
        {
            InitializeComponent();

            initLoad = true;
            Value = DefaultValue;
            lvComment.Text = Value ? "ON" : "OFF";
        }

        public BoolProperty(InstantSceneSubsetPage hostPage, bool onOff)
        {
            InitializeComponent();

            initLoad = true;
            HostPage = hostPage;
            Value = onOff;
            lvComment.Text = Value ? "ON" : "OFF";
            initLoad = false;
        }

        public void TurnOn()
        {
            vControl.IsToggled = true;
        }

        public void TurnOff()
        {
            vControl.IsToggled = false;
        }

        private void OnSwitchToggled(object sender, ToggledEventArgs e)
        {
            if (initLoad) return;

            Value = e.Value;
            lvComment.Text = Value ? "ON" : "OFF";

            HostPage.ContentChanged = true;
            UpdateHostProperties();
        }

        private void UpdateHostProperties()
        {
            if (Property == null) return;

            bool propertyMatched = false;
            for (var i = 0; i < HostPage.Properties.Count; i++)
            {
                if (HostPage.Properties[i].Contains(Property))
                {
                    HostPage.Properties[i] = "\"" + Property + "\":" + (Value ? "1" : "0");
                    propertyMatched = true;
                    break;
                }
            }

            if (!propertyMatched)
            {
                HostPage.Properties.Add("\"" + Property + "\":" + (Value ? "1" : "0"));
            }
        }
    }
}

2.4 构造数据接口

2.4.1 基础属性

根据我的项目中对 BoolProperty 的要求,BoolProperty 具有 SIID/ CIID/ Property/ Type/ Len/ Min/ Max/ Step/ DefaultValue 共9个基础属性,这些属性都和物模型定义的基础模板有关,和如何构建自定义控件没有直接关系。对物模型不感兴趣的童鞋可以忽略这些定义。

2.4.2 可视化属性的 get/set

注意对 Caption 属性和 Value 属性的定义,代码中是这样子的。

Caption 属性对于 Bool Property 而言相当于一个只写属性,对 Caption 赋值相当于对 lvCaption 这个控件的 Text 进行了更新;访问 Caption 属性的值是不允许的(因为没有 get 选项),转而使用 lvCaption.Text 替代。

Value 属性是可读写的,如果从一个类的属性的角度看,Value 属性的内部影子变量是 vControl.IsToggled。

public string Caption
{
    set => lvCaption.Text = value.Trim();
}

public bool Value
{
    get => vControl.IsToggled;
    set => vControl.IsToggled = value;
}

2.4.3 构造函数

定义了两个构造函数。默认构造函数简单地初始化了 vControl 和 lvComment 的值,也就是说,如果没有其它值的定义时,呈现一个无名称、关闭状态、备注为“OFF”的控件。

带参数的构造函数将其宿主页面和开关状态作为参数带进来,这时控件呈现出一个带名称的、指定状态的,相应备注的控件。

2.5 定义开关动作对数据的更新

对数据的更新只会来源于用户对 vControl(Switch 控件)的点击,使用事件处理函数 OnSwitchToggled 更新界面和 HostPage 的相关属性。

状态变化时,对 HostPage.ContentChanged 的赋真值是常用的数据更新做法,当 BoolProperty 这个子控件的值被修改时,也意味着其调用者页面(Host Page)的数据也发生了变化,当 HostPage 退出时可以作为提示保存的依据,同时调用者页面无需管理自身的数据更新标志。

后来的实测证明 Value= e.Value 这条语句没有必要,这是因为 vControl.IsToggled 就是 Value 属性的影子变量。

        private void OnSwitchToggled(object sender, ToggledEventArgs e)
        {
            if (initLoad) return;

            Value = e.Value;
            lvComment.Text = Value ? "ON" : "OFF";

            HostPage.ContentChanged = true;
            UpdateHostProperties();
        }

2.6 initLoad 变量的使用

initLoad 是一个局部变量,当控件在生成阶段为 True,生成结束为 False。用 initLoad 控制在生成期间不发生数据更新回调是非常重要的。这是因为在控件生成期间——也就是带参数的构造函数被执行时,要根据 HostPage 传来的 onOff 参数对 vControl 进行赋值,这个赋值会出发 OnSwitchToggled 函数,OnSwitchToggled 反而又去更新 HostPage 的值。这不是我们希望的动作。使用 initLaod 就可以解决这个问题,当处于生成阶段时, initLoad 为 True,事件处理函数中的

            if (initLoad) return;

这句话就阻挡了在生成期间对 HostPage 的参数更新。

不加控制有可能会导致事实上的死循环的。

2.7 实现截图

这是双路可调 LED 智能灯具的实现,它具有开关(Switch)、亮度(Brightness)和色温(CCT)三个属性,其中“开关”属性采用了上述的 BoolProperty 控件,只要整体色调平稳,看起来也是蛮好看的(献丑了哦 ;)。

3. 小结

使用 Xamarin.Forms 实现自定义控件,远非像 N 多码神所说那么复杂。只要考虑到以下三个环节就可以轻松实现自定义控件。

  1. 一个完整的布局,布局中包含了必要的控件及其呈现方式
  2. 定义好暴露(public)的属性和内部控件的对应关系
  3. 修改内部控件状态/属性值时,更新所绑定的实体数据
  4. 使用 initLoad 避免控件和宿主页面之间的循环更新

把握好这4条,用原生的简单控件也能制作出优雅简洁的自定义控件,同时还不必定义一大堆 BindableProperty 及其回调函数。

欢迎评论指正,一同提升。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1383147.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux系统编程(十一):高级 IO

参考引用 UNIX 环境高级编程 (第3版)嵌入式Linux C应用编程-正点原子 Linux系统编程&#xff08;文章链接汇总&#xff09; 1. 非阻塞 I/O 阻塞就是进入了休眠状态&#xff0c;交出了 CPU 控制权阻塞 I/O 就是对文件的 I/O 操作&#xff08;读写操作&#xff09;是阻塞式的&a…

FGSM方法生成交通信号牌的对抗图像样本

背景&#xff1a; 生成对抗样本&#xff0c;即扰动图像&#xff0c;让原本是“停车”的信号牌识别为“禁止驶入” 实验准备 模型&#xff1a;找一个训练好的&#xff0c;识别交通信号牌的CNN模型&#xff0c;灰度图像 模型地址&#xff1a;GitHub - Daulettulegenov/TSR_CNN:…

高级RAG(六): 句子-窗口检索

之前我们介绍了LlamaIndex的从小到大的检索 的检索方法&#xff0c;今天我们再来介绍llamaindex的另外一种高级检索方法: 句子-窗口检索(Sentence Window Retrieval)&#xff0c;在开始介绍之前让我们先回顾一下基本的RAG检索的流程&#xff0c;如下图所示&#xff1a; 在执行基…

学会编写自定义configure脚本,轻松实现定制化配置

学会编写自定义configure脚本&#xff0c;轻松实现定制化配置 一、configure脚本的作用和重要性二、configure脚本的基本结构和语法三、编写自定义configure脚本的步骤四、示例五、常见的问题总结 一、configure脚本的作用和重要性 configure脚本是用于自动配置软件源代码的脚…

jmeter如何做接口测试?

Jmeter介绍&测试准备&#xff1a; Jmeter介绍&#xff1a;Jmeter是软件行业里面比较常用的接口、性能测试工具&#xff0c;下面介绍下如何用Jmeter做接口测试以及如何用它连接MySQL数据库。 前期准备&#xff1a;测试前&#xff0c;需要安装好Jmeter以及jdk并配置好jdk环…

高级JavaScript。同步和异步,阻塞和非阻塞

同步阻塞 同步非阻塞 异步阻塞 异步非阻塞 在当什么是同步和异步&#xff0c;阻塞与非阻塞的概念还没弄清楚之前&#xff0c;更别提上面这些组合术语了&#xff0c;只会让你更加困惑。 同步和异步 同步和异步其实指的是&#xff0c;请求发起方对消息结果的获取是主动发起…

强化学习应用(五):基于Q-learning算法的无人车配送路径规划(通过Python代码)

一、Q-learning算法介绍 Q-learning是一种强化学习算法&#xff0c;用于解决基于环境的决策问题。它通过学习一个Q-table来指导智能体在不同状态下采取最优动作。下面是Q-learning算法的基本步骤&#xff1a; 1. 定义环境&#xff1a;确定问题的状态和动作空间&#xff0c;并…

NI PXIe-6386国产替代,8路AI(16位,14 MS/s/ch),2路A​O,24路DIO,PXI多功能I/O模块

PXIe-6386 PXIe&#xff0c;8路AI&#xff08;16位&#xff0c;14 MS/s/ch&#xff09;&#xff0c;2路A​O&#xff0c;24路DIO&#xff0c;PXI多功能I/O模块 PXIe-6386是一款同步采样的多功能DAQ设备。该模块提供了模拟 I/O、数字I/O、四个32位计数器和模拟和数字触发。板载N…

2024年【G1工业锅炉司炉】考试及G1工业锅炉司炉考试资料

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 G1工业锅炉司炉考试根据新G1工业锅炉司炉考试大纲要求&#xff0c;安全生产模拟考试一点通将G1工业锅炉司炉模拟考试试题进行汇编&#xff0c;组成一套G1工业锅炉司炉全真模拟考试试题&#xff0c;学员可通过G1工业锅…

【现代密码学】笔记3.1-3.3 --规约证明、伪随机性《introduction to modern cryphtography》

【现代密码学】笔记3.1-3.3 --规约证明、伪随机性《introduction to modern cryphtography》 写在最前面私钥加密与伪随机性 第一部分密码学的计算方法论计算安全加密的定义&#xff1a;对称加密算法 伪随机性伪随机生成器&#xff08;PRG&#xff09; 规约法规约证明 构造安全…

LeetCode刷题.15(哈希表与计数排序解决41. 缺失的第一个正数)

给你一个未排序的整数数组 nums &#xff0c;请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,0] 输出&#xff1a;3 示例 2&#xff1a; 输入&#xff1a;nums …

MCS-51---串行通信的特点

目录 一.同步通信和异步通信 1.异步通信 2.同步通信 二.串行通信的方式 1.单工 2.半双工 3.全双工 三.串行通信的速率 四.MCS-51单片机结构 五.串行口的控制 1.串行口控制寄存器(SCON) 2.电源控制寄存器(PCON) 六.波特率的设计 七.串行口的工作方式 1.方式0 2.…

NLP论文阅读记录 - WOS | ROUGE-SEM:使用ROUGE结合语义更好地评估摘要

文章目录 前言0、论文摘要一、Introduction1.1目标问题1.2相关的尝试1.3本文贡献 二.相关工作三.本文方法四 实验效果4.1数据集4.2 对比模型4.3实施细节4.4评估指标4.5 实验结果4.6 细粒度分析 五 总结 前言 ROUGE-SEM: Better evaluation of summarization using ROUGE combin…

操作系统详解(5.1)——信号(Signal)的相关题目

系列文章&#xff1a; 操作系统详解(1)——操作系统的作用 操作系统详解(2)——异常处理(Exception) 操作系统详解(3)——进程、并发和并行 操作系统详解(4)——进程控制(fork, waitpid, sleep, execve) 操作系统详解(5)——信号(Signal) 文章目录 题目第一问第二问第三问 题目…

python24.1.14while循环

当条件结束时间未知时&#xff0c;while循环比for循环更合适 实践

Debian(Linux)局域网共享文件-NFS

NFS (Network File system) 是一种客户端-服务器文件系统协议&#xff0c;允许多个系统或用户访问相同的共享文件夹或文件。最新版本是 NFS-V4&#xff0c;共享文件就像存储在本地一样。它提供了中央管理&#xff0c;可以使用防火墙和 Kerberos 身份验证进行保护。 本文将指导…

docker-compose部署kafka、SASL模式(密码校验模式)

一.基础kafka部署 zookeeper&#xff0c;kafka&#xff0c;kafka-ui docker-compose.yml 注意点&#xff1a;192.168.1.20 是宿主机的ip version: "3" services:zookeeper:image: wurstmeister/zookeepercontainer_name: zookeeperrestart: alwaysports:- 2181:2…

未来的失业将是常态吗?

2024年&#xff0c;科技巨头谷歌、亚马逊都在本周宣布大规模裁员&#xff0c;影响到众多部门。此外&#xff0c;社交平台 Discord 表示将裁员 17%&#xff0c;游戏服务商 Unity Software 宣布将裁员 25%&#xff0c;语言学习应用程序 Duolingo 则称解雇了 10% 的正式职工&#…

使用 rosdep 管理依赖关系

什么是rosdep&#xff1f; rosdep是 ROS 的依赖管理实用程序&#xff0c;可以与 ROS 包和外部库一起使用。 是一个命令行实用工具&#xff0c;用于标识和安装依赖项以生成或安装包。 在以下情况下&#xff0c;可以调用或调用它&#xff1a;rosdep 构建工作区并需要适当的依赖项…

关于CodeReview的一些实践和思考

在日常开发中&#xff0c;Code Review 的重要性日益凸显。它不仅有助于提升代码质量&#xff0c;还促进了团队成员之间的知识共享和技能提升。本文将主要聚焦于 Code Review&#xff0c;分享在这个过程中的一些心得和思考。 CodeReview常用到的一些术语 之前看到公司的大佬经…