< 上一個課程 | 下一個課程 >

修改記錄

在這一課,您將學習如何修改記錄的欄位值,及新增或移除附件。
這一課的程式碼範例可讓使用者更新她所檢視的記錄。 這個範例透過延伸 ViewRecord.Viewer 類別,支援一個選取動作的組合框及另一個對記錄起始所選取動作的編輯按鈕,來延伸前一個模組的 ViewRecord 範例。 這個 Viewer 的 editRecord 方法,會針對已開啟要更新的記錄來編排其檢視及更新。 這些新的 GUI 元件利用 showRecord 的 future 引數,建立在 ViewRecord.Viewer 顯示畫面中。

除了 ALL_FIELD_VALUES 內容之外,現在 view(CqRecord) 方法還可以讀取該記錄的 LEGAL_ACTIONS 內容。 LEGAL_ACTIONS 內容的值是動作的 Action Proxy 清單,這些動作可合法套用至現行狀態的記錄。 此清單是用來移入組合框控制項,使用者在按一下編輯按鈕之前,可從中選取動作。 此範例不支援某些類型的動作,例如 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 也支援附加分開按鈕,以新增及移除附件欄位的附件檔。

附加按鈕向使用者呈現標準 Swing 檔案選擇器對話框,讓使用者可以選取要附加的檔案。 為了將檔案附加至記錄,會使用 CqAttachment.doCreateAttachment(),並將使用者選取的檔案名稱傳給它。 在呼叫此方法之前,必須先建構定址至適當位置的 CqAttachment Proxy。 即將存放附件資源的資料夾,即是要附加之目標的欄位值 - 這是目前在記錄視圖中選取的欄位,因此它是從 ViewRecord.showRecord 傳回的頁框物件中取得的。 附件資源的唯一名稱是從當天的現行時間產生的。 此名稱只是一個位置保留元,因為 doCreateAttachment() 可能隨時變更資源名稱以符合其需求。

add.addActionListener(new ActionListener(){
            /**
             * 提示使用者輸入要附加至所選取欄位的檔案名稱。
             * 如果有提供,
             * 則檔案的內容會以附件讀入 ClearQuest 資料庫中。
             */
            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);}
                }}});

分開按鈕利用 ViewRecord.selectAttachment,從使用者取得要以 CqAttachment Proxy 形式移除之附件的身分。然後,會呼叫這個 Proxy 的 doUnbindAll 方法,從資料庫中移除該附件。

        remove.addActionListener(new ActionListener(){
            /**
             * 可讓使用者選取與所選欄位相關聯的附件。
             * 如果使用者選取此附件,
             * 則會刪除它。
             */
            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);}
                }
            }
        );

編輯按鈕從組合框提取所選取的 CqAction Proxy,並將它及現行檢視器的記錄 Proxy 傳遞至編輯方法,該方法會實際起始記錄的編輯。 如果所選取動作的 CqAction.Type 是 DUPLICATE,則必須在動作中提供所複製記錄的 ID。 此值是向使用者要求來的,並放在 Action 的引數對映中,作為 original 引數的值。

        start.addActionListener(new ActionListener(){
            /**
             * 使用檢視對話框上的組合框目前選取的動作,
             * 開始編輯記錄。
             */
            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;
}

當使用者按一下記錄檢視器中的編輯按鈕時,會呼叫編輯方法。 會傳給它所要編輯記錄的 Proxy 及要執行編輯的動作的 Proxy。

請注意,用來啟動 ClearQuest® 動作的 Action Proxy 不需要定義任何內容 - 只需要定義其位置及其引數對映(必要的話)。若要形成動作的位置,您需要知道它的 <name>、它要使用的記錄的 <record-type> 及記錄所在的<database> 和 <db-set>。

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

使用新的 Proxy,呼叫 CqRecord.doWriteProperties 來開始編輯作業。 要寫入的唯一內容是要編輯記錄的動作。 doWriteProperties 方法傳回已開啟要編輯之記錄的 Proxy,此 Proxy 將包含 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。主要差異在於它定義 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 Proxy 中設定新欄位值。首先,會使用其 initialize() 方法更新 CqFieldValue 結構。此方法接受大部分欄位類型的字串表示法,並且將字串轉換成適當的欄位資料類型。

在更新 CqFieldValue 之後,它會設定到與該欄位的 PropertyName 相關聯的 CqRecord Proxy 中。 此步驟是必要的,這樣當記錄確定時,新欄位值才能寫入資料庫中。 若缺少此步驟,新欄位值只會留在屬於 ALL_FIELD_VALUES 內容一部分的 CqFieldValue 物件中。 ALL_FIELD_VALUES 內容是不可寫入的,因此變更絕不會寫入資料庫中。 透過直接將修改過的 CqFieldValue 複製到該欄位的 Proxy 項目,該欄位即變成更新的內容,且更新的值將由 Proxy 中所執行的下一個 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);
    
    // 要求收到選擇變更的通知。
    ListSelectionModel rowSM = table.getSelectionModel();
    rowSM.addListSelectionListener(new ListSelectionListener() {
        /**
         * 根據表格中所選的欄位類型,
         * 在對話框中啟用按鈕。
         */
        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);
                    }
            }
        }
    });

檢視按鈕與 ViewRecord.Viewer 中的檢視按鈕一樣。

    buttons.add(show);
    show.addActionListener(new ActionListener(){
            /**
             * 對表格中所選取的欄位值呼叫檢視方法。
             *
             */
            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"));
                    }
                }
            }
        });

遞送按鈕會使用 CqProvider.DELIVER 選項呼叫 doWriteProperties(),在資料庫中確定已修改的記錄。在遞送成功之後,Viewer 會關閉,然後新的 Viewer 會在原始 Proxy 上啟動。 由於 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);
                }
            }
        });

取消按鈕使用記錄 Proxy 的 doRevert 方法來放棄編輯作業。 和遞送按鈕一樣,會關閉 Viewer,然後為原始 Proxy 實例化一個新的 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);
                }
            }
        });

附加分開按鈕與 ViewRecord.Viewer 的那些按鈕相同,只不過遞送順序引數是 HOLD 而不是 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;
}
在這個範例中,編輯的欄位值會保留在用戶端,直到使用者選取遞送按鈕為止。使用者已修改的每一個欄位會在記錄 Proxy 中標示對應內容為已更新。 這些更新的內容會寫入至伺服器,成為遞送作業的首要步驟。 此方法的問題在於要等到最後遞送時才會檢查新欄位值,如果這些值不適合所使用的綱目,就會失敗。

每次修改欄位時,GUI 可呼叫 record.doWriteProperties,以提供使用者即時回應,但也可能效率極差, 這端視用戶端及伺服器之間的通訊協定而定。 GUI 也可以提供更新按鈕,使所有累計更新寫入伺服器中,而非實際遞送它們。 這使綱目有機會檢查值並回報錯誤。 這些修改留給讀者,至於要採取什麼方法,則視此應用程式的預定用途而定。

課程檢查點

現在您已學會如何使用 Rational CM API 來開發用戶端應用程式動作,這些動作會對使用者資料庫中的記錄執行作業。此指導教學的最後一課將示範如何建立新記錄。
在這一課,您學到下列各項:
  • 關於使用 Rational CM API,從用戶端應用程式中,對使用者資料庫中的資源執行作業。
  • 如何使用 Rational CM API,從用戶端應用程式建立產品特定作業。

意見
< 上一個課程 | 下一個課程 >