< 上一课 | 下一课 >

修改记录

在本课程中,您将了解如何修改记录的字段值以及如何添加或删除附件。
本课程中的示例代码让用户可以更新自己正在查看的记录。这个示例通过继承 ViewRecord.Viewer 类扩展了先前模块中的 ViewRecord 示例,从而支持一个组合框用于进行选择操作,另外还增加了 Edit 按钮,用于对记录启动选定的操作。这个 Viewer 的 editRecord 方法可以查看和更新为更新而打开的记录。 利用 showRecord 的 future 自变量,将这些新的 GUI 组件引入到了 ViewRecord.Viewer 显示中。

现在,除了 ALL_FIELD_VALUES 属性,view(CqRecord) 方法还可以读取记录的 LEGAL_ACTIONS 属性。LEGAL_ACTIONS 属性值是针对处于当前状态的记录,可以合法对该记录进行操作的“操作”代理列表。组合框控件中包含了这个操作列表,通过该组合框控件,用户可以在单击 Edit 按钮前选择某个操作。这段示例代码不支持某些类型的操作,例如,SUBMIT 和 RECORD_SCRIPT_ALIAS,因此,即使这些操作属于这组合法的操作,但也没有将其添加到这个组合框控件中。

public JFrame view(CqRecord selected)
{
    try {
        final CqRecord record =
                (CqRecord)selected.doReadProperties(RECORD_PROPERTIES);
        final JButton start = new JButton("Edit");
        final JComboBox choices = new JComboBox();

        for (CqAction a: record.getLegalActions()) {
            if (a.getType() != CqAction.Type.IMPORT
                && a.getType() != CqAction.Type.SUBMIT
                && a.getType() != CqAction.Type.BASE
                && a.getType() != CqAction.Type.RECORD_SCRIPT_ALIAS)
                choices.addItem(a);
        }
        
        final JButton add = new JButton("Attach");
        final JButton remove = new JButton("Detach");
        final ViewRecord.RecordFrame frame = 
            showRecord("View: ", record, choices.getItemCount()==0? null: 
                         new JComponent[]{choices, start, add, remove});

EditView.Viewer 还支持 AttachDetach 按钮,这两个按钮用于添加和除去附件字段的附件。

Attach 按钮向用户呈示了一个标准的 Swing 文件选择器对话框,允许用户选择要附加的文件。要向记录附加文件,可以使用 CqAttachment.doCreateAttachment(),该方法可以传递用户所选文件的名称。在调用此方法之前,必须构造一个寻找正确位置地址的 CqAttachment 代理。附件资源所在的文件夹是与其相连的字段的值 - 即当前在记录视图中选定的字段,因此可以从 ViewRecord.showRecord 返回的 frame 对象获取这个字段值。附件资源的唯一名称将通过当前时间生成。这个名称仅仅是个占位符,因为 doCreateAttachment() 可按自己需要自由地更改资源的名称。

add.addActionListener(new ActionListener(){
            /**
             * Prompts the user for the name of a file to be attached to
             * the selected field. If so provided, the content of the 
             * file is read into the ClearQuest database as an attachment.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                if (!ViewRecord.isAttachmentList(frame.m_fields, 
                                                 frame.m_table.getSelectedRow()))
                    return;

                JFileChooser chooser = new JFileChooser();

                if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
                 try {
                   String filename =  chooser.getSelectedFile().getAbsolutePath();
                   CqFieldValue field = frame.m_fields.get(frame.m_table.getSelectedRow());
                   StpLocation aLoc = (StpLocation) ((StpFolder)field.getValue()).location()
                                     .child("new" + System.currentTimeMillis());
                   CqAttachment attachment = record.cqProvider().cqAttachment(aLoc);
                       
                       attachment = attachment.doCreateAttachment(filename, 
                                                                  null, 
                                                                  CqProvider.DELIVER);
                       
                       JOptionPane.showMessageDialog(frame, 
                                                  "Added '" + filename + "' as " + attachment);
                       frame.dispose();
                       view(record);
                    } catch(Throwable t) 
                        { Utilities.exception(frame, "Add Attachment", t);}
                }}});

Detach 按钮使用 ViewRecord.selectAttachment 从用户获取将要以 CqAttachment 代理形式除去的附件标识。然后调用该代理的 doUnbindAll 方法,将附件从数据库中除去。

        remove.addActionListener(new ActionListener(){
            /**
             * Allows the user to select an attachment associated with
             * the selected field. If the user selects such an attachment
             * it is deleted.
             */
            public void actionPerformed(ActionEvent arg0)
            {
             if (!ViewRecord.isAttachmentList(frame.m_fields, frame.m_table.getSelectedRow()))
                 return;
                
                try {
                   CqAttachment attachment = ViewRecord.selectAttachment
                       (frame, frame.m_fields, frame.m_table.getSelectedRow(), "Remove");
                   
                   if (attachment != null) {
                       attachment.doUnbindAll(null);
                       frame.dispose();
                       view(record);
                   }
                } catch(Throwable t) 
                    { Utilities.exception(frame, "Remove Attachment", t);}
                }
            }
        );

Edit 按钮从组合框取回选中的 CqAction 代理,然后将该代理和当前 Viewer 的记录代理传递给 edit 方法,实际上这会启动记录的编辑。如果选中操作的 CqAction.Type 为 DUPLICATE,那么必须随该操作一起提供重复记录的标识。用户可以请求该值,并将其放在“操作”的自变量映射中,作为 original 自变量的值。

        start.addActionListener(new ActionListener(){
            /**
             * Starts editing of the record using the action currently
             * selected by the combo box on the view dialog.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                CqAction action = (CqAction)choices.getSelectedItem();
                
                try {
                    if (action.getType() == CqAction.Type.DUPLICATE) {
                        String id = JOptionPane.showInputDialog (frame, "Enter ID of duplicated record");
                        
                        if (id == null) return;
                        
                        action.argumentMap(new Hashtable<String>());
                        action.argumentMap().put("original", 
                                 			  record.cqProvider().cqRecord((StpLocation)record
                                                           .location().parent().child(id)));
                    }

                    edit(record, action);
                    frame.dispose();
                } catch (Exception ex) {
                    Utilities.exception(frame, "Duplicate Action", ex);
                }
            }
        });
        
        return frame;
    } catch (WvcmException ex){
        ex.printStackTrace();
    }
    
    return null;
}

在用户单击记录查看器中的 Edit 按钮时会调用 edit 方法。传递要编辑的记录的代理以及在这个要执行编辑的名下的操作代理。

请注意,用于启动 ClearQuest® 操作的“操作”代理无需定义任何属性 - 仅需定义位置和自变量映射(如果需要)。要形成操作的位置,需要知道要查找记录的 <name>、记录要用的 <record-type> 以及记录所在的 <database> 和 <db-set>。

然后,位置为 cq.action:<record-type>/<name>@<db-set>/<database>;例如,cq.action:Defect/Assign@7.0.0/SAMPL

使用新代理,调用 CqRecord.doWriteProperties 来开始编辑操作。 要编写的唯一属性是要编辑记录名下的操作。doWriteProperties 方法返回为编辑而打开的记录的代理,这个代理包含 RECORD_PROPERTIES PropertyRequest 中请求的属性。还可以在 doWriteProperties 返回后,使用 record.doReadProperties(具有相同的 PropertyRequest)获取属性,但这种方法的效率不高,因为针对同一个数据,这种方法要再次对存储库执行一次操作。

        public void edit(CqRecord selected, CqAction action)
        {
            try {
                CqRecord record = m_provider.cqRecord(selected.stpLocation());
        
                record =
                    (CqRecord) record.setAction(action)
                        .doWriteProperties(RECORD_PROPERTIES,
                                           CqProvider.HOLD);
                editRecord("Edit: ", record, selected);
            } catch (WvcmException ex){
                Utilities.exception(null, "Start Action", ex);
            }
        }

Viewer.editRecord 类似于 showRecord。主要差别在于,Viewer.editRecord 定义了 TableModel.isCellEditable 和 TableModel.setValue。

JFrame editRecord(String title, 
                                final CqRecord record, 
                                final CqRecord selected) throws WvcmException 
             {
        final JFrame frame = new JFrame(title + record.getUserFriendlyLocation());
        JPanel panel = new JPanel(new BorderLayout());
        JPanel buttons = new JPanel(new FlowLayout());
        final JButton show = new JButton("View");
        final JButton add = new JButton("Add");
        final JButton remove = new JButton("Remove");
        final JButton cancel = new JButton("Cancel");
        final StpProperty.List<CqFieldValue>> fields = record.getAllFieldValues();
        
        TableModel dataModel = new AbstractTableModel() {
        public int getColumnCount() { return fieldMetaProperties.length; }
        public int getRowCount() { return fields.size();}
        public String getColumnName(int col) 
             { return fieldMetaProperties[col].getRoot().getName(); }
        public Object getValueAt(int row, int col) 
            { 
                try {
                    return fields.get(row).getMetaProperty((MetaPropertyName<?>)
                                                            fieldMetaProperties[col].getRoot());
                } catch(Throwable ex) {
                    if (ex instanceof StpException) {
                        return ((StpException)ex).getStpReasonCode();  
                      } else {
                          String name = ex.getClass().getName();
                          return name.substring(name.lastIndexOf(".")+1);
                      }
                }
            }
        

TableModel.isCellEditable 仅对 VALUE 列返回 true,而且还需要该行属于 REQUIREDNESS 不为 READ_ONLY 的字段。

        public boolean isCellEditable(int row, int col) {
            if (fieldMetaProperties[col].getRoot().equals(StpProperty.VALUE)) {
                CqFieldValue field = fields.get(row);
                
                try {
                    return field.getRequiredness() != Requiredness.READ_ONLY;
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Field Requiredness", ex);
                }
            }
            
            return false;
        }

TableModel.setValueAt 可以将新字段值放入到与该显示相关的 CqRecord 代理中。首先,使用 initialize() 方法更新 CqFieldValue 结构。对于大多数类型的字段,该方法可以接受字符串表示,然后将字符串转换为相应的字段数据类型。

更新 CqFieldValue 之后,将其放入与该字段 PropertyName 关联的 CqRecord 代理中。 此步骤是必需的,以便在落实记录时将新的字段值写入数据库。若无此步骤,新字段值仍然仅保留在 ALL_FIELD_VALUES 属性的 CqFieldValue 对象中。ALL_FIELD_VALUES 属性为不可写,因此永远不会将更改写入数据库中。通过直接将修改过的 CqFieldValue 复制到字段的代理项中,此字段成为已更新的属性,在对代理执行下一个 do 方法时,会将更新过的值写入数据库。

        public void setValueAt(Object aValue, int row, int col)
        {
            if (fieldMetaProperties[col].getRoot().equals(StpProperty.VALUE)) {
                CqFieldValue<Object> field = fields.get(row);
                
                field.initialize(aValue);
                record.setFieldInfo(field.getFieldName(), field);
            }
        }

        private static final long serialVersionUID = 1L;
    };

    final JTable table = new JTable(dataModel);

    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    show.setEnabled(false);
    add.setEnabled(false);
    remove.setEnabled(false);
    
    // Ask to be notified of selection changes.
    ListSelectionModel rowSM = table.getSelectionModel();
    rowSM.addListSelectionListener(new ListSelectionListener() {
        /**
         * Enables the buttons in the dialog based on the type of
         * field selected in the table.
         */
        public void valueChanged(ListSelectionEvent e) {
            if (!e.getValueIsAdjusting()){
                int[] selected = table.getSelectedRows();
                show.setEnabled(false);
                add.setEnabled(false);
                remove.setEnabled(false);
                for (int i=0; i <selected.length; ++i)
                    if (ViewRecord.getRecordReferencedAt(fields, selected[i]) != null) {
                        show.setEnabled(true);
                    } else if (ViewRecord.isAttachmentList(fields, selected[i])) {
                        show.setEnabled(true);
                        add.setEnabled(true);
                        remove.setEnabled(true);
                    }
            }
        }
    });

View 按钮与 ViewRecord.Viewer 中的 View 按钮相同。

    buttons.add(show);
    show.addActionListener(new ActionListener(){
            /**
             * Invokes a view method on the value of the field selected
             * in the table.
             */
            public void actionPerformed(ActionEvent arg0)
            {
                int[] selected = table.getSelectedRows();
                
                for (int i =0; i < selected.length; ++i) {
                    int row = selected[i];
                    CqRecord record = ViewRecord.getRecordReferencedAt(fields, row);
                    
                    if (record != null) {
                        view(record);
                    } else if (ViewRecord.isAttachmentList(fields, row)) {
                        view(ViewRecord.selectAttachment(frame, fields, row, "View"));
                    }
                }
            }
        });

Deliver 按钮通过调用带有 CqProvider.DELIVER 选项的 doWriteProperties() 将已修改的记录落实到数据库。在交付成功后,将废弃这个 Viewer,然后针对原来的代理建立新的 Viewer。由于 view() 方法会重新从数据库读取属性,因此新视图会显示已更新的值。

    JButton deliver = new JButton("Deliver");

    buttons.add(deliver);
    deliver.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent arg0)
            {
                try {
                    int mode = frame.getDefaultCloseOperation();
                    
                    record.doWriteProperties(null, CqProvider.DELIVER);
                    frame.dispose();
                    view(selected).setDefaultCloseOperation(mode);
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Deliver failed", ex);
                }
            }
        });

