Validace hodnot v JTable

Při práci s daty pomocí Swingovské komponenty JTable je občas potřeba kontrolovat data, která jsou pomocí ní do tabulky ukládána a pokud vkládaná hodnota není v pořádku, uživatele včas upozornit. Pokud jste již s JTable někdy pracovali, určitě víte, že to není nic snadného.

Pro snadnou práci s tabulkami rozděluje Swing podle principů MVC práci s tabulkou na komponentu, která ji zobrazuje (JTable) a na její model (TableModel). Ten se stará o správu dat a jejich zpřístupnění komponentě JTable a také má na starosti změnu dat v případě, že uživatel provede editaci pomocí JTable. Toto předávání dat od uživatele se děje pomocí metody setValueAt.

Tento způsob práce s tabulkami v každém případě práci zjednodušuje, zdá se ale, že na některé možné problémy autoři pozapomněli, nebo se rozhodli je neřešit. Tím hlavním, který nás při práci na Guidovi v poslední době trápil byla kontrola dat zadávaných do tabulky. Pokud je správáná libovolná hodnota, kterou je možné v editoru zadat, je vše v pořádku a není nutné nic řešit. Jak ale zařídit, aby do tabulky šlo zadat (téměř) libovolný řetězec, který ale musí být různý od všech ostatních hodnot ve stejném sloupci?

Standardní – a podle mě i správné z hlediska návrhu aplikace – by bylo provádět takovéto kontroly v modelu tabulky. Tím by byla zajištěna funkčnost kontrol automaticky ve všech komponentách, které s modelem pracují bez dalších starostí programátora. Možností jak takové chování zajistit je několik:

  • setValueAt bude vracet boolean podle toho, jestli hodnotu přijala, nebo ne. Pokud hodnotu odmítne, JTable snadno pozná, že zadaná hodnota nebyla správná a nechá uživatele ji opravit.
  • setValueAt bude v případě nesprávné hodnoty vyhazovat výjimku speciálního typu, kterou JTable odchytí a zareaguje na ni. Toto řešení bych považoval za nejvhodnější, protože nejlépe odpovídá principům programování v jazyce Java.
  • TableModel bude obsahovat metodu typu
    boolean isValid(Object value, int row, int column)

    která bude kontrolovat, zda hodnota, kterou uživatel zadává do tabulky je platná. V takovém případě ale stejně bude nutné provádět dodatečné kontroly i v setValueAt a tím je toto řešení nejméně vhodné.

Ve skutečnosti ale není použito ani jedno z nich, “návratová hodnota” setValueAt je typu void a podle definice interface TableModel nesmí vyhazovat výjimky (mimo RuntimeException a odvozených, které není nutné deklarovat v hlavičce metody) – a ani jejich výskyt JTable neošetřuje.

Kontroly v modelu

Pokud chce programátor z modelu tabulky zabránit ukončení editace a nastavení hodnoty, stačí uvnitř setValueAt vyhodit výjimku odvozenou od RuntimeException. Ta projde kódem JTable bez zachycení a defacto ukončí zpracování události, při které byla setValueAt volána. Díky tomu se hodnota do tabulky nenastaví a editor zůstane aktivní. Zatím jsme si nevšimli, že by tímto způsobem docházelo k nějakým defektům v modelu nebo v JTable – ale co není, může být.

Určitou vadou na kráse je stacktrace výjimky vypsaný na konzoli, o který se postará EventDispatchThread a který jednak může zbytečně děsit neinformvané uživatele a navíc působí dost neprofesionálně. Druhou nepříjemnou vlastností je to, že na editoru není žádným způsobem poznat, že došlo k chybě a hodnota nebyla tabulkou přijata. Všimněte si, že pokud používáte standardní editor pro editaci sloupčeku s číselnými hodnotami a zadáte nesmyslnou hodnotu, tak se kolem editoru objeví červený rámeček signalizující, že něco není dobře. A všimněte si také, že tímto způsobem žádný rámeček nevznikne.

