这里的试验是在Grails下做的,但是在Spring MVC中应该也能适用,即便不行,通过简单的扩展也能达到效果。对于其他框架像Struct之类应该也都没问题(不了解,不确定)。
能实现的自动封装包含:
1.一层简单模型
- class AddressCommand {
- String city
- String street
- int hourseNumber
- }
2.One to one,多层模型
- class PersonCommand {
- String name
- int age
- AddressCommand address
- }
3.One to many, 多层模型
- class ChildCommand {
- String name
- int age
- }
- class PersonCommand {
- String name
- int age
- AddressCommand address
- List children
- }
这个模型模型就是我们最终要使用的模型。这里我们不讨论Many to One 和 Many to Many模型,不常见(我还没遇到过),而且也可以通过转换转成以上几种类型;例子中的模型只有两层,多层的依葫芦画瓢,原理不变,增加了一些复杂性。
我们先创建一个Controller,之后我们都会把数据Post到save Action,如果数据对的话,Grails就会帮助自动组装好模型:
- class PersonController {
- def save={PersonCommand person->
- }
- }
一.数据传输格式
数据传输格式是最重要的问题,直接关系到后续过程中前,后台的工作量。
对于一层简单模型和One to one多层模型,数据格式是很成熟的。现有的框架都处理的很好,对于上面的model,他们的数据格式如下:
Name | Value |
---|---|
name | yanical |
age | 26 |
address.city | Zhuhai |
address.street | Jida |
address.hourseNumber | 344 |
其实我们要解决的主要是一对多的问题。一对多模型里面有个list(array)对象,在form里的数据是扁平的,我们要在扁平的数据里带上list顺序特性。
对于Grails,我尝试了几种数据格式,比如:
Name | Value |
---|---|
children.0.name | little |
children.0.age | 2 |
Name | Value |
---|---|
children[0].name | little |
children[0].age | 2 |
children[1].name | large |
children[1].age | 4 |
这里List有两个元素。这个时候还是没有成功,但是出了如下的exception:
- Stacktrace follows:
- java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
- at java.util.ArrayList.RangeCheck(ArrayList.java:547)
- at java.util.ArrayList.get(ArrayList.java:322)
- at java.lang.Thread.run(Thread.java:619)
这个exception说明,grails的form->model模型组装模块已经识别了上传的数据,只是因为我们的list是空的,在组装模型的时候Grails试图从List中找到对应的element来塞数据,但是没有成功。修改Person类后,模型就可以成功组装了。
- class PersonCommand {
- String name
- int age
- AddressCommand address
- List children = []
- Person() {
- children[0] = new ChildCommand()
- children[1] = new ChildCommand()
- }
- }
二.子对象的初始化
上面我们发现对于根对象Person,和One to one的对象,Grails都会帮忙创建对象实例,但是对于One to many,Grails并没有做相关工作。我们要想个方案能够在组装模型前,能把对应的对象实例都创建好。
有两种思路:1.是在代码里就先hardcode创建好一定数量的实例(像前面做的那样),比如10个,在能确定实例数量会很小的时候,这种方案也还可行。2.是考虑可以不可以通过插入一些代码,实现在组装模型的时候动态创建实例。
这里的实现采用第二种思路。
看回上面的Exception,这个exception是去list里取数据的时候发现数据不存在的时候抛出来的。说明数据组装模块会去List里拿实例,再把数据塞到实例里面去,我们就在数据组装模块去List拿数据的时候插入一些代码。
我们使用一个工具来达到这个目的,这个工具就是org.apache.commons.collections.list.LazyList,LazyList在我们这里的作用就是确保数据组装模块去List取数据的时候始终能够把实例取出来。在List的里面有这个对象实例的时候,就把对象实例返回,否则就创建一个新的对象实例。修改Person类如下:
- class PersonCommand {
- String name
- int age
- AddressCommand address
- List children = LazyList.decorate([],
- FactoryUtils.instantiateFactory(ChildCommand.class))
- }
这个时候模型就已经可以成功创建了,即便List里面会有很多的元素,也能很好的处理。
三.Form表单定义
接下来,前台的任务就是要组装出前面要求的数据格式,对于One to one,我们还是一笔带过,form表单如下:
- <input type="text" name="name" id="name" value="">
- <input type="text" name="age" id="age" value="">
- <input type="text" name="address.city" id="age" value="">
- <input type="text" name="address.street" id="age" value="">
- <input type="text" name="address.hourseNumber" id="age" value="">
对于One to many,页面中肯定会有Add和Delete的地方来增加和删除某一个元素。由于增加或者删除元素的过程中会调整UI,所以希望能够提取出一段公用的JS来实现这个功能。代码大致如下,包含两个方法onOneToManyAddClick和onOneToManyDeleteClick ,分别用来处理Add和Delete点击事件(基于JQuery,现在不需要理会这段代码的细节):
- function onOneToManyAddClick(triggerElement, oneToManyProperty) {
- var table = $("table[scaffoldFor='"+oneToManyProperty+"']");
- var count = table.attr("count");
- if(count == ""){
- count = 0;
- }
- var tr = $("tr[scaffoldFor='"+oneToManyProperty+"']").filter(function() {
- return $(this).is(":hidden");
- }).clone();
- tr.attr("index",count);
- var inputs = $(tr).find(":input");//has include input, textarea, select and button
- $.each(inputs, function(i, n) {
- var input = $(n);
- var name = input.attr("name");
- name = name.replace("*","["+count+"]");
- input.attr("name",name);
- input.attr("id",name);
- });
- count++;
- table.attr("count", count);
- tr.appendTo(table)
- tr.show();
- }
- function onOneToManyDeleteClick(triggerElement, oneToManyProperty) {
- var current = $(triggerElement);
- var currentTr;
- while(true) {
- if(current.is("tr") && current.attr("scaffoldFor") && oneToManyProperty.indexOf(current.attr("scaffoldFor"))>=0) {
- currentTr = current;
- break;
- } else {
- current = current.parent();
- }
- }
- var currentIndex = currentTr.attr("index");
- var currentFor = currentTr.attr('scaffoldFor');
- var table = $("table[scaffoldFor='"+currentFor+"']");
- var count = table.attr("count");
- //remove element -- start
- var nextTrs = currentTr.nextAll("tr").filter(function() {
- return $(this).attr("scaffoldFor") == currentFor;
- });
- $.each(nextTrs, function(i, n) {
- var tr = $(n);
- var index = tr.attr("index");
- var replaceFrom = currentFor+"["+index+"]";
- var replaceTo = currentFor+"["+(index-1)+"]";
- tr.attr("index", (index-1));
- $.each($(tr).find(":input"), function(i, n) {
- var input = $(n);
- var name = input.attr("name");
- name = name.replace(replaceFrom, replaceTo);
- input.attr("name",name);
- input.attr("id",name);
- });
- });
- if(count == ""){
- table.attr("count", 0);
- }
- count--;
- table.attr("count", count);
- currentTr.remove();
- //remove element -- end
- }
而,我们的One to many form表单就是这样的了:
- <table scaffoldfor="children" count="1">
- <tbody>
- <tr scaffoldfor="children" style="display: none">
- <td>
- <input type="text" name="children*.name" id="children*.name" value="">
- <input type="text" name="children*.age" id="children*.age" value="">
- <span>
- <a href="#" onclick="onOneToManyDeleteClick(this,'children');return false;">Add</a>
- <a href="#" onclick="onOneToManyAddClick(this,'children');return false;">Delete</a>
- </span>
- </td>
- </tr>
- <tr>
- </tr>
- <tr scaffoldfor="children" index="0">
- <td>
- <input type="text" name="children[0].name" id="children[0].name" value="">
- <input type="text" name="children[0].age" id="children[0].age" value="">
- <span>
- <a href="#" onclick="onOneToManyDeleteClick(this,'children');return false;">Add</a>
- <a href="#" onclick="onOneToManyAddClick(this,'children');return false;">Delete</a>
- </span>
- </td>
- </tr>
- </tbody>
- </table>
初始化的时候包含两个TR:第二个是可见的,就是用户打开页面是可以看到的输入框;第一个是隐藏的,用户点击Add的时候,我们会克隆这个TR并append到table最后显示给用户,隐藏的TR的数据不需要提交给后台,我们设置特殊的input name属性(中间包含*,如children*.name),这样后台Grails就会忽略这些input。
版权所有,转发请注明来源