Grails+Ext之form scaffolding

本文介绍了一种基于Grails的scaffolding理念为ExtJS自动生成表单的方法。通过从领域模型中提取字段信息,并根据约束顺序排列,实现表单的自动化构建。此方法不仅简化了开发流程,还确保了表单与模型的一致性。

 

 

以上图片中间部分是个自动生成的form,不用专门给它写代码或用“表单设计器”工具手工生成代码。这种方法绝非rocket science,只是scaffolding雕虫小技而已。

Grails的scaffolding生成的是HTML代码,而不是JSON形式的form定义,无法直接跟Ext无缝整合。

 

为Ext写个form scaffolding并非难事。跟Grails的scaffolding原理一致,表单中的fields是从domain class中提取出来的,排序则按照它们在constraints中出现的先后顺序。

 

先看一个domain class的示例:

class SiteContactLog {
    
    @Hidden // Gotta hide it, otherwise it will be rendered as a combobox by default. And loading all sites is time-consuming
    Site site

    @Hidden
    User user
    
    Date contactDate
    
    @Xtype(xtype='timefield')
    String contactTime
    
    @SelectionItemFetcher(fetcher="{d->app.getArtefact('Domain','com.xxx.sctms.Person').clazz.findAll('site.id':d._belongsTo)}")
    @LiveSearch(searchBy='name')
    List<Person> personsContacted
    
    @SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactCorrespondence');q.descend('name').orderAscending()}")
    GenericSelectionItem correspondence
    
    @SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactDirection');q.descend('name').orderAscending()}")
    GenericSelectionItem direction
     
    @SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactMethod');q.descend('name').orderAscending()}")
    GenericSelectionItem contactMethod
    
    @SelectionItemFilter(filter="{d,q->q.descend('group').constrain('siteContactTopic');q.descend('seq').orderAscending()}")
    GenericSelectionItem topic

    String summary

    static constraints = {
        contactDate nullable:false
        contactTime()
        personsContacted nullable:false, minSize:1
        correspondence nullable:false
        direction()
        contactMethod()
        topic()
        summary nullable:false, blank:false, maxSize:Integer.MAX_VALUE
    }

}

 

基于domain生成form的机理用以下两个代码片段说明:

static retrieveDomainFields(domain) {
      def artefact = getDomainArtefact(domain)
      def fields = []
      def constrainedProperties = artefact.constrainedProperties
      artefact.properties.each{
        if(!("${it.name}" ==~ /id|parentId|version|dateCreated|createdBy|deleted|dateDeleted|deletedBy/)) {
          def ext = [:]
          def declaredField = artefact.clazz.getDeclaredField(it.name)

          def label = declaredField.getAnnotation(com.xxx.annotations.Label.class)
          def formattedName = label?.label() ?: getFormattedNameByConvention(it.name)

          // extra configurations from annotations:
          def embeddedObject = declaredField.getAnnotation(com.xxx.annotations.EmbeddedObject.class)
          if(embeddedObject) {
            ext['embeddedObject'] = [height: embeddedObject.height(), width: embeddedObject.width()]
          }
          def singleCheckBox = declaredField.getAnnotation(com.xxx.annotations.SingleCheckBox.class)
          if(singleCheckBox) {
            ext['singleCheckBox'] = true
          }
          def radio = declaredField.getAnnotation(com.xxx.annotations.Radio.class)
          if(radio) {
            ext['radio'] = true
          }
          def liveSearch = declaredField.getAnnotation(com.xxx.annotations.LiveSearch.class)
          if(liveSearch) {
            ext['liveSearch'] = true
          }
          def hidden = declaredField.getAnnotation(com.xxx.annotations.Hidden.class)
          if(hidden) {
            ext['hidden'] = true
          }
          def xtype = declaredField.getAnnotation(com.xxx.annotations.Xtype.class)
          if(xtype) {
            ext['xtype'] = xtype.xtype()
          }// TODO: inject more extensions from other annotations...

          ext['declaredField'] = declaredField

          if(constrainedProperties."${it.name}") {
              if(constrainedProperties."${it.name}".maxSize) {
                  ext['maxSize'] = constrainedProperties."${it.name}".maxSize
              }
          }

//          fields << [it.name, formattedName, formattedName, it.type, ext] // name, formattedName, description, type, ext
          fields << [it.name, formattedName, formattedName, ext] // name, formattedName, description, ext (declaredField has been added to ext, do we still need it.type?)
        }
      }

      // sort by the sequence they appear in the constraints
      fields.sort{f1,f2->
         def cp1 = constrainedProperties[f1[0]]
         def cp2 = constrainedProperties[f2[0]]
         cp1?(cp2?cp1.order<=>cp2.order:1):(cp2?-1:0)
      }

      fields
  }
 
