Работа с несколькими пользовательскими моделями таблиц, избегая повторяющегося кода

Я работаю в проекте, в котором у нас есть несколько классов предметной области для моделирования бизнес-данных. Эти классы - простые POJO, и я должен отобразить несколько таблиц, используя их. Например, рассмотрим этот класс:

public class Customer {

    private Long id;
    private Date entryDate;
    private String name;
    private String address;
    private String phoneNumber;

    public Customer(Long id, Date entryDate, String name, String address, String phoneNumber) {
        this.id = id;
        this.entryDate = entryDate;
        this.nombre = name;
        this.domicilio = address;
        this.telefono = phoneNumber;
    }

    // Getters and setters here
}

Затем я создал свою собственную табличную модель, расширяющуюся от AbstractTableModel, чтобы работать непосредственно с Customer учебный класс:

public class CustomerTableModel extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<Customer> customers;

    public CustomerTableModel() {
        String[] header = new String[] {
            "Entry date",
            "Name",
            "Address",
            "Phone number"
        };
        this.columnNames = Arrays.asList(header);
        this.customers = new ArrayList<>();
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getCustomer(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getCustomer(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }

    @Override
    public int getRowCount() {
        return this.customers.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addCustomer(Customer customer) {
        int rowIndex = this.customers.size();
        this.customers.add(customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addCustomers(List<Customer> customerList) {
        if (!customerList.isEmpty()) {
            int firstRow = this.customers.size();
            this.customers.addAll(customerList);
            int lastRow = this.customers.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertCustomer(Customer customer, int rowIndex) {
        this.customers.add(rowIndex, customer);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteCustomer(int rowIndex) {
        if (this.customers.remove(this.customers.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public Customer getCustomer(int rowIndex) {
        return this.customers.get(rowIndex);
    }

    public List<Customer> getCustomers() {
        return Collections.unmodifiableList(this.customers);
    }

    public void clearTableModelData() {
        if (!this.customers.isEmpty()) {
            int lastRow = customers.size() - 1;
            this.customers.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

До сих пор все просто отлично. Однако у этого подхода есть как минимум две проблемы:

  1. Поскольку мне нужно реализовать одну табличную модель для каждого класса, то я сгенерирую много повторяющегося кода, чтобы по существу сделать три вещи: определить соответствующий заголовок таблицы, добавить / удалить объекты в / из базовой структуры (списка), переопределить оба setValueAt() а также getValueAt() методы для работы с пользовательскими объектами.

  2. Допустим, у меня есть один и тот же список клиентов, но я должен представить его в двух разных таблицах с разными заголовками или данными. Я должен был бы создать подкласс для своей модели таблицы и переопределить все, что нужно переопределить, чтобы выполнить это требование. Это не выглядит элегантно на всех.

Вопрос: Есть ли какой-нибудь способ избавиться от стандартного кода, делающего мою табличную модель гибкой и пригодной для повторного использования?

2 ответа

Решение

Как и другие модели Swing (то есть: DefaultComboBoxModel, DefaultListModel), мы можем использовать Generics для создания гибкой и многократно используемой табличной модели, также предоставляя API для работы с определенными пользователем POJO.

Эта настольная модель будет иметь следующие особенности:

  • Это простирается от AbstractTableModel воспользоваться обработкой событий табличной модели.
  • В отличие от CustomerTableModel как показано выше, эта табличная модель должна быть абстрактной, поскольку она не должна переопределять getValueAt() Метод: просто потому, что мы не знаем, какой класс или тип данных будет обрабатывать эта табличная модель, задача переопределить вышеупомянутый метод оставлена ​​подклассам.
  • Наследует пустой setValueAt() реализация от AbstractTableModel, Это имеет смысл, потому что isCellEditable() также наследуется от этого класса и всегда возвращает false,
  • Реализация по умолчанию getColumnClass() также наследуется и всегда возвращает Object.class,

Эти особенности делают эту модель таблицы действительно простой в реализации в зависимости от наших требований:

  • Если нам нужно отобразить таблицу только для чтения, то мы должны переопределить 2 метода top: getValueAt() а также getColumnClass() (последний рекомендуется, но не обязателен).
  • Если наша таблица должна быть редактируемой, то мы должны переопределить 4 метода top: два, упомянутых выше, плюс isCellEditable() а также setValueAt(),

Давайте посмотрим на код нашей табличной модели:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.table.AbstractTableModel;

/**
 * Abstract base class which extends from {@code AbstractTableModel} and 
 * provides an API to work with user-defined POJO's as table rows. Subclasses 
 * extending from {@code DataObjectTableModel} must implement 
 * {@code getValueAt(row, column)} method. 
 * <p />
 * By default cells are not editable. If those have to be editable then 
 * subclasses must override both {@code isCellEditable(row, column)} and 
 * {@code setValueAt(row, column)} methods.
 * <p />
 * Finally, it is not mandatory but highly recommended to override 
 * {@code getColumnClass(column)} method, in order to return the appropriate 
 * column class: default implementation always returns {@code Object.class}.
 * 
 * @param <T> The class handled by this TableModel.
 * @author dic19
 */
public abstract class DataObjectTableModel<T> extends AbstractTableModel {

    private final List<String> columnNames;
    private final List<T> data;

    public DataObjectTableModel() {
        this.data = new ArrayList<>();
        this.columnNames = new ArrayList<>();
    }

    public DataObjectTableModel(List<String> columnIdentifiers) {
        this();
        if (columnIdentifiers != null) {
            this.columnNames.addAll(columnIdentifiers);
        }
    }

    @Override
    public int getRowCount() {
        return this.data.size();
    }

    @Override
    public int getColumnCount() {
        return this.columnNames.size();
    }

    @Override
    public String getColumnName(int columnIndex) {
        return this.columnNames.get(columnIndex);
    }

    public void setColumnNames(List<String> columnNames) {
        if (columnNames != null) {
            this.columnNames.clear();
            this.columnNames.addAll(columnNames);
            fireTableStructureChanged();
        }
    }

    public List<String> getColumnNames() {
        return Collections.unmodifiableList(this.columnNames);
    }

    public void addDataObject(T dataObject) {
        int rowIndex = this.data.size();
        this.data.add(dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void addDataObjects(List<T> dataObjects) {
        if (!dataObjects.isEmpty()) {
            int firstRow = data.size();
            this.data.addAll(dataObjects);
            int lastRow = data.size() - 1;
            fireTableRowsInserted(firstRow, lastRow);
        }
    }

    public void insertDataObject(T dataObject, int rowIndex) {
        this.data.add(rowIndex, dataObject);
        fireTableRowsInserted(rowIndex, rowIndex);
    }

    public void deleteDataObject(int rowIndex) {
        if (this.data.remove(this.data.get(rowIndex))) {
            fireTableRowsDeleted(rowIndex, rowIndex);
        }
    }

    public void notifyDataObjectUpdated(T domainObject) {
        T[] elements = (T[])data.toArray();
        for (int i = 0; i < elements.length; i++) {
            if(elements[i] == domainObject) {
                fireTableRowsUpdated(i, i);
            }
        }
    }

    public T getDataObject(int rowIndex) {
        return this.data.get(rowIndex);
    }

    public List<T> getDataObjects(int firstRow, int lastRow) {
        List<T> subList = this.data.subList(firstRow, lastRow);
        return Collections.unmodifiableList(subList);
    }

    public List<T> getDataObjects() {
        return Collections.unmodifiableList(this.data);
    }

    public void clearTableModelData() {
        if (!this.data.isEmpty()) {
            int lastRow = data.size() - 1;
            this.data.clear();
            fireTableRowsDeleted(0, lastRow);
        }
    }
}

Итак, взяв эту модель стола и Customer класс, полная реализация будет выглядеть так:

String[] header = new String[] {"Entry date", "Name", "Address", "Phone number"};
DataObjectTableModel<Customer> model = new DataObjectTableModel<>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return Date.class;
            case 1: return String.class;
            case 2: return String.class;
            case 3: return String.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Customer customer = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return customer.getEntryDate();
            case 1: return customer.getName();
            case 2: return customer.getAddress();
            case 3: return customer.getPhoneNumber();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex < 0 || columnIndex >= getColumnCount()) {
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        } else {
            Customer customer = getDataObject(rowIndex);
            switch (columnIndex) {
                case 0: customer.setEntryDate((Date)aValue); break;
                case 1: customer.setName((String)aValue); break;
                case 2: customer.setAddress((String)aValue); break;
                case 3: customer.setPhoneNumber((String)aValue); break;
            }
            fireTableCellUpdated(rowIndex, columnIndex);
        }
    }
};

Как мы видим, в нескольких строках кода (LOC < 50) мы имеем полную реализацию.


Работает ли это с сущностями JPA?

Это происходит до тех пор, пока у сущностей есть публичные методы получения и установки. В отличие от реализаций JPA, эта табличная модель не работает с отражением, поэтому нам придется обращаться к свойствам объекта, используя открытый интерфейс класса для реализации getValueAt() а также setValueAt() методы.

Это работает с JDBC?

Нет, это не так. Мы должны были бы обернуть наборы результатов в классы домена и использовать интерфейс класса, как упомянуто выше.

Работает ли это с классами Java по умолчанию?

Да, это так. Еще раз, используя класс "предложенный интерфейс. Например, давайте возьмем java.io.File класс, мы могли бы иметь следующую реализацию модели таблицы:

String[] header = new String[] {
    "Name",
    "Full path",
    "Last modified",
    "Read",
    "Write",
    "Execute",
    "Hidden",
    "Directory"
};

DataObjectTableModel<File> model = new DataObjectTableModel<File>(Arrays.asList(header)) {
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
            case 0: return String.class;
            case 1: return String.class;
            case 2: return Date.class;
            case 3: return Boolean.class;
            case 4: return Boolean.class;
            case 5: return Boolean.class;
            case 6: return Boolean.class;
            case 7: return Boolean.class;
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        File file = getDataObject(rowIndex);
        switch (columnIndex) {
            case 0: return file.getName();
            case 1: return file.getAbsolutePath();
            case 2: return new Date(file.lastModified());
            case 3: return file.canRead();
            case 4: return file.canWrite();
            case 5: return file.canExecute();
            case 6: return file.isHidden();
            case 7: return file.isDirectory();
                default: throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    }
};

В том же ключе, что и в ответе dic19, вы можете использовать модель таблицы строк, которая также использует универсальные шаблоны и предоставляет множество общих методов, которые позволяют динамически обновлять Table Model.

Вам также потребуется реализовать несколько методов, поскольку модель также абстрактна. JButtonTableModel.java код показывает, как вы могли бы сделать это.

Кроме того, если вы хотите стать действительно модным, вы можете посмотреть на Bean Table Model (ссылка находится в приведенном выше блоге), которая расширяет RowTableModel, Эта модель использует отражение для построения Table Model, поэтому вам не нужно реализовывать пользовательскую модель.

Другие вопросы по тегам