Skip to content

Latest commit

 

History

History
1293 lines (998 loc) · 44.9 KB

14.advance_usages.md

File metadata and controls

1293 lines (998 loc) · 44.9 KB

14. 高级用法

14.1 特殊属性绑定

在前面的介绍中,数据绑定都是以 text 属性和 value 属性为例的。实际上,数据绑定可以作用于控件的任意属性。比如,可以绑定 enable 属性来决定控件是否可用,绑定 visible 属性来决定控件是否可见。在下面的几个例子中,我们来看看几种常见属性的绑定方法和应用场景。

14.1.1 控件的显示和隐藏

在有的情况下,我们希望界面上某些控件隐藏起来,在另外一种情况下,我们又希望界面上这些控件能显示出来,这可以通过绑定 visible 属性来实现。

比如,在下面的例子中,其中一个 label 控件在 value 小于 50 时隐藏,另外一个 label 控件在 value 大于 50 时隐藏。视图的 XML 可以这样写:

<window v-model="temperature">
  <label x="center" y="middle:-40" w="50%" h="40" v-data:visible="{value > 50}" text="Visible if value > 50"/>
  <label x="center" y="middle:-40" w="50%" h="40" v-data:visible="{value < 50}" text="Visible if value < 50"/>
  <label x="center" y="middle" w="50%" h="40" v-data:text="{value}"/>
  <slider x="center" y="middle:40" w="80%" h="20" v-data:value="{value}"/>
</window>

这里"{value > 50}"是嵌入的表达式,表达式的用法后面会详细介绍,这里的意思是 value 属性的值大于 50 时,该表达式为真,此时控件可见,否则控件不可见。

前面曾经提到,AWTK-MVVM 并不擅长处理动态界面,而 visible 的属性绑定,可以在一定程度上帮助我们实现界面的动态变化,请根据具体情况酌情使用。

这里,界面上多了两个控件,仍然使用了 temperature.js 作为 ViewModel, 而不需要对 temperature.js 进行任何修改。由此也可以看出,View 和 ViewModel 之间是一种松耦合。

Windows 的命令行下,读者可以运行 jsdemo17 来查看实际的效果。

bin\jsdemo17

14.1.2 数据联动

AWTK-MVVM 非常擅长处理数据联动。数据联动是指一个控件的数据变化,会引起其它相关控件的变化。

填写收货地址就是一个典型的数据联动的例子:

  • 选择"省/直辖市"时,"城市"要跟着变化,"区县"也要跟着变化,完整地址也要跟着变化。

  • 选择"城市"时,"区县"要跟着变化,完整地址也要跟着变化。

  • 选择"区县"时,完整地址要跟着变化。

  • 输入"详细地址"时,完整地址要跟着变化。

address

如果采用传统方式来开发,不但要写一大堆代码去处理事件,更重要的是这些代码与界面耦合到一起,难以维护和自动测试。

现在我们来看看 AWTK-MVVM 如何实现数据联动:

视图的 XML 文件:

<window v-model="address"> 
  <combo_box x="center" y="middle:-80" w="60%" h="30" 
    readonly="true" v-data:options="{province_list}" v-data:text="{province}"/>
  <combo_box x="center" y="middle:-40" w="60%" h="30" 
    readonly="true" v-data:options="{city_list}" v-data:text="{city}"/>
  <combo_box x="center" y="middle" w="60%" h="30" 
    readonly="true" v-data:options="{country_list}" v-data:text="{country}"/>
  <edit x="center" y="middle:40" w="60%" h="30" v-data:value="{detail, Trigger=Changing}" tips="detail address"/>
  <hscroll_label x="center" y="middle:80" w="90%" h="30" v-data:text="{address}"/>
</window>

这里把 combo_box 属性 options 绑定到 ViewModel 相应的数据上,ViewModel 的数据变化时,会自动更新到界面上。比如城市列表绑定到 city_list 数据上,只要 city_list 变化,城市列表会自动更新。

ViewModel 的实现:

为简明起见,这里只使用了很少的 demo 数据。

ViewModel('address', {
  _data : {
    'Beijing' : {'Beijing' : [ 'Dongcheng', 'Xicheng', 'Chaoyang', 'Fengtai', 'Shijingshan', 'Haidian' ]},
    'Shanghai' : {'Shanghai' : [ 'Xuhui', 'Changning', 'Jingan', 'Putuo', 'Hongkou', 'Yangpu' ]},
    'Guangdong' : {
      'Guangzhou' : [ 'Tianhe', 'Huangpu', 'Liwan', 'Yuexiu', 'Haizhu' ],
      'Shenzhen' : [ 'Luohu', 'Futian', 'Nanshan', 'Baoan', 'Longgang' ]
    }
  },
  _province : 'Beijing',
  _city : 'Beijing',
  data : {
    country : 'Dongcheng',
    detail: ''
  },
  // 属性相关函数的集合对象
  computed : {
    // 返回省列表
    province_list : {
      get : function() {
        console.log(this._data);
        return Object.keys(this._data).join(';');
      }
    },
    // 根据当前的省/直辖市返回城市列表。
    city_list : {
      get : function() {
        return Object.keys(this._data[this.province]).join(';');
      }
    },
    // 根据当前的省/直辖市和城市返回区县列表。
    country_list : {
      get : function() {
        return this._data[this.province][this.city].join(';');
      }
    },
    // 地址由省/直辖市、城市、区县和详细地址合成。
    address : {
      get : function() {
        return this.province + ' ' + this.city + ' ' + this.country + ' ' + this.detail;
      }
    },
    // province属性的get、set函数
    province : {
      get : function() {
        return this._province;
      },
      set : function(val) {
        this._province = val;
        this._city = this.city_list.split(';')[0];
      }
    },
    // city属性的get、set函数
    city : {
      get : function() {
        return this._city;
      },
      set : function(val) {
        this._city = val;
        this.country = this.country_list.split(';')[0];
      }
    }
  }
});

Windows 的命令行下,读者可以运行 demo15 来查看实际的效果。

bin\jsdemo15

14.1.3 动态界面

