前言
有這么一個業務,主界面點擊應用窗口進入聲納顯示界面,聲納顯示界面再通過按鈕進入菜單界面,菜單界面有很多關于該聲納顯示界面的設置項,比如量程,增益,時間顯示,亮度,對比度等等,大概十幾個設置。
有些數值類的設置還有子預覽菜單,在子預覽菜單里面通過滑條去設置數值,回到菜單后,設置會顯示子預覽菜單設置的數值。
聲納顯示界面需要顯示一些菜單的設置,比如量程,增益等等。
也就是大概這么一個頁面關系,其中后面三個頁面之間還有數據依賴的關系。

由于菜單的設置項非常多,用傳統的基于控件樹的方法寫起來代碼量很大,而且美工時常改動菜單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來索引到公共屬性的方法,只好放棄。