# 3. MVVM-C案例分析

上一章讲解了如何制作 MVVM-C 项目,为了加深大家对 MVVM-C 的理解,本章将以 Mvvm-C-Demo 为例进行 MVVM-C 项目的案例分析,该案例主要功能如下:

  • 显示多个设备。
  • 可以增加和删除设备。
  • 解锁后可以修改设备类型和设备参数。
  • 可以还原和清空设备列表。
  • 不同设备类型显示不同数据,下图为案例项目中的设备参数表:
图3.1 案例项目中的设备参数表
图3.1 案例项目中的设备参数表

界面及功能如图所示:

图3.2 案例项目界面
图3.2 案例项目界面

# 3.1 模型设计

首先使用 AWTK Designer 新建一个MVVM-C 项目工程,步骤详见上文 2.1 章节,然后判断该项目是否需要增加模型,根据上文案例项目的需要的功能,显然是需要增加一个设备模型,此处将该模型名为"device",该模型包含设备类型、设备参数和数据,具体详见下表:

属性名称 类型 描述 备注
pack_type device_type_t 设备类型 自定义枚举类型,影响设备数据
pack_params device_params_t 设备参数 自定义枚举类型
io1 bool_t 设备数据 随机生成,由设备类型决定是否显示
io1 bool_t 设备数据 随机生成,由设备类型决定是否显示
a1 int32_t 设备数据 随机生成,由设备类型决定是否显示
a2 int32_t 设备数据 随机生成,由设备类型决定是否显示
temp double 设备数据 随机生成,由设备类型决定是否显示
tps double 设备数据 随机生成,由设备类型决定是否显示

本案例中"device"模型的 pack_type 属性和 pack_params 属性均为自定义的枚举类型,实现方式详见下一小节,其中 pack_type 属性将决定该设备显示哪些数据,为了方便演示效果,这些设备数据均是随机生成的。

# 3.1.1 增加自定义枚举类型

为了代码拥有更好的可读性,可以将设备类型和设备参数自定义为一种枚举类型,打开项目目录下“src/models/model_types_def.h”,在该文件中新增自定义枚举类型(注意:注释不可删除并且需要按照格式写,否则在 AWTK Designer 下无法找到该枚举类型)。

设备类型:

  • PACK_SKP_1000
  • PACK_SKP_2000
  • PACK_SKP_3042
  • PACK_SKP_3132
  • PACK_SKP_3142
  • PACK_SKP_5002

在代码处增加设备类型的枚举类型:

/* src/models/model_types_def.h */

/**
 * @enum device_type_t
 * @prefix DEV_TYPE_
*/
typedef enum _device_type_t {
  /**
   * @const DEV_TYPE_PACK_SKP_1000
   */
  DEV_TYPE_PACK_SKP_1000 = 0,
  /**
   * @const DEV_TYPE_PACK_SKP_2000
   */
  DEV_TYPE_PACK_SKP_2000,
  /**
   * @const DEV_TYPE_PACK_SKP_3042
   */
  DEV_TYPE_PACK_SKP_3042,
  /**
   * @const DEV_TYPE_PACK_SKP_3132
   */
  DEV_TYPE_PACK_SKP_3132,
  /**
   * @const DEV_TYPE_PACK_SKP_3142
   */
  DEV_TYPE_PACK_SKP_3142,
  /**
   * @const DEV_TYPE_PACK_SKP_5002
   */
  DEV_TYPE_PACK_SKP_5002,

  DEV_TYPE_MAX_COUNT
} device_type_t;

设备参数类型:

  • NONE
  • NTC_2252K
  • NTC_5K
  • NTC_10K

在代码处增加设备参数类型的枚举类型:

/* src/models/model_types_def.h */

/**
 * @enum device_params_t
 * @prefix DEV_PARAMS_
*/
typedef enum _device_params_t {
  /**
   * @const DEV_PARAMS_NONE
   */
  DEV_PARAMS_NONE = 0,
  /**
   * @const DEV_PARAMS_NTC_2252K
   */
  DEV_PARAMS_NTC_2252K,
  /**
   * @const DEV_PARAMS_NTC_5K
   */
  DEV_PARAMS_NTC_5K,
  /**
   * @const DEV_PARAMS_NTC_10K
   */
  DEV_PARAMS_NTC_10K,

  DEV_PARAMS_MAX_COUNT
} device_params_t;

# 3.1.2 新增设备模型

最后按照案例的需求,新增“设备”模型:device,步骤详见上文 2.3.1 章节。

图3.3 新增设备模型
图3.3 新增设备模型

在 AWTK Designer 新增 device 模型保存后,会自动在项目目录下“src/models”生成 device 模型的相关代码。

# 3.2 视图模型设计

