2

我有一个问题,可能是设计问题而不是技术问题。

假设我有这些课程:

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 调用以从会话变量中删除 b。不幸的是,在我删除 b 之前调用了 A 控制器的索引函数。这意味着,“未保存”的 b 会显示出来,不久之后它们将被删除,这将迫使我进行刷新或等待 b 以某种方式被删除。

也许我尝试完成它的方式无论如何都是错误的 - 在这种情况下,我很乐意提出任何建议。

所以问题是:如何关注可能被丢弃的新对象,而不需要将它的所有信息存储在隐藏字段中以在保存功能上创建它们?

更新:

我应该在之前提到它,但我认为它不是那么重要。

B 是一个抽象类,它被许多类扩展,例如以下示例:

class childOfB extends B {
    def usefulExtraInfo
}

class anotherChildOfB extends B {
    def anotherUsefulExtraInfo
}

除此之外,B 有一个整数字段,表示 A 中 b 的集合中的位置。我知道我可以为此使用 SortedSet,但由于某些特定原因,它必须是一个单独的字段。我提到这一点是因为视图将每个元素呈现为可排序列表的一个元素,可以通过拖放重新排序。

用例:用户添加一些 childOfB、anotherChildOfB 并根据需要对它们重新排序。我如何在不将类型存储在视图中的情况下跟踪它们的类型,我认为这也是不好的做法?

问候

4

1 回答 1

2

用户需要保存他对 A 的更改,否则一切都将被丢弃

在我看来,您在不需要时急切地创建B's - 您只想在用户通过保存确认整个操作时创建它们A

一个页面,他可以在其中编辑 A 的 b,他可以添加新的 b

看起来您也有一个页面,其中所有Bs 都显示以供编辑,因此没有真正需要在整个地方保留隐藏字段。

然后我要做的是保留视图中的所有当前更改,使用正常的表单输入,并调用单个事务操作,根据参数保存A和创建/修改/删除B's。

根据您的应用程序的外观,您可以通过多种方式执行此操作。

我过去使用的一个是有一个模板(比如说editB),它接收 a B、一个索引和 aprefix并显示给定B名称的相应输入,并以前缀为前缀${property}.(即它B在编辑模式下呈现给定)。

然后,编辑视图A将呈现它拥有editB的所有B',并且:

  • 添加一个新的B将触发一个 Ajax 调用,以检索该模板以获取新的 B、前缀bA属性的名称)和对应于列表长度的索引。
  • 删除一个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's 的 HTML 片段和处理索引的 Javascript 函数,但我希望这个想法很清楚:)。

更新

我假设:

  • 依赖会话获取关键信息并不是很好:会话可能会失效(例如,用户注销)并且它们不适合扩展。
  • 为了不必在视图中携带对象而保存对象是一个糟糕而脆弱的想法 - 它很容易中断(会话无效,用户关闭浏览器,有一个部署并且会话没有持久化)并且需要清理。可以做到,但在我看来成本太高了。

我认为这需要一个更好的客户端,而不是依赖服务器技巧。您描述的更改并没有太大的不同。

  • 将索引作为 的属性B使思考比处理SortedSet/更容易List
    • 显示时Aa.b.sort { it.index }需要添加以保持顺序。
    • 渲染时B,需要添加索引的隐藏输入。
    • 当拖放或删除发生时,需要重新计算索引的 Javascript 函数。
    • 绑定数据时,没有任何变化,因为索引只是一个属性。
  • 继承B确实需要将域类作为视图中的隐藏输入(或使用一些 Javascript 来跟踪该信息,但我没有看到好处)。我不明白为什么这么糟糕。您正在使用继承作为“B 类型”。如果你有一个属性而不是继承Btype那么你会使用它的输入,对吗?
    • 渲染新的时,需要传递B“类型”( )domainClassName
    • 渲染时B,如果没有id,则需要传递类名的隐藏输入
    • 保存时A,新B的是使用特定的域类创建的,否则没有任何变化。

我已更新代码以反映此更改。

如果我真的想预先保存对象怎么办?

如果您真的确信这是正确的方法,我仍然会尝试避免会话并为B被调用添加一个新属性confirmed

  • 当用户添加一个新的B,confirmed 设置为 false。
  • 当用户保存一个A时,所有B没有被删除的所有物都被confirmed设置为true,被删除的很好,被删除了:)。
  • 显示时A,仅显示已确认B的 。

即使用户关闭浏览器或会话失效,未确认的也永远不会显示给用户,并且在再次保存B时最终会被删除。A您还可以添加一个 Quartz 作业,B根据一些超时定期清理未确认的 s,但这很棘手 - 因为保存未确认数据的整个想法是:-)。

于 2015-07-28T14:15:56.197 回答