我们在本章的开头,展示了用 visible 属性来实现一定程度的动态界面,但是如果有大量控件都需要通过 visible 来控制,XML 文件写起来就很繁琐。这时我们可以用 pages 控件对需要显示和隐藏的控件进行分组。

比如,我们有一个通信设置界面,通信有串口和 socket 两种方式,只能二选一。由于两者的参数截然不同,此时就可以把串口和 socket 的参数分别放到不同的 page 中,然后通过 type 进行切换。

  • 串口界面:

  • Socket 设置界面

视图的 XML 文件:

<window v-model="com_settings"> 
  <combo_box x="r:24" y="10" w="200" h="30" options="UART;SOCKET" v-data:value="{type}" readonly="true"/>
  <pages x="10" y="50" w="-20" h="-90" v-data:value="{type}">
    <view x="0" y="0" w="100%" h="100%" children_layout="default(c=1,h=25,m=0,s=5)">
      <row children_layout="default(c=0,r=1,m=0,s=5)">
        <label text="Device" w="80"/>
        <combo_box w="200" options="COM1;COM2;COM3" v-data:text="{device}" readonly="true"/>
      </row>
      <row children_layout="default(c=0,r=1,m=0,s=5)">
        <label text="Baud Rate" w="80"/>
        <combo_box w="200" options="9600;115200;" v-data:text="{baudrate}" readonly="true"/>
      </row>
      <row children_layout="default(c=0,r=1,m=0,s=5)">
        <label text="Parity" w="80"/>
        <combo_box w="200" options="none;odd;even" v-data:value="{parity}" readonly="true"/>
      </row>
    </view>
    <view x="0" y="0" w="100%" h="100%" children_layout="default(c=1,h=25,m=0,s=5)">
      <row children_layout="default(c=0,r=1,m=0,s=5)">
        <label text="IP" w="80"/>
        <edit w="200" v-data:text="{ip, Trigger=Changing}" max="15"/>
      </row>
      <row children_layout="default(c=0,r=1,m=0,s=5)">
        <label text="Port" w="80"/>
        <edit  w="200" input_type="uint" min="0" max="10000" auto_fix="true" v-data:text="{port, Trigger=Changing}"/>
      </row>
    </view>
  </pages>
  <label x="10" y="m" w="100%" h="30" v-data:text="{summary}" />
</window>

重点看看下面两行代码,combo_box 和 pages 都绑定到 type 属性上,用户通过 combo_box 选择不同的通信方式时,pages 会自动切换。通过 pages 的切换,就可以更灵活的实现动态界面。

  <combo_box x="r:24" y="10" w="200" h="30" options="UART;SOCKET" v-data:value="{type}" readonly="true"/>
  <pages x="10" y="50" w="-20" h="-90" v-data:value="{type}">

Model 实现非常简单,仅有几个数据成员。

ViewModel('com_settings', {
  data: {
    type: 0,
    ip: '192.168.1.1',
    port: '8088',
    device: 'COM1',
    baudrate: '9600',
    parity: 0
  },
  computed: {
    // 概要由其他属性合成。
    summary: function () {
      var summary = '';
  
      if(this.type == 0) {
        var parity_name = ["None", "Odd", "Even"];
        summary = 'UART: ' + this.device + ' ' + this.baudrate + ' ' + parity_name[this.parity];
      } else {
        summary = "SOCKET: " + this.ip + ' ' + this.port;
      }
  
      return summary;
    }
  }
});

Windows 的命令行下,读者可以运行 demo16 来查看实际的效果。

bin\jsdemo16

14.2 嵌入表达式

AWTK-MVVM 允许在界面描述的 XML 文件中嵌入简单的表达式,合理的使用表达式可以简化 ViewModel、代替简单的数据转换器和数据检验器。在本章开头我们给出的例子就把 visible 属性绑定到表达式"value > 50"上了。

<window v-model="temperature">
  <label x="center" y="middle:-40" w="50%" h="40" v-data:visible="{value > 50}" text="Visible if value > 50"/>
  <label x="center" y="middle:-40" w="50%" h="40" v-data:visible="{value < 50}" text="Visible if value < 50"/>
  <label x="center" y="middle" w="50%" h="40" v-data:text="{value}"/>
  <slider x="center" y="middle:40" w="80%" h="20" v-data:value="{value}"/>
</window>

最新版本使用 fscript 脚本引擎,请参考 fscript 脚本引擎 获取更多信息。

注意:嵌入表达式的变量名以 "$" 符号开头是过时用法,比如 v-data:visible="{$value > 50}" 中的符号 "$" 可以删除,但仍然保存对旧版的兼容。

变量名代表 ViewModel 中的属性,中所有的属性均可以通过变量引用,比如 value 可以引用 ViewModel 中的 value 属性。

表达式可以有多条语句,语句之间用英文的分号分隔,其值以最后一个子表达式为准。

比如,下面这个表达式的值为最后一个子表达式,即:123

widget_set(window.inc.visible, if(value < 90, true, false));123

14.2.4 MVVM 新增函数

除了 fscript 本身提供的函数外,MVVM 增加了下面的函数:

widget 相关函数请参考:https://github.com/zlgopen/awtk/blob/master/docs/fscript_widget.md

14.2.4.5 navigator_to

打开新窗口

  • 原型
navigator_to(target) => void
  • target 新窗口名称
  • 返回值:无
14.2.4.5 navigator_replace

打开新窗口,并关闭当前窗口。

  • 原型
navigator_replace(target) => void
  • target 新窗口名称
  • 返回值:无
14.2.4.6 navigator_close

关闭指定窗口。

  • 原型
navigator_close(target) => void
  • target 目标窗口的名称
  • 返回值:无

完整示例请参考 demo28:

