用户需要将他的更改保存到 A,否则一切都会
丢弃
在我看来,您在不需要时急切地创建B - 您只想在用户通过保存A 确认整个操作时创建它们。
一个页面,他可以在其中编辑 A 的 b,他可以添加新的 b
看起来您还有一个页面,其中显示了所有 Bs 以供编辑,因此没有真正需要在所有地方保留隐藏字段。
然后我要做的是保留视图中的所有当前更改,使用正常的表单输入,并调用单个事务操作来保存 A 并根据参数。
根据您的应用程序的外观,您可以通过多种方式执行此操作。
我过去使用的一个模板(比如说editB)接收B、索引和prefix,并显示给定B的相应输入以${property}. 为前缀的名称(即在编辑模式下呈现给定的B)。
A 的编辑视图随后将为它拥有的所有 B 呈现 editB,并且:
- 添加一个新的
B 会触发一个Ajax 调用来检索这个模板以获得一个新的B,前缀b(A 的属性的名称)和一个对应于列表长度的索引。
- 删除
B 会简单地删除与模板对应的 HTML 片段,并重新计算索引。
然后,在保存 A 时,控制器将检查 params.list('b') 中的内容并相应地创建、更新和删除。
一般来说是这样的:
模板/templates/_editB.gsp
<g:if test="${instance.id}">
<input type="hidden" name="${prefix}.id" value="${instance.id}" />
</g:if>
<g:else>
<input type="hidden" name="${prefix}.domainClassName" value=${instance.domainClass.clazz.name}" />
</g:else>
<input type="hidden" name="${prefix}.index" value=${instance.index}" />
<input type="..." name="${prefix}.info" value="${instance.info}" />
编辑 A 的视图
<g:each var="b" in="${a.b.sort { it.index }}">
<g:render template="/templates/editB" model="${[instance: b, prefix: 'b']}" />
<button onClick="deleteTheBJustUpThereAndTriggerIndexRecalculation()">Remove</button>
</g:each>
<button onClick="addNewBByInvokingAController#renderNewB(calculateMaxIndex())">Remove</button>
控制器:
class AController {
private B getBInstance(String domainClassName, Map params) {
grailsApplication
.getDomainClass(domainClassName)
.clazz.newInstance(params)
}
def renderNewB(Integer index, String domainClassName) {
render template: '/templates/editB', model: [
instance: getBInstance(domainClassName, [index: index]),
prefix: 'b'
]
}
def save(Long id) {
A a = a.get(id)
bindData(a, params, [exclude: ['b']]) // We manually bind b
List bsToBind = params.list('b')
List<B> removedBs = a.b.findAll { !(it.id in bsToBind*.id) }
List newBsToBind = bsToBind.findAll { !it.id }
A.withTransaction { // Or move it to service
removedBs.each { // Remove the B's not present in params
a.removeFromB(it)
it.delete()
}
bsToBind.each { bParams ->
if (bParams.id) { // Just bind data for already existing B's
B b = a.b.find { it.id == bParams.id }
bindData(b, bParams, [exclude: 'id', 'domainClassName'])
}
else { // New B's are also added to a
B newB = getBInstance(bParams.remove('domainClassName'), bParams)
a.addToB(b)
}
}
a.save(failOnError:true)
}
}
}
缺少用于调用renderNewB、删除现有B 的HTML 片段以及处理索引的Javascript 函数,但我希望这个想法很清楚:)。
更新
我假设:
- 依赖会话获取关键信息并不好:会话可能会失效(例如,用户注销)并且它们不适合扩展。
- 为了不必在视图中携带对象而保存对象是一个糟糕而脆弱的想法 - 它很容易被破坏(会话无效,用户关闭浏览器,有一个部署并且会话不持久)并且需要干净向上。可以,但是我觉得成本太高了。
我认为这需要更好的客户端,而不是依赖服务器技巧。您描述的更改并没有太大的不同。
- 将索引作为
B 的属性使思考比处理SortedSet/List 更容易:
- 显示
A时,需要添加a.b.sort { it.index }以保留顺序。
- 在渲染
B 时,需要添加索引的隐藏输入。
- 当发生拖放或删除时,需要重新计算索引的 Javascript 函数。
- 绑定数据时,没有任何变化,因为索引只是一个属性。
- 在
B 中具有继承确实需要将域类作为视图中的隐藏输入(或使用一些Javascript 来跟踪该信息,但我看不到好处)。我不明白为什么这么糟糕。您正在使用继承作为“B 类型”。如果你在B 中有一个名为type 的属性而不是继承,那么你会使用它的输入,对吗?
- 在渲染新的
B 时,需要传递“类型”(domainClassName)
- 在渲染
B时,如果没有id,则需要传递一个隐藏的类名输入
- 保存
A 时,新的B 将使用特定的域类创建,否则不会发生任何变化。
我已更新代码以反映此更改。
如果我真的想提前保存对象怎么办?
如果您真的确信这是正确的方法,我仍然会尝试避免会话并向B 添加一个名为confirmed 的新属性。
- 当用户添加新的
B 时,confirmed 设置为 false。
- 当用户保存
A时,所有未删除的B所属的confirmed设置为true,删除的就好了,删除:)。
- 显示
A 时,仅显示已确认的B。
即使用户关闭浏览器或会话失效,未确认的B 永远不会显示给用户,并且在再次保存A 时最终会被删除。您还可以添加一个 Quartz 作业,根据一些超时定期清理未确认的 Bs,但这很棘手 - 因为保存未确认数据的整个想法是:-)。