Skip to Content
Author's profile photo Hongyan Shao

CRM Web UI能实现动态变化一个字段的必输项Mandatory控制吗?

本文实例用的系统环境(SAP内部系统):

  • WEBCUIF             749
  • BBPCRM              715

本文的主要内容:

  1. 如何定义某字段为必输项;
  2. 分析了必输项控制在程序中的逻辑对应于界面上的表现 ,比如如何生成对应的html等;
  3. 介绍在目前的CRM Web UI框架下,动态变化必输项控制带来的界面上的错误表现形式;
  4. 尝试探讨一下在目前的CRM Web UI框架下,如何解决这个问题;

本文参考的文献:

 

玩转CRM Web UI的同学或许曾经实现过这样的需求,定义一个字段是否为必输项,总结而言有两个途径:

  1. 通过configuration,定义字段的属性值Mandatory打开,如下图
  2. 通过程序实现,在该字段的P getter中加入程序

那么如果我们来一个较高级版本的需求,这个字段的必输与否需要取决于另一个字段当前选择的值。让我们暂且编造一个需求,在创建个人客户(Individual Account)的时候,如果Academic Title 选择的是0002,在我这个测试系统中是Professor,那么除了必须提供Last Name也就是姓以外,还必须提供First Name也就是名。

承接上面的信息,再使用configuration的话,就意味着我需要对configuration的xml做动态处理,这也是可以实现的,但是对于这里的这个简单需求而言,花费的精力比较大,不推荐。那么我们决定选择用P Getter来实现,只把以上的第五步的程序稍作改动如下

原本以为大功告成了,结果发现页面是这么一个表现形式

下巴掉下来了,且慢,让我们来仔细分析一下,这是怎么回事儿。

当按下Enter,或者是一个roundtrip之后,针对一个必输项字段,如果值为空,会有三个表现

对照前面的视频可以发现我们的动态必输项字段的这三个表现是不一致的。选择好Academic Title的值之后第一次按下Enter表现1和表现2的a会出现或者消失,这个是我们想要的。但是表现2的b和表现3都必须要在第二次按下Enter才会正确出现。而且表现3也就是错误信息才是真正能够阻止保存的关键,这样的不一致会导致必须输入的字段没有输入就能保存,或者是在已经不必须输入的时候无法保存的现象,并不是我们想要的。

针对这个问题的原因,在KBA 2013392 里面有所阐述。在目前的CRM Web UI设计框架下,使用常规的P getter这个情况是无法避免的,KBA 2013392 也不得不给出一个是受标准所限的无奈结论。那么我们就穷途末路了吗?倒也不是,让我们就着这个KBA里面引用自开发的一段描述来一起研究一下这个功能的机理。同时看看是否存在其他的方法克服这个困难。

这里的DO_FINISH_INPUT方法指的是CL_BSP_WD_VIEW_CONTROLLER method DO_FINISH_INPUT,让我们先来观察一下这个方法中的程序

这里说的的Issue Error就是前面说到的表现3。这里的Set Error status就是表现2。顺便究一下这个Set Error status的后续机理。如下图,在CL_THTMLB_INPUTFIELD中根据前面DO_FINISH_INPUT中传递过来的error状态,程序赋予html元素th-onerror这个标志,然后对应css里面定义的颜色形成了前面说的表现2中的a和b。

在研究上面这个Error状态时,也顺便看到了同样程序稍后对于必输项的处理,如下在CL_THTMLB_INPUTFIELD中根据字段的required属性,赋予html元素th-ip-sp-md这个标志然后对应css里面定义的颜色形成了前面说的表现2中的a。至于红色星号,这个则属于Label的处理,在CL_THTMLB_LABEL里面很快找到了针对required必输项属性的处理,这里增加了星号,并且给了th-lb-txtreq这个标志,由此也决定了星号的颜色。

那么这个required属性又是从何而来呢?就是先前提到的定义必输项的两个途径。具体程序中的对应如下