Cancel 按钮使用记录代理的 doRevert 方法放弃编辑操作。 与使用 Deliver 按钮一样,这会关闭这个 Viewer,然后针对原来的代理创建新的 Viewer 实例。

    buttons.add(cancel);
    cancel.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent arg0)
            {
                try {
                    int mode = frame.getDefaultCloseOperation();
                    
                    record.doRevert(null);
                    frame.dispose();
                    
                    if (mode == JFrame.EXIT_ON_CLOSE)
                        System.exit(0);
                    
                    view(selected).setDefaultCloseOperation(mode);
                } catch (WvcmException ex) {
                    Utilities.exception(frame, "Cancel failed", ex);
                }
            }
        });

AttachDetach 按钮与 ViewRecord.Viewer 的按钮一样,不过交付顺序自变量为 HOLD,而非 DELIVER。这将延迟落实附件的添加或删除,直到通过 Deliver 按钮落实整条记录。

    buttons.add(add);
    add.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent arg0)
        {
            JFileChooser chooser = new JFileChooser();
            int returnVal = chooser.showOpenDialog(frame);

            if (returnVal == JFileChooser.APPROVE_OPTION) {
                try {
                   String filename = chooser.getSelectedFile().getAbsolutePath();
                   CqFieldValue field = fields.get(table.getSelectedRow());
                   StpLocation aLoc = (StpLocation) ((StpFolder)field.getValue()).location()
                                                                      .child("new" + System.currentTimeMillis());
                   CqAttachment attachment = record.cqProvider().cqAttachment(aLoc);
                   
                   attachment = attachment.doCreateAttachment(filename, 
                                                                       null, 
                                                                       CqProvider.HOLD);
                   
                   JOptionPane.showMessageDialog(frame, 
                                                      "Added '" + filename + "' as " + attachment 
                                                    + " (pending delivery)");
                } catch(Throwable t) 
                    { Utilities.exception(frame, "Add Attachment", t);}
            }}});
    
    buttons.add(remove);
    remove.addActionListener(new ActionListener(){
        public void actionPerformed(ActionEvent arg0)
        {
            try {
               CqAttachment attachment = ViewRecord.selectAttachment
                                                   (frame, fields, table.getSelectedRow(), "Remove");
               if (attachment != null)
                   attachment.doUnbindAll(null);
            } catch(Throwable t) 
                { Utilities.exception(frame, "Remove Attachment", t);}
            }
        }
    );
    
    panel.add(new JScrollPane(table), BorderLayout.CENTER);
    panel.add(buttons, BorderLayout.SOUTH);
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    frame.setContentPane(panel);
    frame.setBounds(300, 300, 600, 300);
    frame.setVisible(true);
    
    return frame;
}
在这个示例中,在用户选择 Deliver 按钮之前,已编辑的字段值会一直保存在客户机这里。用户修改的每个字段都会将记录代理中相应的属性标记为已更新。在交付操作的第一步中,这些已更新的属性会写入服务器。这个方法的问题在于最终交付之前不会检查新字段值,如果这些值不适合于正在使用的模式,那么可能会导致失败。

在每次修改字段时,GUI 会调用 record.doWriteProperties,这使用户能立即得到反馈,但是根据客户机和服务器之间的通信协议,这种方法的效率可能极低。 GUI 还可以提供 Update 按钮,这个按钮可以将所有累积的更新写入服务器,而无需实际交付这些更新。这样模式可以检查这些值并返回错误报告。这些修改将留给读者练习,采用哪种方法取决于您打算怎么使用这个应用程序。

课程检查点

现在,您已经了解了如何利用 Rational CM API 来开发客户机应用程序 - 对用户数据库中的记录执行一些操作。 本教程的最后一个课程阐述了如何创建新记录。
在本课程中,您学习到了以下内容:
  • 利用 Rational CM API,通过客户机应用程序对用户数据库中的资源执行一些操作。
  • 如何利用 Rational CM API,通过客户机应用程序创建这个产品所特有的一些操作。
< 上一课 | 下一课 >

反馈