<window v-model="temperature" children_layout="default(h=30,c=2,m=3,s=2,keep_invisible=true)">
  <slider name="slider1">
    <property name="v-data:value">
      <![CDATA[
      {value,
      ToModel=
      widget_set(window.inc.visible, if(value < 90, true, false))
      widget_set(window.dec.visible, if(value > 10, true, false))
      value}
      ]]>
    </property>
  </slider>
  <slider name="slider2" v-data:value="{value, ToModel=value+10, ToView=value-10}" />
  <label name="text1" v-data:text="{value}" />
  <label name="text2">
    <property name="v-data:text">
      <![CDATA[{if(value < 50, "low", "high")}]]>
    </property>
  </label>
  <button name="inc" text="Inc" v-on:click="{fscript, Args=widget_set(window.slider1.value, widget_get(window.slider1.value)+10)}" />
  <button name="dec" text="Dec" v-on:click="{fscript, Args=widget_set(window.slider1.value, widget_get(window.slider1.value)-10)}" />
  <button text="Open" v-on:click="{fscript, Args=navigator_to(temperature28)}" />
  <button text="Replace" v-on:click="{fscript, Args=navigator_replace(temperature28)}" />
  <button text="Exec Open" v-on:click="{fscript, Args=exec(navigate, temperature28)}" />
</window>
<window v-model="temperature" children_layout="default(h=30,c=1,m=3,s=2)">
  <label v-data:text='{string(number("123") + number("456"))}'/>
  <label v-data:text='{fformat("%2.2lf", value)}'/>
  <label v-data:text='{fformat("%2.2lf ℃", value)'/>
  <label v-data:text='{iformat("%d ℃", value)'/>
  <label v-data:text='{fformat("%2.2lf F", value * 1.8 + 32)}'/>
  <label v-data:text='{fformat("%2.2lf F", round(value * 1.8 + 32))}'/>
  <label v-data:text='{fformat(tr("current temperature %f"), value)+"℃"}'/>
  <label v-data:text='{string(value)+" ℃"}'/>
  <label v-data:text='{if(value > 60, "too height", if(value < 20, "too low", "normal"))}'
    v-data:style='{if(value >= 20 &amp;&amp; value <= 60, "green", "red")}'
    />
  <label v-data:text="{value}"/>
  <slider v-data:value="{value}"/>
  <button text="Back" v-on:click="{fscript, Args=back()}" />
  <button text="Home" v-on:click="{fscript, Args=back_to_home()}" />
</window>

14.2.6 表达式中的特殊字符

由于表达式中<>"等字符对于 XML 来说是特殊字符,需要转换成对应的实体 (entity),但是转换之后表达式不太直观,此时可以把属性提出来,放在 property 标签中,并用 CDATA 把它的值包起来。如:

<window v-model="temperature">
  <label x="center" y="middle:-40" w="50%" h="40">
    <property name="v-data:text"><![CDATA[ {(value < 50) ? "low" : "high"} ]]></property>
    <property name="v-data:style"><![CDATA[ {(value < 50) ? "green" : "default" }]]></property>
  </label>
  <label x="center" y="middle" w="50%" h="40" v-data:text="{value}"/>
  <slider x="center" y="middle:40" w="80%" h="20" v-data:value="{value}"/>
</window>

嵌入表达式提供了一定的灵活性,但是表达式在 XML 中,没有办法参与单元测试,而且调试很困难,所以请不要在 XML 中嵌入复杂的表达式。