图形化这整个流程(一次round trip)如下,可以看到,每次判定错误消息的依据是上一次的bsp的必输项状态,P getter在此之后才被调用,所以出现了不一致。

研究到这里的时候本人有一种打通任督二脉的感觉,但是且慢,最初的那个问题还丝毫没有被解决。:P

本人跟我们Web UI的开发商量过以上这个问题,开发已经把这个问题排入了计划,会研究如何改变上面这个已经定型的框架。但是貌似这不是一朝一夕能够完美解决的,所以让我们尝试在目前这个框架的基础上如何腾挪一下。

早在2013年有一篇博文,相同需求下在customer bsp component里面的处理方法。这里提出在enhance了标准component的情况下如何操作。

让我们来仔细看一下上面这张藏宝图,这里CL_BSP_WD_VIEW_CONTROLLER POST_MANDATORY_FIELD担任了增加必输项的责任,这个方法也是一个public方法,也就是说如果动态结果是增加了必输项的话,我们可以很方便的在DO_FINISH_INPUT里面GET_EMPTY_MANDATORY_FIELDS之前调用POST_MANDATORY_FIELD来增加需要的必输项。但是怎样能够减少必输项呢?观察CL_BSP_WD_VIEW_CONTROLLER 的属性MANDATORY_FIELDS是一个私有属性,也就是无法从外部直接操作。所以这个方向走不通。那么我们还有其他几种选择,一个是在redefine的method中完全复制标准的DO_FINISH_INPUT的逻辑,针对GET_EMPTY_MANDATORY_FIELDS的结果添加类似P-getter的逻辑改变需要增加error status和加上error message的字段,以达到目的。还有就是redefine方法GET_EMPTY_MANDATORY_FIELDS,直接改变这个方法的返回内容。这里我们尝试了后面那种方法。

感谢同学的帮助,程序写成了如下内容。里面关于判断title字段是否为0002和标准GET_EMPTY_MANDATORY_FIELDS是否已经正确返回的程序有必要抽出做一个独立的接口或者方法,便于未来的扩展。这里的检查该字段是否当前为空的逻辑拷贝自CL_BSP_WD_VIEW_CONTROLLER 方法GET_EMPTY_MANDATORY_FIELDS。

 

  METHOD get_empty_mandatory_fields.
    CALL METHOD super->get_empty_mandatory_fields
      RECEIVING
        rt_result = rt_result.

    DATA: lv_cnode_name              TYPE string,
          lv_last_cnode_name         TYPE string,
          lv_attr_path               TYPE string,
          lv_attr_name               TYPE string,
          lv_attr_value              TYPE string,
          lv_attr_meta               TYPE REF TO if_bsp_metadata,
          lv_exception               TYPE REF TO cx_bol_exception,
          lv_dynamic_mandatory_field TYPE bsp_dlc_binding_string.

    lv_dynamic_mandatory_field = '//HEADER/STRUCT.FIRSTNAME'.

*   split binding expression
    SPLIT lv_dynamic_mandatory_field+2(*) AT '/' INTO lv_cnode_name lv_attr_path.
    TRANSLATE lv_cnode_name TO LOWER CASE.                "#EC SYNTCHAR

    IF lv_cnode_name NE lv_last_cnode_name.
*     get cnode instance
      FIELD-SYMBOLS: <cnode> TYPE lbsp_model_item.
      READ TABLE me->m_models WITH KEY model_id = lv_cnode_name
                 ASSIGNING <cnode>.
    ENDIF.
    IF <cnode> IS ASSIGNED.
*     was the field input ready?
      DATA: lv_cnode     TYPE REF TO cl_bsp_wd_context_node,
            lv_component TYPE string,
            lv_type      TYPE i.
      TRY.
          TRY.
              lv_cnode ?= <cnode>-instance.

              DATA: lv_title    TYPE string.

              lv_title = <cnode>-instance->if_bsp_model_binding~get_attribute( attribute_path = 'STRUCT.TITLE_ACA1' ).

            CATCH cx_sy_move_cast_error.