Kontroly v editoru

Pátrání po původu červeného rámčeku mě dovedlo až do zdrojáku samotné JTable. Autoři od DefaultCellEditoru odvodili speciální editory postavené na textovém editačním poli, které umožńují editovat libovolnou hodnotu, pokud její třída obsahuje konstruktor vytvářející nové hodnoty z řetězců. Pokud novou hodnotu vytvořit nelze (při vytváření dojde k výjimce), nastaví si editor červený rámeček a pokračuje dál, jako kdyby se nic nestalo.

Ukazuje se, že jediný způsob, jak dosáhnout podobného efektu je vytvoření vlastního TableCellEditoru, který bude provádět kontroly před ukončením editace podobně, jako to dělá generický editor od autorů JTable při konverzi z řetězce. Tento generický editor bohužel je v privátní vnořená třída v JTable.java a tak ho není možné recyklovat a vylepšit o další kontroly. Naštěstí množství změn není tak velké, aby podle nich nešlo napsat vlastní editor fungující stejným způsobem.

Protože se jedná o obecný princip, je samotná kontrola z editoru vydělená do objektu implementujícího následující interfce. Metoda isTableCellValueValid vrací true, pokud je hodnota v zadaném sloupci a řádku přijatelná, jinak vrátí false.

public interface TableCellValidator {
    boolean isTableCellValueValid(
                        Object value, int row, int column);
}

Editor je upravený tak, aby při ukončení editace zkonvertovanou hodnotu předložil validátoru ke kontrole a dále jí předával jen po odslouhlasení.

public class ValidatingTableCellEditor
                            extends DefaultCellEditor {
    private Object value;
    private int editedColumn;
    private int editedRow;
    private TableCellValidator validator;

    // ... redakčně kráceno ...

    public Component getTableCellEditorComponent(
                JTable table, Object value,
                boolean isSelected, int row, int column) {
        this.value = value;
        this.editedColumn = column;
        this.editedRow = row;
        // nastav výchozí (černou) barvu rámečku 
        ((JComponent)getComponent()).setBorder(
                    new LineBorder(Color.BLACK));
        // ...
        return super.getTableCellEditorComponent(
                            table, value, isSelected,
                            row, column);
    }

    public boolean stopCellEditing() {
        String str_val = (String)super.getCellEditorValue();
        Object val = null;

        // ... zkovertuj str_val na val ...
        if(validator != null
                && !validator.isTableCellValueValid(
                            val, editedRow, editedColumn)) {
            validationFailed();
            return false;
        }
        this.value = val;
        return super.stopCellEditing();
    }

    protected void validationFailed() {
        ((JComponent)getComponent()).setBorder(
                    new LineBorder(Color.RED));
    }
}

Takové řešení má velkou výhodu v tom, že uživatel díky červenému orámování jasně vidí, že zadaná hodnota nebyla přijata a také nejsou vypisovány žádné chybové hlášky na konzoli. Výrazná nevýhoda tohoto přístupu naopak je v tom, že je nutné se explicitně postarat o to, aby se editor provádějící kontroly používal správným způsobem ve všech tabulkových komponentách, které s modelem pracují, nehledě na to, že hodnoty do tabulky nemusí být vždy zadávané přes JTable a jeji cell editor.

Správné řešení (ve smyslu nejlepší, které znám) je zkombinovat oba dva přístupy a uživatele blokovat jak při editaci pomocí editoru, tak, pokud si nenechá říct, na něj vyhběhnout s RuntimeException a on už si to rád rozmyslí. Pokud bude obojí provedeno dost důsledně, bude výsledkem jak konzistentní uživatelské rozhraní, tak bezpečné zázemí objektů pro správu dat.

Poud byste měli zájem o zdrojové kódy editoru, který používáme, napiště mi na ondrasejATcentrumDOTcz a domluvíme se ;-)

Bookmark and Share

Comments are closed.