XML 中常用的转义字符如下:

  • 空格" ":&nbsp;
  • 小于号"<":&lt;
  • 大于号">":&gt;
  • 与符号"&":&amp;
  • 双引号""":&quot;
  • 单引号"'":&apos;

Windows 的命令行下,读者可以运行 demo18 来查看实际的效果。

bin\jsdemo18

14.3 动态渲染

在项目中,经常会遇到动态加载不同内容的场景,比如显示动态变化的数据集合,或者根据不同的条件加载不同的内容。

MVVM 提供了两种方式来实现上述场景:

  • 列表渲染:v-for;

  • 条件渲染:v-if、v-elif、v-else。

具体用法详见下文。

动态渲染过程中,控件只有在满足显示条件时才会创建。

由于 MVVM 绑定时需遍历 XML 上控件的所有属性,以查找控件的动态渲染规则。当界面复杂时,效率较低。此时可通过定义宏 MVVM_FAST_UI_LOAD 提高效率。启用该宏时有一点需注意,就是动态渲染规则须为控件在 XML 上的第1个属性。

14.3.1 列表渲染

列表渲染可将指定控件作为模板,将列表中的各项数据进行重复渲染。

列表渲染规则也是一个控件属性:

  • 属性的名称固定为 v-for,表示该属性是一个列表渲染规则。

  • 属性的值放在'{'和'}'之间,里面是 ViewModel 中的一个数组的名称,即可使用数组中的各项数据重复渲染该控件。

  • 数组当前项下标的变量名默认为 index,在v-for属性所属的控件及其子控件中可以通过该变量名访问当前项下标,在列表渲染规则中可以使用 Index 参数指定该变量名。

  • 数组当前项的变量名默认为 item,在v-for属性所属的控件及其子控件中可以通过该变量名访问当前项的值,在列表渲染规则中可以使用 Item 参数指定该变量名。

比如,

v-for="{items, Index=index_name, Item=item_name}"

为了兼容v-for-items的用法,可以用 v-for="{}" 表示绑定的数组为 ViewModel 自身,以实现类似的 v-for-items="true"的效果。

下面看一个图书列表的示例。

14.3.1.1 视图
  • name 表示书名。可以用 item.name 访问它。
  • stock 表示库存数量。存数量大于 0 表示可以卖。可以用 item.stock 访问它。
  • index 表示当前的索引序数。
  • 通过 Args 设置命令参数为当前的索引序数。
<window anim_hint="htranslate" v-model="books">
   <label x="0" y="0" w="100%" h="30" v-data:text="{items.length}"/>
   <list_view x="0"  y="30" w="100%" h="-80" item_height="40">
    <scroll_view name="column" x="0"  y="0" w="100%" h="100%">
      <list_item v-for="{items}" children_layout="default(rows=1,cols=0,s=4)">
        <property name="v-data:style">
          <![CDATA[ {(index % 2) ? "odd" : "even"} ]]>
        </property>

        <label w="20" v-data:text="{index}"/>
        <label w="35%" v-data:text="{item.name}"/>
        <label w="40" v-data:text="{item.stock}"/>
        <column  w="128" children_layout="default(rows=1,cols=0,s=5,ym=5)">
          <button w="70" text="Remove" v-on:click="{remove, Args=fscript?index=index}"/>
          <button w="50" text="Sale" v-on:click="{sale, Args=fscript?index=index}"/>
        </column>
      </list_item>
    </scroll_view>
    <scroll_bar_m name="bar" x="right" y="0" w="6" h="100%" value="0"/>
  </list_view>
  <column  x="0" y="b" w="100%" h="40" children_layout="default(rows=1,col=2,s=5,m=5)">
    <button text="Add" v-on:click="{add}"/>
    <button text="Clear" v-on:click="{clear}"/>
  </column>
</window>

下面这行代码让 label 控件显示序数。

  <label w="20" v-data:text="{index}"/>

下面这行代码让 label 控件显示书名。

  <label w="35%" v-data:text="{item.name}"/>

下面这行代码让 label 控件显示库存。

  <label w="40" v-data:text="{item.stock}"/>

显示效果如下:

books

14.3.1.2 视图模型

绑定列表视图的 data 对象中包含 v-for 属性指定的 items 数组,methods 对象中可以提供一些成员函数供视图绑定命令。这里就提供了 remove、clear、sale 和 add 四个命令。

ViewModel('books', {
  data: {
    items: []
  },
  methods: {
    _add: function () {
      var item = {
        name: "book" + Math.round(Math.random() * 1000),
        stock: Math.round(Math.random() * 100)
      }
      this.items.push(item);
    },
    add: function () {
      this._add();
      this.notifyItemsChanged(this.items);
    },
    canRemove: function(args) {
      return args.index < this.items.length;
    },
    remove: function(args) {
      this.items.splice(args.index, 1);
      this.notifyItemsChanged(this.items);
    },
    canSale: function(args) {
      return this.items[args.index].stock > 0;
    },
    sale: function(args) {
      this.items[args.index].stock = this.items[args.index].stock - 1;
      this.notifyPropsChanged();
    },
    canClear: function() {
      return this.items.length > 0;
    },
    clear: function() {
      this.items.splice(0, this.items.length);
      this.notifyItemsChanged(this.items);
    },
    canClear: function () {
      return this.items.length > 0;
    },
    clear: function () {
      this.items.splice(0, this.items.length);
      this.notifyItemsChanged(this.items);
    }
  },
  onCreate: function (req) {
    for (var i = 0; i < 100; i++) {
      this._add();
      this._add();
      this._add();
    }
  }
});

Windows 的命令行下,读者可以运行 jsdemo13 来查看实际的效果。

bin\jsdemo13
14.3.1.3 过时用法 v-for-items

使用 v-for-items 属性同样可以实现列表渲染,但固定绑定 ViewModel 自身。在绑定列表视图时,设置属性 v-for-items 为"true",那么该控件的子控件就会动态生成,第一个子控件为生成其它子控件的模板。

v-for-items 是过时用法,推荐使用 v-for 代替。

由于 v-for-items 是过时用法,因此,此处只做简单的示例演示,我们来看一个使用 v-for-items 实现的图书列表示例:

v-for-items 的命令如果没有显示设置参数时,默认会将当前项的下标作为参数,其视图如下:

<window anim_hint="htranslate" v-model="books">
   <label x="0" y="0" w="100%" h="30" v-data:text="{items}"/>
   <list_view x="0"  y="30" w="100%" h="-80" item_height="40">
    <scroll_view v-for-items="true" name="column" x="0"  y="0" w="100%" h="100%">
      <list_item children_layout="default(rows=1,cols=0,s=4)">
        <property name="v-data:style">
          <![CDATA[ {(index % 2) ? "odd" : "even"} ]]>
        </property>

        <label w="20" v-data:text="{index}"/>
        <label w="35%" v-data:text="{item.name}"/>
        <label w="40" v-data:text="{item.stock}"/>
        <column  w="128" children_layout="default(rows=1,cols=0,s=5,ym=5)">
          <button w="70" text="Remove" v-on:click="{remove}"/>
          <button w="50" text="Sale" v-on:click="{sale}"/>
        </column>
      </list_item>
    </scroll_view>
    <scroll_bar_m name="bar" x="right" y="0" w="6" h="100%" value="0"/>
  </list_view>
  <column  x="0" y="b" w="100%" h="40" children_layout="default(rows=1,col=2,s=5,m=5)">
    <button text="Add" v-on:click="{add}"/>
    <button text="Clear" v-on:click="{clear}"/>
  </column>
</window>

Model 中定义一个动态数组 books 用来生成列表项,并提供 remove、clear、sale 和 add 四个命令的函数,声明如下:

/**
 * @class book_store_t
 * book store。
 *
 * @annotation ["collection:book_t", "model"]
 *
 */
typedef struct _book_store_t {
  /**
   * @property {uint32_t} items
   * @annotation ["fake", "readable"]
   * 总数量。
   */

  /*private*/
  darray_t books;
} book_store_t;

/**
 * @method book_store_create
 * 创建book_store对象。
 *
 * @annotation ["constructor"]
 *
 * @return {book_store_t*} 返回book_store对象。
 */
book_store_t* book_store_create(void);

/**
 * @method book_store_destroy
 * 销毁book_store对象。
 *
 * @annotation ["destructor"]
 * @param {book_store_t*} book_store book_store对象。
 *
 * @return {ret_t} 返回RET_OK表示成功,否则表示失败。
 */
ret_t book_store_destroy(book_store_t* book_store);

/**
 * @method book_store_clear
 * 清除全部数据。
 *
 * @annotation ["command"]
 * @param {book_store_t*} book_store book_store对象。
 *
 * @return {ret_t} 返回RET_ITEMS_CHANGED表示模型有变化,View需要刷新;返回其它表示失败。
 */
ret_t book_store_clear(book_store_t* book_store);

/**
 * @method book_store_remove
 * 删除指定序数的book。
 *
 * @annotation ["command"]
 * @param {book_store_t*} book_store book_store对象。
 * @param {uint32_t} index 序数。
 *
 * @return {ret_t} 返回RET_ITEMS_CHANGED表示模型有变化,View需要刷新;返回其它表示失败。
 */
ret_t book_store_remove(book_store_t* book_store, uint32_t index);

/**
 * @method book_store_can_remove
 * 检查remove命令是否可以执行。
 *
 * @param {book_store_t*} book_store book_store对象。
 * @param {uint32_t} index 序数。
 *
 * @return {bool_t} 返回FALSE表示不能执行,否则表示可以执行。
 */
bool_t book_store_can_remove(book_store_t* book_store, uint32_t index);

/**
 * @method book_store_get_items
 * 获取总数。
 *
 * @param {book_store_t*} book_store book_store对象。
 *
 * @return {uint32_t} 返回总数。
 */
uint32_t book_store_get_items(book_store_t* book_store);

/**
 * @method book_store_get
 * 获取指定序数的图书。
 *
 * @param {book_store_t*} book_store book_store对象。
 * @param {uint32_t} index 序数。
 *
 * @return {book_t*} 返回book对象。
 */
book_t* book_store_get(book_store_t* book_store, uint32_t index);

/**
 * @method book_store_add
 * 增加一本图书。
 *
 * @annotation ["command"]
 * @param {book_store_t*} book_store book_store对象。
 *
 * @return {ret_t} 返回RET_ITEMS_CHANGED表示模型有变化,View需要刷新;返回其它表示失败。
 */
ret_t book_store_add(book_store_t* book_store);

Windows 的命令行下,读者可以运行 demo13 来查看实际的效果。

bin\demo13

14.3.2 条件渲染

条件渲染可以根据不同的条件进行不同的渲染。

条件渲染规则也是一个控件属性:

  • 属性的名称可以是 v-if、v-elif 或者 v-else,其中 v-if="{condition}" 是必须的,用来判断是否需要渲染该控件,v-elif 和 v-else 则是可选的,用来添加一个 else 块。

  • 属性的值放在'{'和'}'之间,里面是用来判断是否需要渲染该控件的表达式,v-else的值默认为空。

比如,

<label v-if="{value < 0"/>                         <!-- value 小于0 时渲染这个label控件 -->
<label v-elif="{value > 0 &amp;&amp; value < 2}"/> <!-- value 大于0且小于2时渲染这个label控件 -->
<label v-else=""/>                                 <!-- 其他情况则渲染这个label控件 -->

下面看与列表渲染配合使用的一个示例。

在一个设备管理器中,根据设备类型分别渲染不同的 view 控件,视图代码如下:

      <list_item v-for="items" children_layout="default=(c=0,r=1,s=2,m=5)" v-on:click="{setCurrent, Args=fscript?index=index, AutoDisable=false}">
        <label name="index" w="10%" v-data:text="{index}"/>
        <combo_box w="25%" h="80%" readonly="true" value="0" 
          options="PACK_SKP_1000;PACK_SKP_2000;PACK_SKP_3042;PACK_SKP_3132;PACK_SKP_3142;PACK_SKP_5002"
          v-data:enable="{unlocked}" v-data:value="{item.pack_type}"/>
        <combo_box w="25%" readonly="true" value="0"
          options="None;NTC-2252k;NTC-5k;NTC-10k"
          v-data:enable='{unlocked}' v-data:value="{item.pack_params}"/>
        <view v-if="{item.pack_type == 0}" w="40%" children_layout="default(r=1,c=0)">
          <label w="20%" text="IO1:" style:text_align_h="left" />
          <check_button w="30%" v-data:value="{item.io1}"/>
          <label w="20%" text="IO2:" style:text_align_h="left" />
          <check_button w="30%" v-data:value="{item.io2}"/>
        </view>
        <view v-elif="{item.pack_type > 0 &amp;&amp; item.pack_type <= 2}" w="40%" children_layout="default(r=1,c=0)">
          <label w="20%" text="Temp:" style:text_align_h="left" />
          <label w="80%" v-data:text='{fformat("%lf", item.temp)}'/>
        </view>
        <view v-elif="{item.pack_type > 2 &amp;&amp; item.pack_type <= 4}" w="40%" children_layout="default(r=1,c=0)">
          <label w="20%" text="A1:" style:text_align_h="left" />
          <label w="30%" v-data:text='{fformat("%d", item.a1)}' />
          <label w="20%" text="A2:" style:text_align_h="left" />
          <label w="30%" v-data:text='{fformat("%d", item.a2)}' />
        </view>
        <view v-else="" w="40%" children_layout="default(r=1,c=0)">
          <label w="20%" text=" TPS:" style:text_align_h="left" />
          <label w="80%" v-data:text='{fformat("%d", item.tps)}'/>
        </view>
      </list_item>

Windows 的命令行下,读者可以运行 demo35 来查看实际的效果。

bin\demo35

14.4 外部设备的集成

在温度控制器中,当温度高于某个阀值时,打开蜂鸣器发出警报,此时蜂鸣器也是和用户交互的接口,是视图的一部分。但是蜂鸣器却不是一个 GUI 控件,没有办法像其它 GUI 控件那样,在 XML 中使用数据绑定规则。如何把蜂鸣器这类外部设备集成到 AWTK-MVVM 之中,享受和其它 GUI 控件同等的待遇呢?

答案自然是把这类外设包装成 Widget 组件。把蜂鸣器包装成 Widget 组件之后,就可以直接在 XML 文件中使用这些外设组件了。比如,在温度大于 50 时发出蜂鸣声,就可以在 XML 中这样写:

  <buzzer v-data:on="{value > 50}"/>

我们还可以用 freq、volume 和 duration 属性,来指定蜂鸣器的频率、音量和持续时间:

  <buzzer v-data:on="{value > 50}" freq="3000" volume="60" duration="3000"/>

这样一来,除编写 XML 中的绑定规则,不需要写其它代码,即可使用这些外设组件。这样做的好处有:

  • 简单。可以在 XML 中直接引用外设。
  • 重用。外设组件只需要开发一次,即可在各个项目中使用。
  • 方便于可视化开发环境集成。由于外设包装成了 Widget 接口,具有其它 GUI 控件同等的待遇,直接 IDE 中拖拽和设置属性。

将外设直接包装成 Widget,此类 Widget 就会依赖于硬件,这会带来两个副作用:

  • 在项目的早期,硬件还没有做好,应用程序就没办法运行。
  • 即使硬件可用,但是应用程序不能在 PC 上模拟运行,开发效率会成倍下降。

把硬件抽象成接口,并提供软件实现,让应用程序能脱离硬件运行,是良好架构必备的特征之一。为此我们按下列方式来组织相关的类:

WidgetHardware 实现了 Widget 接口,用来将外设包装成一个 Widget,这样就不需要为每种外设编写一个 Widget 类了。

DeviceObject 是各种外设的抽象,蜂鸣器、LED、GPIO 和各种传感器实现了该接口,才能接入 AWTK-MVVM 框架中来。

WidgetHardware 不能直接创建 DeviceObject,否则 WidgetHareware 就和具体硬件耦合到一起了,所以引入 DeviceFactory 来隔离具体的硬件。

将外设接入 AWTK-MVVM 架构中,将外设包装成一个 DeviceObject 即可。下面我们来看看如何将外设包装成 device_object。

14.4.1 输出型的外设

将输出类型的外设包装成 device_object 非常简单,只需要实现 set_prop 函数即可,根据指定的属性执行对应的操作。

下面我们以蜂鸣器为例,基于软件实现一个蜂鸣器。蜂鸣器提供了音量、持续时间、频率和开关几个属性。

  /**
   * @property {uint32_t} volume
   * @annotation ["set_prop","get_prop"]
   * 音量 (0-100)。
   */
  uint32_t volume;

  /**
   * @property {uint32_t} duration 
   * @annotation ["set_prop","get_prop"]
   * 持续时间 (ms)。
   */
  uint32_t duration;

  /**
   * @property {uint32_t} freq
   * @annotation ["set_prop","get_prop"]
   * 频率。
   */
  uint32_t freq;

  /**
   * @property {bool_t} on
   * @annotation ["set_prop","get_prop"]
   * 开启。
   */
  bool_t on;

软件实现的蜂鸣器只是把收到的属性打印出来,所以实现非常简单,也就几行代码。基于硬件实现,则在此调用硬件的函数。

static ret_t buzzer_log_set_prop(tk_object_t* obj, const char* name, const value_t* v) {
  buzzer_log_t* buzzer = BUZZER_LOG(obj);
  return_value_if_fail(obj != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (v->type == VALUE_TYPE_STRING) {
    log_debug("buzzer: %s = %s\n", name, value_str(v));
  } else {
    log_debug("buzzer: %s = %d\n", name, value_int(v));
  }

  return RET_OK;
}

static const object_vtable_t s_buzzer_log_vtable = {.type = "buzzer_log",
                                                    .desc = "buzzer_log",
                                                    .size = sizeof(buzzer_log_t),
                                                    .set_prop = buzzer_log_set_prop};

object_t* buzzer_log_create(const char* args) {
  return tk_object_create(&s_buzzer_log_vtable);
}

14.4.2 输入型的外设

输入类型的外设的实现稍微复杂一点,主要原因就是开启一个定时器,在定时器中去读取当前的状态。下面我们温度传感器为例,看看如何用软件实现一个温度传感器,该温度传感器随机生成温度值。

温度传感器有以下温度和采用间隔时间两个属性:

  /**
   * @property {double} value
   * @annotation ["get_prop"]
   * 最新的温度。
   */
  double value;

  /**
   * @property {int32_t} sample_interval
   * @annotation ["set_prop","get_prop"]
   * 采样时间间隔 (ms)。
   */
  int32_t sample_interval;
static ret_t temperature_sensor_random_on_destroy(tk_object_t* obj) {
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);

  timer_remove(temperature_sensor->timer_id);
  temperature_sensor->timer_id = TK_INVALID_ID;

  return RET_OK;
}

static ret_t temperature_sensor_random_set_prop(tk_object_t* obj, const char* name, const value_t* v) {
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);
  return_value_if_fail(obj != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_eq(name, TEMPERATURE_SENSOR_PROP_SAMPLE_INTERVAL)) {
    int32_t interval = value_int(v);

    if (interval > 0) {
      timer_modify(temperature_sensor->timer_id, interval);
    } else {
      timer_modify(temperature_sensor->timer_id, 0xffffffff);
    }
    temperature_sensor->sample_interval = interval;

    return RET_OK;
  }

  return RET_NOT_FOUND;
}

static ret_t temperature_sensor_sample(tk_object_t* obj) {
  event_t e = event_init(EVT_VALUE_CHANGED, obj);
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);

  temperature_sensor->value = random() % 100;

  emitter_dispatch(EMITTER(obj), &e);

  return RET_REPEAT;
}

static ret_t temperature_sensor_on_timer(const timer_info_t* info) {
  temperature_sensor_sample(TK_OBJECT(info->ctx));

  return RET_REPEAT;
}

static ret_t temperature_sensor_random_get_prop(tk_object_t* obj, const char* name, value_t* v) {
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);
  return_value_if_fail(obj != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_eq(name, TEMPERATURE_SENSOR_PROP_SAMPLE_INTERVAL)) {
    value_set_int(v, temperature_sensor->sample_interval);
    return RET_OK;
  } else if (tk_str_eq(name, TEMPERATURE_SENSOR_PROP_VALUE)) {
    value_set_int(v, temperature_sensor->value);
    return RET_OK;
  }

  return RET_NOT_FOUND;
}

static const object_vtable_t s_temperature_sensor_random_vtable = {
    .type = "temperature_sensor_random",
    .desc = "temperature_sensor_random",
    .size = sizeof(temperature_sensor_random_t),
    .is_collection = FALSE,
    .on_destroy = temperature_sensor_random_on_destroy,
    .get_prop = temperature_sensor_random_get_prop,
    .set_prop = temperature_sensor_random_set_prop};

object_t* temperature_sensor_random_create(const char* args) {
 tk_object_t* obj = tk_object_create(&s_temperature_sensor_random_vtable);
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);
  return_value_if_fail(temperature_sensor != NULL, NULL);

  temperature_sensor_sample(obj);
  temperature_sensor->timer_id = timer_add(temperature_sensor_on_timer, obj, 5000);

  return obj;
}

对于硬件实现,在采样函数中,将生成随机温度的代码换成读取实际温度的代码即可。

static ret_t temperature_sensor_sample(tk_object_t* obj) {
  event_t e = event_init(EVT_VALUE_CHANGED, obj);
  temperature_sensor_t* temperature_sensor = TEMPERATURE_SENSOR(obj);

  temperature_sensor->value = random() % 100;

  emitter_dispatch(EMITTER(obj), &e);

  return RET_REPEAT;
}  

14.4.3 注册控件和设备

工厂是一种依赖注入常见的方法。设备和 Widget 都是插件,把它们注册到工厂中,框架才能在需要的时候创建它们。应用程序在初始化时,根据当前的平台注册软件模拟的设备或者真实的硬件设备,在有硬件外设时就使用硬件外设,在没有硬件外设时就用软件模拟的外设,应用程序的其它部分完全不用关心当前运行的平台。

ret_t hardware_init(void) {
  device_factory_init();
#ifdef AWORKS_OS
  /*register hardware device here */
#else
  device_factory_register(BUZZER_TYPE, buzzer_log_create);
  device_factory_register(TEMPERATURE_SENSOR_TYPE, temperature_sensor_random_create);
#endif/*AWORKS_OS*/

  widget_factory_register(widget_factory(), BUZZER_TYPE, widget_buzzer_create);
  widget_factory_register(widget_factory(), TEMPERATURE_SENSOR_TYPE,
                          widget_temperature_sensor_create);

  return RET_OK;
}

Windows 的命令行下,读者可以运行 demo37 来查看实际的效果。

bin\demo37

14.5 组合 View Model

有时我们需要访问多个 view model,此时可以把这些 view model 组合起来。

14.5.1 用法

组合 view model 非常简单,将多个 view model 的类型,用“+”联机起来即可。

比如下面的例子把 temperature, humidity 和 app_conf 三个 view model 组合到一起。

<window v-model="app_conf" >
  <view w="100%" h="100%" v-model="temperature+humidity+sub_view_model:led">
    ...
   </view>
</window>

注意:sub_view_model 是 app_conf 的一种用法,具体请参考 配置类界面

完整的 xml 如下:

<window v-model="app_conf" >
  <view w="100%" h="100%" v-model="temperature+humidity+sub_view_model:led" children_layout="default(h=30,c=1,m=5,s=5)">
    <label v-data:text="{temp}"/>
    <slider v-data:value="{temp, Trigger=Changing}"/>
    
    <label v-data:text="{humi}"/>
    <slider v-data:value="{humi, Trigger=Changing}"/>

    <label v-data:text="{lightness}"/>
    <slider v-data:value="{lightness, Trigger=Changing}"/>

    <button text="Save" floating="true" self_layout="default(x=20, y=b:10, w=80, h=30)" 
      v-on:click="{save}"/>
    <button text="Reload" floating="true" self_layout="default(x=c, y=b:10, w=80, h=30)" 
      v-on:click="{reload}"/>
    <button text="Close" floating="true" self_layout="default(x=r:20, y=b:10, w=80, h=30)" 
          v-on:click="{nothing, QuitApp=true}"/>
   </view>
</window>

在这个例子中:

  • 属性 temp 取自于 temperature

  • 属性 humi 取自于 humidity

  • 属性 led.lightness 取自于 app_conf

  • 命令 save/reload 取自于 app_conf

14.5.2 注意事项

  • 如果出现同名的属性和命令,优先使用前面的 view model。所以当用到 app_conf 时,要放到最后,因为 app_conf 任何属性都是支持的。

  • 不支持 view modal array。

完整例子请参考:demo27

14.6 嵌套 View Model

有时我们需要按树形结构组织多个 view model,访问数据时,先从当前的 view model 去找,如果找不到,就去上一级 view model 找,直到找到数据或到达顶层为止。

14.6.1 用法

比如下面这个例子:

<window anim_hint="htranslate">
  <view v-model="temperature" w="100%" h="100%" children_layout="default(r=2, c=1, m=10)">
    <view>
      <label text="1" x="center" y="middle" w="50%" h="40" v-data:text="{temp}" />
      <slider value="1" x="center" y="middle:40" w="80%" h="20" v-data:value="{temp, Trigger=Changing}" />
    </view>
    <view v-model="humidity">
      <label text="1" x="center" y="middle:-40" w="50%" h="40" v-data:text="{temp}" />
      <label text="1" x="center" y="middle" w="50%" h="40" v-data:text="{humi}" />
      <slider value="1" x="center" y="middle:40" w="80%" h="20" v-data:value="{humi, Trigger=Changing}" />
    </view>
  </view>
  <button text="Close" floating="true" x="right:10" y="bottom:4" w="60" h="30" v-on:click="{nothing, CloseWindow=True, QuitApp=true}" />
</window>

在 humidity 这个 view model 里,是没有 temp 这个数据的,所以它就会到上一级的 temperature 里去找。

    <view v-model="humidity">
      <label text="1" x="center" y="middle:-40" w="50%" h="40" v-data:text="{temp}" />
      <label text="1" x="center" y="middle" w="50%" h="40" v-data:text="{humi}" />
      <slider value="1" x="center" y="middle:40" w="80%" h="20" v-data:value="{humi, Trigger=Changing}" />
    </view>

完整例子请参考:demo31

14.7 在多个窗口之间共享一个 View Model。

有时需要在多个窗口之间共享一个 view model,其实很简单,弄一个全局变量保存 view model 的指针。

但是要注意 view model 的生命周期管理,在第一次创建时真正创建,以后每次把引用计数加 1,返回该对象即可。

  • 方法一

销毁后要把全局变量置为空,否则可能出现野指针的问题。

示例:

static view_model_t* s_view_model_temp_share = NULL;

static ret_t temperature_view_model_on_destroy(void* ctx, event_t* e) {
  s_view_model_temp_share = NULL;

  return RET_OK;
}

static view_model_t* temperature_view_model_create_share(navigator_request_t* req) {
  if (s_view_model_temp_share == NULL) {
    s_view_model_temp_share = temperature_view_model_create(req);
    emitter_on(TK_OBJECT(s_view_model_temp_share), EVT_DESTROY, temperature_view_model_on_destroy,
               NULL);
  } else {
    TK_OBJECT_REF(s_view_model_temp_share);
  }

  return s_view_model_temp_share;
}

ret_t application_init(void) {
  view_model_factory_register("temperature", temperature_view_model_create_share);

  return navigator_to("home_page");
}
  • 方法二

在第一次创建后,把引用计数加 1,可以保证该对象不会被释放。记得在应用程序退出时,要释放该对象。

示例:

static view_model_t* s_view_model_temp_share = NULL;

static view_model_t* temperature_view_model_create_share(navigator_request_t* req) {
  if (s_view_model_temp_share == NULL) {
    s_view_model_temp_share = temperature_view_model_create(req);
    TK_OBJECT_REF(s_view_model_temp_share);
  } else {
    TK_OBJECT_REF(s_view_model_temp_share);
  }

  return s_view_model_temp_share;
}

ret_t application_init(void) {
  view_model_factory_register("temperature", temperature_view_model_create_share);

  return navigator_to("home_page");
}

ret_t application_deinit(void) {
  
  ...
  TK_OBJECT_UNREF(s_view_model_temp_share);
  ...
  return RET_OK;
}

14.8 自定义 binder

在一些特殊情况下,awtk-mvvm 缺省的方法不支持特殊的绑定规则,此时可以自定义一个 binder 并将其注册到 awtk-mvvm 中。

比如,自定义控件 table_view 有点特殊,列表项无需事先创建,并且数据无需全部加载到内存,awtk-mvvm 提供的列表渲染 (v-for) 方法不支持这种特殊用法,我们可以自定义一个 binder 实现该功能。

自定义控件 table_view 的源码以及使用方法请参考:awtk-widget-table-view

14.8.1 实现 bind 函数

在本示例中,bind 函数为 table_client_bind(),代码如下:

static ret_t table_client_on_load_data_mvvm(void* ctx, uint32_t row_index, widget_t* row) {
  items_binding_t* binding = ITEMS_BINDING(ctx);
  widget_t* widget = widget_get_child(row->parent, 0);

  if (widget == row) {
    binding->start_item_index = TABLE_ROW(widget)->index;
    binding_context_update_to_view(BINDING_RULE_CONTEXT(binding));
  }

  return RET_OK;
}

static ret_t table_client_on_items_changed(void* ctx, event_t* e) {
  value_t v;
  binding_rule_t* rule = BINDING_RULE(ctx);
  items_binding_t* binding = ITEMS_BINDING(rule);
  binding_context_t* bctx = BINDING_RULE_CONTEXT(rule);

  if (binding_context_get_prop_by_rule(bctx, rule, binding->items_name, &v) == RET_OK) {
    if (v.type == VALUE_TYPE_OBJECT) {
     tk_object_t* obj = value_object(&v);

      if (obj == TK_OBJECT(e->target)) {
        widget_t* widget = WIDGET(BINDING_RULE_WIDGET(rule));
        if (obj == TK_OBJECT(BINDING_RULE_VIEW_MODEL(rule)) && tk_object_is_collection(obj)) {
          table_client_set_rows(widget, tk_object_get_prop_int(obj, VIEW_MODEL_PROP_ITEMS, 0));
        } else {
          table_client_set_rows(widget, tk_object_get_prop_int(obj, TK_OBJECT_PROP_SIZE, 0));
        }
      }
    }
  }

  return RET_OK;
}

static ret_t table_client_on_prepare_row_mvvm(void* ctx, widget_t* client, uint32_t prepare_cnt) {
  binding_rule_t* rule = BINDING_RULE(ctx);
  items_binding_t* binding = ITEMS_BINDING(rule);

  if (binding->fixed_widget_count != prepare_cnt) {
    value_t v;
    binding_context_t* bctx = BINDING_RULE_CONTEXT(rule);

    if (binding_context_get_prop_by_rule(bctx, rule, binding->items_name, &v) == RET_OK) {
      if (v.type == VALUE_TYPE_OBJECT) {
       tk_object_t* obj = value_object(&v);

        if (obj == TK_OBJECT(BINDING_RULE_VIEW_MODEL(rule)) && tk_object_is_collection(obj)) {
          table_client_set_rows(client, tk_object_get_prop_int(obj, VIEW_MODEL_PROP_ITEMS, 0));
        } else {
          table_client_set_rows(client, tk_object_get_prop_int(obj, TK_OBJECT_PROP_SIZE, 0));
        }

        binding->fixed_widget_count = prepare_cnt;
        binding_context_notify_items_changed_sync(bctx, obj);
      }
    }
  }

  return RET_OK;
}

static ret_t table_client_bind(binding_context_t* ctx, binding_rule_t* rule) {
  if (binding_rule_is_items_binding(rule)) {
    emitter_t* emitter = EMITTER(ctx->view_model);
    items_binding_t* binding = ITEMS_BINDING(rule);
    widget_t* widget = BINDING_RULE_WIDGET(rule);
    return_value_if_fail(widget != NULL && ctx != NULL, RET_BAD_PARAMS);

    binding->fixed_widget_count = 0;
    table_client_set_on_prepare_row(widget, table_client_on_prepare_row_mvvm, rule);
    table_client_set_on_load_data(widget, table_client_on_load_data_mvvm, rule);
    emitter_on(emitter, EVT_ITEMS_CHANGED, table_client_on_items_changed, rule);
  }

  return RET_OK;
}

14.8.2 注册自定义 binder

ret_t table_client_custom_binder_register(void) {
  return custom_binder_register(WIDGET_TYPE_TABLE_CLIENT, table_client_bind);
}

将自定义 binder 注册到系统,AWTK-MVVM 框架才能根据自定义的 bind 函数实现相应的绑定规则。