在前面的介绍中,数据绑定都是以 text 属性和 value 属性为例的。实际上,数据绑定可以作用于控件的任意属性。比如,可以绑定 enable 属性来决定控件是否可用,绑定 visible 属性来决定控件是否可见。在下面的几个例子中,我们来看看几种常见属性的绑定方法和应用场景。
在有的情况下,我们希望界面上某些控件隐藏起来,在另外一种情况下,我们又希望界面上这些控件能显示出来,这可以通过绑定 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
AWTK-MVVM 非常擅长处理数据联动。数据联动是指一个控件的数据变化,会引起其它相关控件的变化。
填写收货地址就是一个典型的数据联动的例子:
-
选择"省/直辖市"时,"城市"要跟着变化,"区县"也要跟着变化,完整地址也要跟着变化。
-
选择"城市"时,"区县"要跟着变化,完整地址也要跟着变化。
-
选择"区县"时,完整地址要跟着变化。
-
输入"详细地址"时,完整地址要跟着变化。
如果采用传统方式来开发,不但要写一大堆代码去处理事件,更重要的是这些代码与界面耦合到一起,难以维护和自动测试。
现在我们来看看 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
我们在本章的开头,展示了用 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
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
除了 fscript 本身提供的函数外,MVVM 增加了下面的函数:
widget 相关函数请参考:https://github.com/zlgopen/awtk/blob/master/docs/fscript_widget.md
打开新窗口
- 原型
navigator_to(target) => void
- target 新窗口名称
- 返回值:无
打开新窗口,并关闭当前窗口。
- 原型
navigator_replace(target) => void
- target 新窗口名称
- 返回值:无
关闭指定窗口。
- 原型
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 && 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>
由于表达式中<>"等字符对于 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 中常用的转义字符如下:
- 空格" ":
- 小于号"<":<
- 大于号">":>
- 与符号"&":&
- 双引号""":"
- 单引号"'":'
Windows 的命令行下,读者可以运行 demo18 来查看实际的效果。
bin\jsdemo18
在项目中,经常会遇到动态加载不同内容的场景,比如显示动态变化的数据集合,或者根据不同的条件加载不同的内容。
MVVM 提供了两种方式来实现上述场景:
-
列表渲染:v-for;
-
条件渲染:v-if、v-elif、v-else。
具体用法详见下文。
动态渲染过程中,控件只有在满足显示条件时才会创建。
由于 MVVM 绑定时需遍历 XML 上控件的所有属性,以查找控件的动态渲染规则。当界面复杂时,效率较低。此时可通过定义宏 MVVM_FAST_UI_LOAD 提高效率。启用该宏时有一点需注意,就是动态渲染规则须为控件在 XML 上的第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"的效果。
下面看一个图书列表的示例。
- 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}"/>
显示效果如下:
绑定列表视图的 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
使用 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
条件渲染可以根据不同的条件进行不同的渲染。
条件渲染规则也是一个控件属性:
-
属性的名称可以是 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 && 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 && 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 && 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
在温度控制器中,当温度高于某个阀值时,打开蜂鸣器发出警报,此时蜂鸣器也是和用户交互的接口,是视图的一部分。但是蜂鸣器却不是一个 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。
将输出类型的外设包装成 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);
}
输入类型的外设的实现稍微复杂一点,主要原因就是开启一个定时器,在定时器中去读取当前的状态。下面我们温度传感器为例,看看如何用软件实现一个温度传感器,该温度传感器随机生成温度值。
温度传感器有以下温度和采用间隔时间两个属性:
/**
* @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;
}
工厂是一种依赖注入常见的方法。设备和 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
有时我们需要访问多个 view model,此时可以把这些 view model 组合起来。
组合 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
-
如果出现同名的属性和命令,优先使用前面的 view model。所以当用到 app_conf 时,要放到最后,因为 app_conf 任何属性都是支持的。
-
不支持 view modal array。
完整例子请参考:demo27
有时我们需要按树形结构组织多个 view model,访问数据时,先从当前的 view model 去找,如果找不到,就去上一级 view model 找,直到找到数据或到达顶层为止。
比如下面这个例子:
<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
有时需要在多个窗口之间共享一个 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;
}
在一些特殊情况下,awtk-mvvm 缺省的方法不支持特殊的绑定规则,此时可以自定义一个 binder 并将其注册到 awtk-mvvm 中。
比如,自定义控件 table_view 有点特殊,列表项无需事先创建,并且数据无需全部加载到内存,awtk-mvvm 提供的列表渲染 (v-for) 方法不支持这种特殊用法,我们可以自定义一个 binder 实现该功能。
自定义控件 table_view 的源码以及使用方法请参考:awtk-widget-table-view
在本示例中,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;
}
ret_t table_client_custom_binder_register(void) {
return custom_binder_register(WIDGET_TYPE_TABLE_CLIENT, table_client_bind);
}
将自定义 binder 注册到系统,AWTK-MVVM 框架才能根据自定义的 bind 函数实现相应的绑定规则。