接下来是为主界面 home_page 新建一个视图模型 home_page_view_model,步骤详见上文 2.2.1 章节,在 AWTK Designer 新增视图模型 home_page_view_model 保存后,会自动在项目目录下“src/view_models”生成 home_page_view_model 的相关代码。

# 3.2.1 为视图模型增加属性

根据案例项目的需要的功能,为视图模型增加下图属性:

图3.4 为视图模型增加属性
图3.4 为视图模型增加属性

items 对象数组和 current_index 属性在实现命令功能时就会使用,而 unlocked 属性会在讲解界面设计时才会使用。

# 3.2.2 为视图模型增加命令

根据案例项目的需要的功能,为视图模型增加下图命令:

图3.5 为视图模型增加命令
图3.5 为视图模型增加命令

最后需要跳转到命令代码,实现命令功能函数和判断命令是否可执行的函数。

# 3.2.3 实现设置当前设备序号功能

该功能是用于设置设备插入或删除序号,是实现插入设备功能和删除设备功能的前提功能,通过执行 setCurrent 命令,更改 current_index 属性的值。

/* src/view_models/home_page_view_model_impl.inc */

static bool_t home_page_view_model_can_setCurrent(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  (void)args;
  return_value_if_fail(model != NULL, FALSE);

  return TRUE;
}

ret_t home_page_view_model_setCurrent(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  tk_object_t* a = object_default_create();
  (void)args;
  return_value_if_fail(model != NULL && a != NULL, RET_BAD_PARAMS);

  /* 将字符串类型的命令参数转换为 object 对象 */
  tk_command_arguments_to_object(args, a);
  /* 更新当前选中设备序号 */
  model->current_index = tk_object_get_prop_int32(a, "index", -1);
  TK_OBJECT_UNREF(a);
  return RET_OBJECT_CHANGED;
}

# 3.2.4 实现插入设备功能

该功能是将新增设备插入到设备列表里当前选中设备序号处,通过执行 insert 命令,将新建的 device 设备对象,插入到 items 对象数组里的 current_index (该值通过设置当前设备序号功能修改) 处。

/* src/view_models/home_page_view_model_impl.inc */

static bool_t home_page_view_model_can_insert(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  object_array_t* items = NULL;
  (void)args;
  return_value_if_fail(model != NULL, FALSE);

  items = OBJECT_ARRAY(model->items);
  return_value_if_fail(items != NULL, FALSE);

  /* 当前选中设备序号合法时,才可以使用 insert 命令 */
  return model->current_index >= 0 && model->current_index < items->size;
}

ret_t home_page_view_model_insert(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  value_t v;
  tk_object_t* device = NULL;
  ret_t ret = RET_OK;
  (void)args;
  return_value_if_fail(model != NULL && model->items != NULL, RET_BAD_PARAMS);

  /* 创建设备并插入到设备列表里当前选中设备序号处 */
  device = device_create();
  return_value_if_fail(device != NULL, RET_FAIL);

  value_set_object(&v, device);
  emitter_on(EMITTER(device), EVT_PROP_CHANGED, emitter_forward, model);
  ret = object_array_insert(model->items, model->current_index, &v);
  TK_OBJECT_UNREF(device);

  return ret;
}

# 3.2.5 实现移除设备功能

该功能是将设备列表里当前选中设备序号处的设备移除,通过执行 remove 命令,将 items 对象数组里的 current_index (该值通过设置当前设备序号功能修改) 处 device 设备对象移除。

/* src/view_models/home_page_view_model_impl.inc */

static bool_t home_page_view_model_can_remove(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  object_array_t* items = NULL;
  (void)args;
  return_value_if_fail(model != NULL, FALSE);

  items = OBJECT_ARRAY(model->items);
  return_value_if_fail(items != NULL, FALSE);

  /* 当前选中设备序号合法时,才可以使用 remove 命令 */
  return model->current_index >= 0 && model->current_index < items->size;
}

ret_t home_page_view_model_remove(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  (void)args;
  return_value_if_fail(model != NULL && model->items != NULL, RET_BAD_PARAMS);

  /* 删除设备列表里当前选中设备序号处的设备 */
  return object_array_remove(model->items, model->current_index);
}

# 3.2.6 实现清除设备列表功能

该功能是将设备列表里的所有设备,通过执行 clear 命令,将 items 对象数组里的所有 device 设备对象移除。

/* src/view_models/home_page_view_model_impl.inc */

static bool_t home_page_view_model_can_clear(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  object_array_t* items = NULL;
  (void)args;
  return_value_if_fail(model != NULL, FALSE);

  items = OBJECT_ARRAY(model->items);
  return_value_if_fail(items != NULL, FALSE);

  /* 设备列表不为空时,才可以使用 clear 命令 */
  return items->size > 0;
}

