前言
有这么一个业务,主界面点击应用窗口进入声纳显示界面,声纳显示界面再通过按钮进入菜单界面,菜单界面有很多关于该声纳显示界面的设置项,比如量程,增益,时间显示,亮度,对比度等等,大概十几个设置。
有些数值类的设置还有子预览菜单,在子预览菜单里面通过滑条去设置数值,回到菜单后,设置会显示子预览菜单设置的数值。
声纳显示界面需要显示一些菜单的设置,比如量程,增益等等。
也就是大概这么一个页面关系,其中后面三个页面之间还有数据依赖的关系。

由于菜单的设置项非常多,用传统的基于控件树的方法写起来代码量很大,而且美工时常改动菜单UI,容易影响界面代码,我当时自然而然的选择了AWTK特有的MVVM框架来完成菜单设置的显示逻辑。
一开始由于赶项目时间,我就直接在界面上使用了mvvm的app_conf功能,app_conf在AWTK-MVVM还有专门的model,使得不用写代码就能完成配置文件与多个界面间的数据联动与设置保存,十分贴心。
虽然这种方法能够快速实现功能,但是后期维护性极差,因为业务的配置key都写死在xml中,跟xml耦合。
比如这样的一个设置项:
<window v-model="m_home" name="filter_color"><view name="v_setting_color" x="175" y="0" w="387" h="183"><label name="title" x="9" y="10" w="58" h="28" text="Color"/><view name="view" x="34" y="58" w="359" h="112" children_layout="default(c=3,r=3)"><radio_button name="radio_button" v-data:value="{A.color==1}" text="color1"/><radio_button name="radio_button" v-data:value="{A.color==2}" text="color2"/><radio_button name="radio_button" v-data:value="{A.color==3}" text="color3"/><radio_button name="radio_button" v-data:value="{A.color==4}" text="color4"/><radio_button name="radio_button" v-data:value="{A.color==5}" text="color5"/><radio_button name="radio_button" v-data:value="{A.color==6}" text="color6"/><radio_button name="radio_button" v-data:value="{A.color==7}" text="color7"/><radio_button name="radio_button" v-data:value="{A.color==8}" text="color8"/></view></view>
</window>
实际业务是三种不同的声纳模式的设置菜单A, B, C, 三个设置菜单每个界面都有十几个行十几个设置项,加起来就是三十几个设置项,而且这些设置项的UI大都重复。
每个声纳的大部分属性还是各自独立的,也就是不同菜单的同一个设置,上面的color,A,B,C菜单都在用,A用A.color, B就是B.color,同样的key,父路径不同,这就导致没法用AWTK的component机制将这些设置项UI抽象出来复用,变成这样:
<window v-model="m_home" name="filter_color"><?include filename="comp_setting_color.xml" ?>
</window>
(话说AWTK的这个组件机制真的鸡肋,就是单纯的include替换,连个內部slot, 组件通信也没有)
如果用上述的绑死app_conf key的方式来做,后面一旦加了什么影响UI的新功能或者菜单风格更改,又要一个个菜单的去照着美工原型图去改,十分痛苦。
而且app_conf模型自身的命令绑定十分有限,稍微复杂一点的需求(比如点击按钮发送MQTT)就做不了,还是要结合自定义model的命令绑定或者传统的基于控件名索引的widget_on, 自己写函数
等到项目周期开始放缓,我决心把之前写死了配置key的几个菜单页面给重构了,改成用自定义model的自定义的属性来做数据绑定,在代码层面实现具体的选择菜单A,B,C的逻辑。
重构跟本文的逻辑不大,就不展开了,我在这边引出这些,是因为之前直接用app_conf有一个优点,就是不用关心页面之间数据联动的问题,awtk-mvvm内部代码自己会处理好,如果用自定义model, 就要写代码理清窗口导航的数据流通关系了,确保窗口退出时返回正确的设置数据给上一个页面。
实践
回到这个界面关系中,由于显示界面,设置菜单,子菜单都指向同一个对象的数据,考虑到三个页面后期可能的变动,我索性让三个页面都使用同一个model了。
我的目标是,弄清楚三个页面使用同一个model之后窗口的传参如何处理,才能实现子菜单设置时能够返回保存的数据。
抽象的例子如下,所有界面绑定一个叫m_home的model。
sonar_page有bottom_lock,noise_limiter,pic_advance三个界面,每个界面都是在独立的子菜单中设置,设置完的结果会在sonar_page上显示。
<window v-model="m_home" name="sonar_page"><button name="button" x="272" y="320" w="100" h="36" v-on:click="{mreturn}" text="Back"/><label name="value" x="272" y="49" w="160" h="28" v-data:text="{value_int}"/><label name="key" x="272" y="104" w="160" h="28" v-data:text="{noise_limiter}" text="setting_item"/><label name="key" x="272" y="178" w="160" h="28" v-data:text="{pic_advance}" text="setting_item"/><button name="button1" x="101" y="49" w="100" h="36" text="Button" v-on:click="{home_navigate, Args=string?page_name=bottom_lock}"/><button name="button1" x="101" y="104" w="100" h="36" text="Button" v-on:click="{home_navigate, Args=string?page_name=noise_limiter}"/><button name="button1" x="101" y="170" w="100" h="36" text="Button" v-on:click="{home_navigate, Args=string?page_name=pic_advance}"/>
</window>
一开始我犯了个错误,感觉一个页面重复建相同的Model,旧Model还要拷贝数据到新Model, 开销比较大,就把Model的创建和销毁搞成了引用计数的模式:
m_home_t *last_page_model = NULL;
m_home_t *g_home_ref = NULL;
static int g_home_ref_count = 0;m_home_t* m_home_create(navigator_request_t* req)
{m_home_t *home = NULL;if(!g_home_ref){home = TKMEM_ZALLOC(m_home_t);str_init(&home->pic_advance, 32);}else{home = g_home_ref;}g_home_ref = home;g_home_ref_count++;home->req = req;printf("m_home=%#x created, value: %d ref_count: %d\r\n", home, home->value_int, g_home_ref_count);return home;
}ret_t m_home_on_return(navigator_request_t* req, const value_t* result)
{m_home_t *home = g_home_ref;printf("set last model %p\r\n", home);emitter_dispatch_simple_event(EMITTER(home), EVT_PROPS_CHANGED);return RET_OK;
}ret_t m_home_mreturn(m_home_t *home)
{value_t v;navigator_request_on_result(home->req, &v);navigator_back();return RET_OK;
}ret_t m_home_destroy(m_home_t* home)
{g_home_ref_count--;if(g_home_ref_count == 0){str_reset(&home->pic_advance);TKMEM_FREE(home);g_home_ref = NULL;}printf("m_home=%#x destroyed\r\n", home);return RET_OK;
}
但是后面发现子菜单上设置的值返回后无法在sonar_page上显示,查了半天,才发现m_home_on_return设置的其实是只跟当前界面有关的view_model,一旦返回这个页面就销毁了,根本影响不到上一个页面的view model。
只好老实了,乖乖用默认的一个view一个model的传统构建方法,导航到新页面时把旧model作为参数, 传给新model,新model拷贝旧model的参数,退出页面时,旧model从新model加载数据。
实际业务是进页面从app_conf load数据,退页面save 数据到app_conf,然后旧model再从app_conf save数据的,这个例子里面省略了,直接copy对象来表示。
#include "m_home.h"
#include "awtk.h"
#include "mvvm/mvvm.h"
#include "mvvm/base/utils.h"m_home_t *last_page_model = NULL;
m_home_t *g_current_home_ref = NULL;void m_home_data_copy(m_home_t *ahome, m_home_t *bhome)
{ahome->value_int = bhome->value_int;str_set(&ahome->pic_advance, bhome->pic_advance.str);ahome->noise_limiter = bhome->noise_limiter;
}m_home_t* m_home_create(navigator_request_t* req)
{m_home_t *home = NULL;home = TKMEM_ZALLOC(m_home_t);str_init(&home->pic_advance, 32);m_home_t *last_model = tk_object_get_prop_pointer(TK_OBJECT(req), "last_model");if(last_model != NULL){m_home_data_copy(home, last_model);}g_current_home_ref = home;home->req = req;printf("m_home=%p created, value: %d last_model: %p\r\n", home, home->value_int, last_model);return home;
}ret_t m_home_destroy(m_home_t* home)
{str_reset(&home->pic_advance);TKMEM_FREE(home);g_current_home_ref = NULL;printf("m_home=%#x destroyed\r\n", home);return RET_OK;
}ret_t m_home_on_return(navigator_request_t* req, const value_t* result)
{m_home_t *home = g_current_home_ref;m_home_t *last_model = tk_object_get_prop_pointer(TK_OBJECT(req), "last_model");printf("set last model %p\r\n", last_model);if(last_model != NULL){m_home_data_copy(last_model, home);emitter_dispatch_simple_event(EMITTER(last_model), EVT_PROPS_CHANGED);}return RET_OK;
}ret_t m_home_mreturn(m_home_t *home)
{value_t v;g_current_home_ref = home;navigator_request_on_result(home->req, &v);navigator_back();return RET_OK;
}ret_t m_home_to_navigate(m_home_t *home, const char *args)
{ tk_object_t *obj = object_default_create();tk_command_arguments_to_object(args, obj);const char *page_name = tk_object_get_prop_str(obj, "page_name");navigator_request_t* req = navigator_request_create(page_name, m_home_on_return);tk_object_set_prop_pointer(TK_OBJECT(req), "last_model", home);navigator_to_ex(req);tk_object_unref(TK_OBJECT(req));return RET_OK;
}ret_t m_home_set_prop_int(m_home_t *home, const char *args)
{tk_object_t *obj = object_default_create();tk_command_arguments_to_object(args, obj);const char *key = tk_object_get_prop_str(obj, "key");int32_t value = tk_object_get_prop_int(obj, "value", 0);if(tk_str_eq(key, "noise_limiter")){home->noise_limiter = value;}else{home->value_int = value;}printf("m_home_set_prop_int: %s = %d\r\n", key, value);TK_OBJECT_UNREF(obj);return RET_OBJECT_CHANGED;
}ret_t m_home_set_prop_str(m_home_t *home, const char *args)
{tk_object_t *obj = object_default_create();tk_command_arguments_to_object(args, obj);const char *key = tk_object_get_prop_str(obj, "key");const char *value = tk_object_get_prop_str(obj, "value");if(tk_str_eq(key, "pic_advance")){str_set(&home->pic_advance, value);printf("pic_advance set %s\r\n", home->pic_advance.str);}printf("m_home_set_prop_str: %s = %s\r\n", key, value);TK_OBJECT_UNREF(obj);return RET_OBJECT_CHANGED;
}
逻辑展示如下:
懒得展开了,放上代码:
https://gitee.com/tracker647/awtk-practice/tree/master/awtk_mvvm_shared_model_return_test
效果:
附录:关于sub_view_model
虽然app_conf有一个sub_view_model的功能可以缓解不同object有一样的配置key的问题,但是实际业务里配置既有私有配置也有共通配置,共通配置还是混杂在私有配置里面的,配置文件的结构是这样:
shared_conf:{range_mode:1range_val:10
};
A:{color:1
}
B:{color:2
}
C:{color:3
}
设置项的位置表现上,是这种情况:
私有属性
共有属性
私有属性
共有属性
如果使用sub_view_model,就要另外给相关的配置包上带sub_view_model属性的view标签。
上面的例子就要包两次sub_view_model标签,对于之前业务那种设置项多的情况就是会建立很多个冗余的只用于限定设置作用域的model, 程序上十分不优雅且有不稳定的风险,我找了一圈AWTK库,没有找到在sub_view_model的标签作用域里引用父级model来索引到公共属性的方法,只好放弃。