Decoupling GUI and Business Logic

My GUIs suck. They are hard to use and are a nightmare behind the scenes. In particular, I find it hard to separate the business logic from the GUI code. However I managed to divide these parts in a recent project into 100% disjoint namespaces. Let's see how.

Talking to Clojure

GUI code is all about handling state. So we have to cope with that. Luckily Clojure gives us a variety of tools here. I decided to use atoms for my little project.

The state is modified by the user via UI interaction. So we need a way to change the atom once some interaction happens. This can be easily done via the huge listener machinery of Swing. Here is an example of a button modifying the atom.

(defn process
  [stuff canceled?]
  (doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
    (process-item x)))

(defn some-click-handler
  [evt]
  (let [stuff     (get-eg.-selected-stuff-from-gui)
        canceled? (atom false)
        cancel    (JButton. "Cancel")
        ...]
    (add-action-listener
      cancel
      (fn [_]
       (reset! canceled? true)))
    ...
    (future (process stuff canceled?))))

add-action-listener is a small helper from clojure.contrib.swing-utils. Otherwise the idea should be pretty straight-forward. The future call is necessary, so that the processing doesn't run on the Swing Event Dispatch Thread.

We first retrieve some stuff we want to process from our GUI. Then we set up a Dialog with a cancel button and fire off the processing. Pressing the cancel button should stop the processing of our input data. In order for this to work, our processing function has to take the atom carrying the state of whether the computation should be stopped. And in fact it has to check the atom from time to time. Here we do this after every item. A bit hacky but worked for me. Beware chunked seqs, though.

Showing Progress

So far this is not very exciting. To keep the user entertained let's add a progress bar to our dialog.

(defn process
  [stuff canceled? progress]
  (doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
    (process-item x)
    (do-swing
      (.setValue progress (inc (.getValue progress))))))

(defn some-click-handler
  [evt]
  (let [stuff     (get-eg.-selected-stuff-from-gui)
        canceled? (atom false)
        cancel    (JButton. "Cancel")
        progress  (JProgressBar. 0 (count stuff))
        ...]
    (add-action-listener
      cancel
      (fn [_]
       (reset! canceled? true)))
    ...
    (future (process stuff canceled? progress))))

Again, we have to pass the progress bar down to our processing function in order to be able to update it after an item was processed.

But wait! Now our processing function has to know, what a progress bar is! And it has to know, that updating a progress bar has to be done on the Swing EDT. What the heck is an EDT? Things like do-swing from clojure.contrib.swing-utils help a bit. But why should our processing function be bothered with such stuff. Again GUI stuff leaked into my logic. Bleh.

Talking to the GUI

But Clojure to the rescue: we can simply fix the situation using a feature of Clojure's reference types: a watch. We can wire-up the state of the progress bar with a Clojure atom. Updating the atom will then also update the progress bar. That way the processing function does not have to know how the GUI works. It just updates the atom.

(defn process
  [stuff canceled? progress]
  (doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
    (process-item x)
    (swap! progress inc)))

(defn some-click-handler
  [evt]
  (let [stuff      (get-eg.-selected-stuff-from-gui)
        canceled?  (atom false)
        cancel     (JButton. "Cancel")
        progress   (atom 0)
        bar        (JProgressBar. 0 (count stuff))
        ...]
    (add-action-listener
      cancel
      (fn [_]
       (reset! canceled? true)))
    (add-watch
      progress
      ::update-progress-bar
      (fn [_ _ _ new-val]
        (do-swing
          (.setValue bar new-val))))
    ...
    (future
      (process stuff canceled? progress))))

Ah. Much better the progress bar update code moved back to the other GUI stuff again. And the processing function is independent of the GUI code again.

Upshot

Using watches, we were able to completely separate the GUI code from the business logic. Neither is dependent on the implementation of the other.

Appendix

Here a full example which shows the described technique.

(ns example.gui
  (:import
    javax.swing.JButton
    javax.swing.JFrame
    javax.swing.JProgressBar
    com.jgoodies.forms.layout.CellConstraints
    com.jgoodies.forms.layout.FormLayout
    com.jgoodies.forms.factories.ButtonBarFactory
    com.jgoodies.forms.builder.PanelBuilder)
  (:use
    [clojure.contrib.swing-utils
     :only (do-swing do-swing* add-action-listener)]))

(defn processing
  [items done? canceled? progress]
  (doseq [item (take-while (fn [_] (not @canceled?)) items)]
    (println item)
    (Thread/sleep 5000)
    (swap! progress inc))
  (reset! done? true))

(defn startup
  []
  (let [items     (take 5 (iterate inc 0))
        frame     (JFrame. "Example GUI")
        layout    (FormLayout. "fill:default:grow" "pref,3dlu,pref")
        cc        (CellConstraints.)
        progress  (atom 0)
        pbar      (JProgressBar. @progress (count items))
        done?     (atom false)
        done      (JButton. "Done")
        canceled? (atom false)
        cancel    (JButton. "Cancel")
        bbar      (ButtonBarFactory/buildCenteredBar (into-array [done cancel]))
        panel     (-> (PanelBuilder. layout)
                    (doto
                      (.setDefaultDialogBorder)
                      (.add pbar (.xy cc 1 1))
                      (.add bbar (.xy cc 1 3)))
                    .getPanel)]
    ; Wire up the done button.
    (.setEnabled done false)
    (add-action-listener
      done
      (fn [_]
        (do-swing
          (doto frame
            (.setVisible false)
            (.dispose)))))
    (add-watch
      done?
      ::toggle-buttons-on-done
      (fn [_ _ _ done?]
        (do-swing
          (when done?
            (.setEnabled cancel false)
            (.setEnabled done true)))))
    ; Wire up cancel button.
    (add-action-listener
      cancel
      (fn [_]
        (reset! canceled? true)))
    ; Wire up progress bar.
    (add-watch
      progress
      ::update-progress-bar
      (fn [_ _ _ v]
        (do-swing
          (.setValue pbar v))))
    (doto frame
      (.setDefaultCloseOperation JFrame/DISPOSE_ON_CLOSE)
      (-> .getContentPane (.add panel))
      (.pack)
      (.setVisible true))
    (-> #(processing items done? canceled? progress) Thread. .start)))

(defn main
  []
  (do-swing* :now startup))

Published by Meikel Brandmeyer on .