ret_t home_page_view_model_clear(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  return_value_if_fail(model != NULL && model->items != NULL, RET_BAD_PARAMS);
  (void)args;

  /* 清空设备列表 */
  return object_array_clear_props(model->items);
}

# 3.2.7 实现重置设备列表功能

该功能是将重置设备列表,为了方便演示效果,该功能的实现是插入50个属性随机的设备对象到设备列表中,通过执行 reset 命令,在 items 对象数组里增加50个属性随机的 device 设备对象。

/* src/view_models/home_page_view_model_impl.inc */

static bool_t home_page_view_model_can_reset(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  object_array_t* items = NULL;
  return_value_if_fail(model != NULL, FALSE);

  items = OBJECT_ARRAY(model->items);
  return_value_if_fail(items != NULL, FALSE);

  /* 设备列表为空时,才可以使用 reset 命令 */
  return items->size == 0;
}

ret_t home_page_view_model_reset(tk_object_t* obj, const char* args) {
  home_page_view_model_t* model = HOME_PAGE_VIEW_MODEL(obj);
  ret_t ret = RET_OK;
  uint32_t i = 0;
  (void)args;
  return_value_if_fail(model != NULL && model->items != NULL, RET_BAD_PARAMS);

  /* 插入50个属性随机的设备对象到设备列表中 */
  for (i = 0; i < 50 && ret == RET_OK; i++) {
    tk_object_t* device = device_create();
    if (device != NULL) {
      value_t v;
      tk_object_set_prop_int(device, "pack_type", random() % DEV_TYPE_MAX_COUNT);
      tk_object_set_prop_int(device, "pack_params", random() % DEV_PARAMS_MAX_COUNT);
      tk_object_set_prop_bool(device, "io1", random() % 2 == 0);
      tk_object_set_prop_bool(device, "io2", random() % 2 == 0);
      tk_object_set_prop_double(device, "temp", random() / 10.0);
      tk_object_set_prop_int32(device, "a1", random());
      tk_object_set_prop_int32(device, "a2", random());
      tk_object_set_prop_double(device, "tps", random() / 10.0);

      value_set_object(&v, device);
      object_array_push(model->items, &v);
      emitter_on(EMITTER(device), EVT_PROP_CHANGED, emitter_forward, model);
      TK_OBJECT_UNREF(device);
    } else {
      ret = RET_FAIL;
    }
  }

  return ret;
}

# 3.3 界面设计

最后是界面设计,在案例项目里需要显示多个设备,使用列表视图是一个不错的选择,按照《AWTK-Designer快速使用指南》进行界面设计,可以得到设备列表界面的雏形:

图3.6 设备列表界面
图3.6 设备列表界面

# 3.3.1 列表渲染

列表渲染可将指定控件作为模板,将列表中的各项数据进行重复渲染。下一步将视图模型 home_page_view_model 中的 items 设备列表数组绑定到 list_item 列表项控件的列表渲染规则中:

步骤一:点击下图框选位置的按钮:

图3.7 为列表项控件设置列表渲染规则步骤一
图3.7 为列表项控件设置列表渲染规则步骤一

步骤二:在"绑定的数组"中点击选择 items 设备列表数组(上文在视图模型中添加的属性),或者点击右侧按钮进行更详细地设定,之后按确定完成绑定:

图3.8 为列表项控件设置列表渲染规则步骤二
图3.8 为列表项控件设置列表渲染规则步骤二

列表渲染设置完成后,在 list_item 控件对象中点击的下图红色处就会有个小图标显示:

图3.9 为列表项控件设置列表渲染规则完成
图3.9 为列表项控件设置列表渲染规则完成

接下来在 list_item 控件及其子控件中可以通过 index 变量名访问当前项下标,item 变量名访问当前项的数组元素,在该案例项目里其元素就是 device 模型对象。

注意:list_item 是界面中的列表项控件;items 是在视图模型中添加的object_array类型属性,意为设备列表数组,用于存取设备;item 为条件渲染规则中用于获取绑定变量数组当前项元素的属性,注意不要混淆。

下一步给 list_item 设置子控件布局,在下文会有作用:

图3.10 设置子控件布局
图3.10 设置子控件布局

下一步给 list_item 增加子控件,通过数据绑定(步骤详见上文 2.2.3 章节),就可以在子控件显示列表当前项下标和设备信息了。

label 控件 的 text 属性绑定 index 变量用于显示列表当前项下标:

图3.11 绑定当前项下标
图3.11 绑定当前项下标

combo box 控件 的 value 属性绑定 item.pack_type 变量用于给用户选择当前项设备的设备类型(item.pack_type 表示 device 模型对象的 pack_type 属性):