*             unexpected context node type -> consider field to be input ready!
          ENDTRY.

          READ TABLE rt_result INTO DATA(ls_firstname_mandatory) WITH KEY table_line = lv_dynamic_mandatory_field.

          IF lv_title <> '0002' AND ls_firstname_mandatory IS NOT INITIAL.
            DELETE rt_result WHERE table_line = lv_dynamic_mandatory_field.
          ELSEIF lv_title = '0002' AND ls_firstname_mandatory IS INITIAL.
            TRY.
                lv_cnode->if_bsp_model_util~disassemble_path( EXPORTING path      = lv_attr_path
                                                              IMPORTING name      = lv_attr_name
                                                                        component = lv_component
                                                                        type      = lv_type ).
                DATA: lv_disabled TYPE string.
                CASE lv_type.
                  WHEN if_bsp_model_binding=>co_type_struct.
                    IF lv_attr_name = 'EXT'.
                      lv_disabled = lv_cnode->get_i_s_ext( component = lv_component ).
                    ELSE.
                      lv_disabled = lv_cnode->get_i_s_struct( component = lv_component ).
                    ENDIF.
                  WHEN if_bsp_model_binding=>co_type_simple.
                    lv_disabled = lv_cnode->get_i_s_struct( component = lv_attr_name ).
                ENDCASE.
                CHECK lv_disabled IS INITIAL OR lv_disabled(1) CS 'F'. "means 'f', 'F', 'false' or 'FALSE'

              CATCH cx_sy_move_cast_error.
*             unexpected context node type -> consider field to be input ready!
            ENDTRY.

              lv_attr_value = <cnode>-instance->if_bsp_model_binding~get_attribute( attribute_path = lv_attr_path ).
              IF lv_attr_value IS INITIAL.
*           mandatory field is still initial
                APPEND lv_dynamic_mandatory_field TO rt_result.
              ELSE.
*           string is not initial -> field might still be initial depending on type
                lv_attr_meta = <cnode>-instance->if_bsp_model_binding~get_attribute_metadata( attribute_path = lv_attr_path ).
                IF lv_attr_meta IS BOUND AND lv_attr_meta->get_abap_type( ) CA 'DTIPFNbs'.
*             field type is Date, Time, Integer, Packed, or Float -> number passed
                  IF lv_attr_value NA '123456789'.
*               mandatory field is still initial
                    APPEND lv_dynamic_mandatory_field TO rt_result.
                  ENDIF.
                ENDIF.
              ENDIF.
          ENDIF.
        CATCH cx_bol_exception INTO lv_exception.
          IF lv_exception->textid = cx_bol_exception=>entity_already_freed.
            DATA: lv_pattern TYPE string.
            CONCATENATE '//' lv_cnode_name '/*' INTO lv_pattern.
*            delete LT_MANDATORY_FIELDS where TABLE_LINE cp LV_PATTERN.
          ELSE.
            RAISE EXCEPTION lv_exception.
          ENDIF.
      ENDTRY.

    ENDIF.

    lv_last_cnode_name = lv_cnode_name.

  ENDMETHOD.

再次声明,针对这个问题,我们的开发同学已经把它放到了待处理事项中,我们指日可待标准框架里面能解决这个问题。在解决之前,可以考虑以上的临时方案,本人没有在不同的component里面测试,如果您发现上述方法有任何问题,欢迎补充,谢谢。

 

更新于 2018 Jun 22nd

我们的开发经过多重讨论,建议客户把这个需求提交到即将开始的 Customer Connection project https://influence.sap.com/CRM2019  (开始于2018年8月23日 到2018年12月3日) 。然后等待其他客户的投票,依据投票排名,我们的开发团队会研究可行性然后安排实现。让我们去投票让它进入标准系统吧。

Assigned Tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo William Wang
      William Wang

      写的很细致,谢谢作者