private buildFormScaffold(domain, fields) {
        def artefact = DomainUtils.getDomainArtefact(domain)
        def dn = artefact.clazz.name

        def maxCaptionWidthAndControlsTotalHeight = processFields(fields)
        def captionWidth = maxCaptionWidthAndControlsTotalHeight[0], captionHeight = 22, controlsTotalHeight = maxCaptionWidthAndControlsTotalHeight[1]

        def width = COMPONENT_WIDTH, top = FORM_MARGIN, left = captionWidth + FORM_MARGIN, tabIndex = 100
        def processingRightHalf = false, leftTop = 0, rightTop = 0

        def leftControlsTotalHeight = 0

        def scaffold = [
            "id":"${UUID.randomUUID()}",
            "xtype":"form",
            "layout":"absolute",
            "name":dn,
            "height":0, //TBD
            "width":(captionWidth + width + FORM_MARGIN*2)*2,
            "title":"FORM SCAFFOLD",
            "items":[]]

        for(int i = 0; i < fields.size(); i++) {
            if(leftControlsTotalHeight*2 >= controlsTotalHeight && !processingRightHalf) {
                // reset top and left
                processingRightHalf = true
                top = FORM_MARGIN
                left += (width + FORM_MARGIN * 2 + captionWidth)
            }

            def field = fields[i]

            // e.g., Site has a 'study' field, and site belongs to study
            // the study field should not show up
            if(isBelongsToField(dn, field[0]) || field[3]['hidden'])continue

            def htmlType = field[3].xtype ?: field[3].htmlType

            def nullableConstraint = artefact.constraints."${field[0]}".getAppliedConstraint('nullable')
            scaffold.items << [
              xtype:htmlType,id:"${UUID.randomUUID()}",name:field[0],
		      allowBlank:nullableConstraint ? nullableConstraint.isNullable() : true,
              forceSelection:false,
              readOnly:false,
              captionText:field[1],captionPosition:"Left",captionHeight:captionHeight,captionWidth:captionWidth,
              x:(left-(htmlType=='placeHolder'?captionWidth:0)),y:top,
              height:field[3].height,width:(width+(htmlType=='placeHolder'?captionWidth:0)),tabIndex:tabIndex]
            if(htmlType == 'datefield') {
                scaffold.items[-1].format = DATE_FORMAT    
            } else if(htmlType == 'radiogroup') {
                if(field[3].singleCheckBox) {
                    scaffold.items[-1].xtype = 'checkbox'
                    scaffold.items[-1].singleCheckBox = true
                    scaffold.items[-1].xtype = "checkbox"
                    scaffold.items[-1].boxLabel = field[1]
                    scaffold.items[-1].y += 4
                }
            }

            tabIndex += 100
            top      += (field[3].height + 4)

            if(processingRightHalf) {
                rightTop = top
            } else {
                leftControlsTotalHeight += field[3].height
                leftTop  = top
            }
        }
        scaffold.height = (leftTop > rightTop ? leftTop : rightTop) + (FORM_MARGIN-4)
        scaffold as JSON
    }
 

生成的form形如