图3.12 绑定设备模型属性
图3.12 绑定设备模型属性

按照以上方法将设备固定部分的信息绑定绑定完成后,效果如下图:

图3.13 在列表项中显示设备固定部分的信息
图3.13 在列表项中显示设备固定部分的信息

还有其他设备数据会根据设备类型的不同,所显示的内容也不同,这时需要用到条件渲染。

# 3.3.2 条件渲染

条件渲染可以根据不同的条件进行不同的渲染。下一步使用条件渲染实现不同设备类型显示不同数据的功能点:

图3.14 不同设备类型显示不同数据
图3.14 不同设备类型显示不同数据

要想实现上图的效果,首先设计用于显示设备数据的控件组合,并用容器 view 将其归类:

图3.15 显示设备数据的控件组合
图3.15 显示设备数据的控件组合

将这四个容器放到 list_item 中,在 list_item 子控件布局的作用下,会发现这四个容器会超出 list_item 的显示范围:

图3.16 显示设备数据的控件组合
图3.16 显示设备数据的控件组合

下一步给这四个容器设置条件渲染规则,运行时只有符合条件的那个容器才会被创建,从而达到不同条件下显示不同内容的效果。

步骤一:选择第一个容器,点击框选按钮:

图3.17 设置条件渲染步骤一
图3.17 设置条件渲染步骤一

步骤二:在弹出对话框里点击框选处:

图3.18 设置条件渲染步骤二
图3.18 设置条件渲染步骤二

步骤三:在弹出对话框中设置渲染条件,图中框选条件意思为当前设备类型为0(DEV_TYPE_PACK_SKP_1000),这渲染该控件,设置完成后点击确定:

图3.19 设置条件渲染步骤三
图3.19 设置条件渲染步骤三
图3.20 设置第一个容器条件渲染规则完成
图3.20 设置第一个容器条件渲染规则完成

步骤四:接着设置下一个容器,和步骤二、步骤三的操作相同:

图3.21 设置条件渲染步骤四
图3.21 设置条件渲染步骤四

步骤五:到最后一个容器,就可以选择 else 判断,在前三个容器都不符合条件时,就会渲染该容器,之后点击确定按钮保存渲染规则:

图3.22 设置条件渲染步骤五
图3.22 设置条件渲染步骤五

设置完成后,就会出现图中框选效果:

图3.23 设置条件渲染完成
图3.23 设置条件渲染完成

之后为容器中控件绑定当前项设备的设备属性即可,如将设备中的 io2 属性绑定到 check button 控件的 value 属性上:

图3.24 数据绑定
图3.24 数据绑定

如将设备中的 temp 属性绑定到 label 控件的 text 属性上,并对显示内容格式化:

图3.25 数据绑定
图3.25 数据绑定

目前为止,已经实现了显示多个设备和不同设备类型显示不同数据的功能点,下一步实现解锁后才可以修改设备类型和设备参数这一功能:

增加 check button 控件,将 unlocked 属性绑定到 check button 控件的 value 属性:

图3.26 数据绑定
图3.26 数据绑定

再将 unlocked 属性绑定到用于选择设备类型的 combo box 的 enable 属性,就可以实现通过 check button 控制设备类型能否被修改(控制设备参数能否被修改和该操作类似,这里就不赘述了):

图3.27 数据绑定
图3.27 数据绑定

下一步实现设备的增加和删除功能,首先增加两个按钮,一个用于增加并插入设备,一个用于删除选中设备,绑定在视图模型设计好的 insert 命令和 remove 命令到对应的按钮的点击事件上,以绑定 insert 命令为例:

图3.28 命令绑定
图3.28 命令绑定

还需要将 setCurrent 命令绑定到 list_item 的点击事件上,实现通过点击 list_item 选中设备插入或删除序号(这里的参数是以fscript表达式形式的参数序列传入的,其意思是给名称为"index"参数赋值为列表当前项下标(index),想了解更多请参阅 AWTK-MVVM 命令绑定 (opens new window)):

图3.29 命令绑定
图3.29 命令绑定

最后一步实现还原和清空设备列表功能:在界面处增加两个按钮,一个用于还原设备列表,一个用于清空设备列表,绑定在视图模型设计好的 reset 命令和 clear 命令到对应的按钮的点击事件上即可,以绑定 clear 为例:

图3.30 命令绑定
图3.30 命令绑定

到这一步,案例项目的所有功能就完成了。

# 3.4 运行效果

最后将项目打包并编译,点击模拟运行,即可看到其运行效果:

图3.31 操作步骤
图3.31 操作步骤
图3.32 运行案例项目
图3.32 运行案例项目