public void doSave(Widget frmModel, JXPathContext jctx) throws BindingException {
    // (See comment in doLoad about type checking and throwing a meaningful exception.)
    Repeater repeater = (Repeater) selectWidget(frmModel, this.repeaterId);

    // Perform shortcut binding if the repeater is empty
    // and the deleteIfEmpty config option is selected.
    if (repeater.getSize() == 0 && this.deleteIfEmpty) {
      // Delete all of the old data for this repeater.
      jctx.removeAll(this.repeaterPath);

      // Otherwise perform the normal save binding.
    } else {

      // Narrow to the repeater context, creating the path if it did not exist.
      JXPathContext repeaterContext = jctx.getRelativeContext(jctx.createPath(this.repeaterPath));

      // Start by deleting all of the old row data.
      repeaterContext.removeAll(this.rowPath);

      // Verify that repeater is not empty and has an insert row binding.
      if (repeater.getSize() > 0) {
        if (this.insertRowBinding != null) {

          // register the factory!
          // this.insertRowBinding.saveFormToModel(repeater, repeaterContext);

          // Iterate through the repeater rows.
          for (int i = 0; i < repeater.getSize(); i++) {

            // Narrow to the repeater row context.
            Pointer rowPointer = repeaterContext.getPointer(this.rowPathInsert);
            JXPathContext rowContext = repeaterContext.getRelativeContext(rowPointer);

            // Variables used for virtual rows.
            // They are initialized here just to keep the compiler happy.
            Node rowNode = null;
            Node virtualNode = null;

            // If virtual rows are requested, create a temporary node and
            // narrow the context to this initially empty new virtual row.
            if (virtualRows) {
              rowNode = (Node) rowContext.getContextBean();
              Document document = rowNode.getOwnerDocument();
              virtualNode = document.createElementNS(null, "virtual");
              Node fakeDocElement = document.getDocumentElement().cloneNode(false);
              fakeDocElement.appendChild(virtualNode);
              rowContext = JXPathContext.newContext(repeaterContext, fakeDocElement);
              rowContext = rowContext.getRelativeContext(rowContext.getPointer("virtual"));
            }

            // Perform the insert row binding
            this.insertRowBinding.saveFormToModel(repeater, rowContext);

            // Perform the save row binding.
            this.rowBinding.saveFormToModel(repeater.getRow(i), rowContext);

            // If virtual rows are requested, finish by appending the
            // children of the virtual row to the real context node.
            if (virtualRows) {
              NodeList list = virtualNode.getChildNodes();
              int count = list.getLength();
              for (int j = 0; j < count; j++) {
                // The list shrinks when a child is appended to the context
                // node, so we always reference the first child in the list.
                rowNode.appendChild(list.item(0));
              }
            }
            getLogger().debug("bound new row");
          }
        } else {
          getLogger()
              .warn(
                  "TempRepeaterBinding has detected rows to insert, "
                      + "but misses the <on-insert-row> binding to do it.");
        }
      }
    }
  }