{
   "id" : "a4fe0cda-ec31-4ec0-a809-bdbe73891fdf", "xtype" : "form", "layout" : "absolute", "name" : "com.xxx.sctms.SiteContactLog", "height" : 236, "width" : 832, "title" : "FORM SCAFFOLD", "items" : [ {
      "xtype" : "datefield", "id" : "f9a7a0ab-6799-473c-b811-7a6957ea95cc", "name" : "contactDate", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Date", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 15, "height" : 22, "width" : 250, "tabIndex" : 100, "format" : "M/d/Y"}
   , {
      "xtype" : "timefield", "id" : "838ec058-4ccb-4134-81df-c77d001365da", "name" : "contactTime", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Time", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 41, "height" : 22, "width" : 250, "tabIndex" : 200}
   , {
      "xtype" : "checkboxgroup", "id" : "7ad425bc-49e4-4c93-9051-a42c7a42d197", "name" : "personsContacted", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Persons Contacted", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 67, "height" : 102, "width" : 250, "tabIndex" : 300}
   , {
      "xtype" : "combo", "id" : "34e2a5fd-c37d-41b5-ad26-4c98ec5a91f6", "name" : "correspondence", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Correspondence", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 173, "height" : 22, "width" : 250, "tabIndex" : 400}
   , {
      "xtype" : "combo", "id" : "c146e14d-2ac7-4cbd-8648-55c3f62bb306", "name" : "direction", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Direction", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 151, "y" : 199, "height" : 22, "width" : 250, "tabIndex" : 500}
   , {
      "xtype" : "combo", "id" : "93070ee3-1def-4c93-b7c1-2a722ed20409", "name" : "contactMethod", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Contact Method", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 15, "height" : 22, "width" : 250, "tabIndex" : 600}
   , {
      "xtype" : "combo", "id" : "a3a427dd-c499-4378-9e63-f9a3d59e2958", "name" : "topic", "allowBlank" : true, "forceSelection" : false, "readOnly" : false, "captionText" : "Topic", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 41, "height" : 22, "width" : 250, "tabIndex" : 700}
   , {
      "xtype" : "textarea", "id" : "ef85d8cb-50e6-4508-9d9c-83056208427e", "name" : "summary", "allowBlank" : false, "forceSelection" : false, "readOnly" : false, "captionText" : "Summary", "captionPosition" : "Left", "captionHeight" : 22, "captionWidth" : 136, "x" : 567, "y" : 67, "height" : 102, "width" : 250, "tabIndex" : 800}
   ]}

这样生成的form排版比较单一,分两栏,高度大致相等。若需修改,可使用表单设计器...

 

### 实现Grails中通过GSP页面实现Excel文件的数据导入功能 要在Grails应用程序中实现Excel数据导入功能,可以按照以下方法设计并开发: #### 1. 添加依赖项 为了处理Excel文件,需要引入第三方库来解析Excel文档。常用的库有Apache POI或JXL。可以通过`build.gradle`文件添加这些依赖。 ```groovy dependencies { implementation 'org.apache.poi:poi-ooxml:5.2.3' // Apache POI用于读取Excel文件 } ``` 此操作允许程序支持`.xls`和`.xlsx`格式的文件[^3]。 --- #### 2. 创建控制器逻辑 创建一个专门负责上传和解析Excel文件的控制器。例如,名为`ImportController.groovy`。 ```groovy class ImportController { def index() {} def upload() { MultipartFile file = request.getFile('excelFile') if (file && !file.isEmpty()) { try { Workbook workbook = new XSSFWorkbook(file.getInputStream()) Sheet sheet = workbook.getSheetAt(0) for (Row row : sheet) { Cell cellA = row.getCell(0) Cell cellB = row.getCell(1) String valueA = cellA?.stringCellValue ?: "" String valueB = cellB?.stringCellValue ?: "" println "Value A: $valueA, Value B: $valueB" // 将数据保存到数据库或其他存储位置 } flash.message = "成功导入 ${sheet.getLastRowNum()} 条记录." } catch (Exception e) { flash.error = "发生错误: ${e.getMessage()}" } } else { flash.error = "未选择任何文件!" } redirect(action: 'index') } } ``` 在此代码片段中,使用了Apache POI中的`XSSFWorkbook`类来加载Excel文件,并逐行遍历工作表的内容[^4]。 --- #### 3. 设计GSP视图 在`grails-app/views/import/index.gsp`中定义HTML表单以供用户上传Excel文件。 ```html <!DOCTYPE html> <html> <head> <title>Excel 数据导入</title> </head> <body> <h1>Excel 文件导入</h1> <g:if test="${flash.message}"> <div class="message">${flash.message}</div> </g:if> <g:if test="${flash.error}"> <div class="error">${flash.error}</div> </g:if> <form action="${createLink(controller:'import', action:'upload')}" method="post" enctype="multipart/form-data"> <label for="excelFile">请选择 Excel 文件:</label><br/> <input type="file" id="excelFile" name="excelFile"/><br/><br/> <button type="submit">上传并导入</button> </form> </body> </html> ``` 这段代码提供了一个简单的界面让用户可以选择本地计算机上的Excel文件并通过POST请求提交给服务器[^5]。 --- #### 4. 配置路由 确保应用能够识别新的动作路径,在`UrlMappings.groovy`中加入如下配置: ```groovy "/import"(controller:"import", action:"index") "/import/upload"(controller:"import", action:"upload") ``` 这一步使得URL `/import`指向默认显示页而`/import/upload`则接收来自用户的文件上传请求[^6]。 --- #### 5. 测试与优化 测试整个流程是否正常运作,包括但不限于验证不同类型的Excel文件兼容性、异常情况下的反馈机制以及性能表现等方面。如果发现某些特定场景下存在问题,则需进一步调整和完善相应部分。 --- ### 注意事项 - **安全性**:应考虑对上传文件进行病毒扫描及大小限制。 - **用户体验**:可增加进度条等功能提升交互体验。 - **扩展性**:未来可能还需要支持更多种类电子表格软件产生的文件格式转换需求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值