我遇到的问题可能是设计问题,而不是技术问题。
假设我有这些课程:
class A {
def someInfo
static hasMany = [b: B]
}
class B {
A a
def info
def moreInfo
...
def aLotMoreInfo
}
现在让我们假设用户在一个页面上,他可以编辑A的b,他可以添加新的b。但是用户需要将他的更改保存到A,否则一切都将被丢弃。
我目前的方法是创建额外的b,通过AJAX渲染它们并将它们的ID保存在会话变量中,这样我就可以删除“未保存”的b。
这适用于一个常见用例: 用户刷新页面。
我使用window.onunload-event通知用户他将丢失未保存的更改,并对其中的删除函数进行AJAX调用以从session-variable中删除b。 不幸的是,在删除b之前调用A控制器的索引功能。 这意味着,显示“未保存的”b,并且很快它们将被删除,这将迫使我刷新或等待以某种方式删除b。
也许我试图完成它的方式无论如何都是错误的 - 在这种情况下,我会对任何建议感到高兴。
所以问题是:如何密切关注可能被丢弃的新对象而无需将其中的每个信息存储在隐藏字段中以在保存函数上创建它们?
更新
我之前应该提到它,但我认为它并不重要。
B是一个抽象类,它由许多类扩展,如下例所示:
class childOfB extends B {
def usefulExtraInfo
}
class anotherChildOfB extends B {
def anotherUsefulExtraInfo
}
除此之外,B有一个整数字段,表示A中b的Set中的位置。我知道我可以使用SortedSet,但由于某些特定的原因,它必须是一个单独的字段。 我提到这一点是因为视图将每个视图呈现为可排序列表的元素,可以通过拖放重新排序。
用例:用户添加一些childOfB,anotherChildOfB并根据需要重新排序。如何在不在视图中存储类型的情况下跟踪它们的类型,这也是不好的做法,我想?
此致
答案 0 :(得分:2)
用户需要将他的更改保存到A,否则一切都会 丢弃
听起来,当您不需要时,您正急切地创建B
- 您只想在用户通过保存A
确认整个操作时创建它们。
他可以编辑A的b的页面,他可以添加新的b
看起来您还有一个页面,其中显示所有B
以进行编辑,因此无需在整个地方保留隐藏字段。
我要做的是使用普通表单输入保留视图中的所有当前更改,并调用保存A
并创建/修改/删除B
'的单个事务操作根据参考文献。
根据应用程序的外观,您可以通过多种方式执行此操作。
我过去使用过的一个模板(让我们说editB
)接收B
,索引和prefix
并显示相应的输入赋予B
前缀为${property}.
的名称(即在编辑模式下呈现给定的B
)。
A
的修改视图会为editB
所有B
呈现B
,并且:
b
将触发Ajax调用以检索此模板以获取新的B,前缀A
(B
的属性名称)和对应的索引列表的长度。A
会简单地删除与模板对应的HTML片段,并重新计算索引。然后,在保存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}" />
<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>
编辑A
的视图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)
}
}
}
<强> AController 强>:
renderNewB
用于调用B
的Javascript函数,用于删除现有B
的HTML片段,并且缺少处理索引但是我希望这个想法很清楚:)。
<强>更新强>
我假设:
我认为这需要更好的客户端而不是依赖服务器技巧。您描述的更改并没有使它与众不同。
SortedSet
的属性使得思考比处理List
/ A
更容易:
a.b.sort { it.index }
时,需要添加B
以保留订单。B
时,需要添加索引的隐藏输入。 B
中继承真的需要将域类作为视图中的隐藏输入(或者使用一些Javascript来跟踪该信息,但我没有看到它的好处)。我不明白为什么这么糟糕。您正在使用继承作为“B类型”。如果您在type
中有一个名为B
的属性而不是继承,那么您可以使用输入,对吧?
domainClassName
时,需要传递“类型”(B
)id
时,如果没有A
,则需要传递类名的隐藏输入B
时,新的B
是使用特定的域类创建的,否则不会发生任何变化。我已更新代码以反映此更改。
如果我真的想提前保存对象怎么办?
如果您确信这是正确的方法,我仍会尝试避开会话并向名为confirmed
的{{1}}添加新属性。
B
时,已确认设置为false。A
时,尚未删除的所有归属B
将confirmed
设置为true
,删除的已归好,已删除:)A
时,仅显示已确认的B
。即使用户关闭浏览器或会话失效,也不会向用户显示未确认的B
,并且会在再次保存A
时最终删除。您还可以添加一个Quartz作业,该作业会根据一些超时定期清除未经证实的B
,但这很棘手 - 因为保存未确认数据的整个想法是: - )。