Skip to content
Michael Karneim edited this page Oct 22, 2018 · 33 revisions

What is Beanfabrics?

Beanfabrics is a framework for creating graphical user interfaces (GUI) for Java desktop applications following the presentation model (PM) pattern introduced by Martin Fowler in 2004. According to Fowler the presentation model "represents the state and behavior of the presentation independently of the GUI controls used in the interface".

Requirements

To compile and run programs with Beanfabrics you need Java 7 (or a higher version) and the Beanfabrics library in the classpath.

What are the main features of Beanfabrics?

Beanfabrics provides

  • a set of ready-to-use presentation model components
  • a set of ready-to-use visual presentation view components
  • a framework to create custom model and view components

Beanfabrics

  • has a mechanism for binding visual components dynamically to specific nodes of the presentation model
  • is supported by some GUI-Designer tools.

How does it work?

Using Beanfabrics in an application architecture leads to the division of the presentation tier into two separate layers: the presentation view and the presentation model:

These layers have the following responsibilities:

The Presentation View

The Presentation View is formed by classes that establish the application's appearance. These are basic GUI controls that are combined into View components. A View component is a mediator between a set of visual component (e.g. a Swing JTextField or a SWT Text) and an applicable presentation model component (e.g. a TextPM). It attaches itself as a change listener to both sides and synchronizes their state. By implementing the View interface a view component declares it's appropriate model type and must implement accessor methods for setting and getting it.

Beanfabrics includes a simple framework for creating custom view components from scratch or by combination of existing ones. The package org.beanfabrics.swing provides a palette of basic view components for nearly all Swing components. And org.beanfabrics.swt is an experimental package with a small set of view components for SWT controls.

Examples of view components:

PresentationViewComponents.png

The Presentation Model (PM)

The Presentation Model lays as a link layer between the presentation tier and the application tier. It is a tree-like combination of presentation model components that represent the state of your application's user interface, in a manner that is independent from a concrete GUI framework like Swing or SWT. By separating the presentation model from the presentation view you can concentrate on implementing the application's user interface logic without caring too much about layout, events, and other GUI API details.

A PM component's job is to map the application data to its presentational form, as well as to handle the user input. It performs input validation and and is responsible for calling functions of the application tier. Each PM component encapsulates a specific detail of the application state. This includes application data, a validation state and - depending on the type - other typical attributes like "editable" and "mandatory". It provides methods for accessing, converting, formatting, and validating it's content. It informs listeners about state changes by firing events like the PropertyChangeEvents or other. By implementing the PresentationModel any class can become a PM component and can be combined with others to form a complex presentation model.

Beanfabrics comes with a set of prefabricated PM components.

Some examples of PM components:

PresentationModelComponents.png
  • the TextPM holds String data
  • the IntegerPM holds an integer value with conversions to int, long, BigInteger, etc
  • the DatePM holds a Java Date
  • the IListPM is a dynamic sequence of other PM components
  • the MapPM is a dynamic dictionary that maps custom key objects to PM components

Feature Demos

The following section contains some small examples that demonstrate Beanfabrics features in action.

A Hello world program

This is the Beanfabrics "Hello world" program. It demonstrates the cooperation of the presentation view and the presentation model.

---!!!--- TODO include Helloworld.png ---!!!---
package wiki.helloworld;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JDialog;
import javax.swing.JPanel;

import org.beanfabrics.model.TextPM;
import org.beanfabrics.swing.BnTextField;

public class HelloWorld {
    public static void main(String[] args) {

        // Compose GUI components
        BnTextField textfield = new BnTextField();
        textfield.setColumns(15);
        JPanel panel = new JPanel();
        panel.add(textfield);
        JDialog dlg = new JDialog();
        dlg.add(panel);

        // Create the presentation model
        TextPM mytext = new TextPM();
        mytext.setText("Hello, world!");

        // Define the binding between view and presentation model
        textfield.setPresentationModel(mytext); 

        // Show GUI
        dlg.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        dlg.setSize(200, 100);
        dlg.setLocationRelativeTo(null);
        dlg.setVisible(true);

    }
}

Static binding vs. dynamic binding

In the class above the GUI compontent (textfield) is statically bound to a presentation model (mytext) by using the setPresentationModel(...) method. The following snippet shows the relevant code:

// Define the binding between view and presentation model
textfield.setPresentationModel(mytext); 

Alternatively, you can bind the textfield dynamically to it's presentation model by using a ModelProvider and a Path:

// Create a model provider for dynamically binding views to models    
ModelProvider modelProvider = new ModelProvider();                  

//Define the binding between view and presentation model               
textfield.setModelProvider(modelProvider); 
textfield.setPath(new Path("this")); 

// Make the model available now                                       
modelProvider.setPresentationModel(mytext);                         

The significant difference between static and dynamic binding is the time of model assignment. Static binding assigns the model to the view exactly at that time when the setPresentationModel(...) method is called. Dynamic binding observes a specified Path. It is evaluated whenever a node changes. The view is assigned to whatever model the path points to, which can be any time after the binding has been configured.

So what are the benefits of dynamic binding?

  • you can easily exchange the model without touching all bound GUI compontents manually, just by calling aProvider.setPresentationModel( aNewModel) again.
  • you can bind a GUI component to any subnode of a given presentation model by specifying the relative Path from the root PM to the subnode.

The following modification of the previous example demonstrates how to dynamically bind a view component by using a relative Path:

// Create the presentation model                                      
class WelcomeDialogPM extends AbstractPM {
    TextPM message = new TextPM();

    public WelcomeDialogPM() {
        PMManager.setup(this);
    }
}

WelcomeDialogPM rootModel = new WelcomeDialogPM();
rootModel.message.setText("Hello, world!");

// Create a model provider for dynamically binding views to models    
ModelProvider modelProvider = new ModelProvider();

//Define the binding between view and presentation model               
textfield.setModelProvider(modelProvider);
textfield.setPath(new Path("this.message"));

// Make the model available now                                       
modelProvider.setPresentationModel(rootModel);

Creating an editable table

This sample shows how to create a table and a corresponding presentation model.

---!!!--- TODO include ContactBrowserDialog.png ---!!!---
package wiki.contact;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JDialog;
import javax.swing.JScrollPane;

import org.beanfabrics.ModelProvider;
import org.beanfabrics.Path;
import org.beanfabrics.model.AbstractPM;
import org.beanfabrics.model.ListPM;
import org.beanfabrics.model.PMManager;
import org.beanfabrics.model.TextPM;
import org.beanfabrics.swing.table.BnColumnBuilder;
import org.beanfabrics.swing.table.BnTable;

public class ContactBrowserDialog extends JDialog {

    public static void main(String[] args) {

        // Create GUI and show it
        ContactBrowserDialog dlg = new ContactBrowserDialog();
        dlg.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        dlg.setSize(300, 300);
        dlg.setLocationRelativeTo(null);
        dlg.setVisible(true);
    }

    public ContactBrowserDialog() {
        // Compose GUI
        BnTable table = new BnTable();
        JScrollPane scrollpane = new JScrollPane(table);
        getContentPane().add(scrollpane);

        // Create the model
        ContactBrowserPM model = new ContactBrowserPM();

        // Bind GUI to model
        ModelProvider localModelProvider = new ModelProvider();
        table.setModelProvider(localModelProvider);
        table.setPath(new Path("this"));
        table.setColumns(new BnColumnBuilder() // configure the columns
                .addColumn().withPath("name").withName("Name") // <- 1st column
                .addColumn().withPath("email").withName("Email") // <- 2nd column
                .build());

        // Populate model with some data
        model.loadData();

        // Make the model available now
        localModelProvider.setPresentationModel(model);
    }

    class ContactBrowserPM extends ListPM<ContactPM> {
        public ContactBrowserPM() {
            PMManager.setup(this);
        }

        public void loadData() {
            add(new ContactPM("Mickey Mouse", "mickey@disney.com"));
            add(new ContactPM("Donald Duck", "donald@disney.com"));
            add(new ContactPM("Goofy", "goofy@disney.com"));
        }
    }

    class ContactPM extends AbstractPM {
        TextPM name = new TextPM();
        TextPM email = new TextPM();

        public ContactPM(String aName, String aEmail) {
            PMManager.setup(this);
            name.setText(aName);
            email.setText(aEmail);
        }
    }

}

The code above illustrates that you can bind the BnTable component to a presentation model of the type IListPM. The element type of this ListPM is ContactPM, which is used as the row model for this table. For each element of the list a row is created in the table. You can configure the visible table structure by calling addColumn(Path pathToRowProperty, String columnHeader) for each column you are interested in.

Creating a Master-Detail-View

This sample shows how to create a Master-Detail-View with a table as the master view and a text area as the detail view. The text area shows some information about the row that is selected in the table.

---!!!--- TODO include EmailBrowserFrame.png ---!!!---
package wiki.email;

import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;

import org.beanfabrics.ModelProvider;
import org.beanfabrics.Path;
import org.beanfabrics.model.AbstractPM;
import org.beanfabrics.model.DatePM;
import org.beanfabrics.model.ListPM;
import org.beanfabrics.model.PMManager;
import org.beanfabrics.model.TextPM;
import org.beanfabrics.support.OnChange;
import org.beanfabrics.support.PropertySupport;
import org.beanfabrics.swing.BnTextArea;
import org.beanfabrics.swing.table.BnColumn;
import org.beanfabrics.swing.table.BnTable;

public class EmailBrowserFrame extends JFrame {

    public static void main(String[] args) {
        Locale.setDefault(Locale.US);

        // Create GUI and show it
        EventQueue.invokeLater( new Runnable() {

            public void run() {
                // Create GUI and show it
                EmailBrowserFrame frame = new EmailBrowserFrame();
                frame.addWindowListener( new WindowAdapter() {
                    public void windowClosing(WindowEvent e) {
                        System.exit(0);
                    }
                });
                frame.setSize(500, 400);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public EmailBrowserFrame() {
        // Compose GUI
        BnTable table = new BnTable();
        table.setCellEditingAllowed(false); // disable editing for this GUI element

        JSplitPane splitpane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        splitpane.setDividerLocation(100);
        splitpane.setTopComponent( new JScrollPane(table));

        BnTextArea textarea = new BnTextArea();
        splitpane.setBottomComponent( new JScrollPane(textarea));

        getContentPane().add(splitpane);

        // Bind GUI to model
        EmailBrowserPM model = new EmailBrowserPM();
        // This is a mediator between GUI and model
        ModelProvider provider = new ModelProvider(); 
        // This loads the model into the provider
        provider.setPresentationModel(model); 

        // This binds the table to the provider
        table.setModelProvider(provider); 
        // This bind the table to the model relative to the provider's root node
        table.setPath( new Path("this")); 
        table.addColumn( new BnColumn( new Path("from"),"From"));
        table.addColumn( new BnColumn( new Path("subject"),"Subject"));
        table.addColumn( new BnColumn( new Path("receivedDate"),"Received"));

        textarea.setModelProvider(provider);
        textarea.setPath( new Path("selectedEmail.content"));

        // Populate model with some data
        model.loadData();

    }

    class EmailBrowserPM extends ListPM<EmailPM> {

        EmailPM selectedEmail;

        public EmailBrowserPM() {
            PMManager.setup(this);
        }

        public void loadData() {
            add( new EmailPM("donald@disney.com", "Happy Birthday!", 
                 new Date(), "Hi dude,\nI wish you a happy birthday!"));
            add( new EmailPM("mickey@disney.com", "Invitation to your birthday party", 
                 new Date(), "Hello my friend,\nI'm sorry I can't make it to your party."));
            add( new EmailPM("invoice@shopping.xyz", "Invoice No 324321", 
                 new Date(), 
                 "Dear customer,\nThank your for your order.\nEnclosed you find the invoice."));
        }

        @OnChange // <- adds this method as a change event handler to this object
        void updateSelectedEmail() {
            selectedEmail = getSelection().getFirst();
            PropertySupport.get(this).refresh(); // <- fires a change event for "selectedEmail"
        }
    }

    class EmailPM extends AbstractPM {
        TextPM from = new TextPM();
        TextPM subject = new TextPM();
        DatePM receivedDate = new DatePM();
        TextPM content = new TextPM();

        public EmailPM(String aFrom, String aSubject, Date aReceivedDate, String aContent) {
            from.setText(aFrom);
            subject.setText(aSubject);
            receivedDate.setFormat( DateFormat.getDateTimeInstance(
              DateFormat.MEDIUM, DateFormat.SHORT
            ));
            receivedDate.setDate( aReceivedDate);
            content.setText(aContent);
            PMManager.setup(this);
        }
    }
}

@OnChange Annotation

The master-detail-functionality is implemented mainly by the @OnChange annotation. By attaching it to any method of your presentation model you instruct the PMManager to call it whenever a model change event happens, including selection changes of the IListPM. When updateSelectedEmail is called, it gets the first selected list element and assigns it to the selectedEmail property, which is part of the binding path of the text area. In order to inform the text area about the new reference we just call PropertySupport.get(this).refresh() which checks all properties of the model for changes, and in case, fires an change event. This makes sure that the text area contents will be updated with the contents of the selected email.

Creating an input form with validation

This sample shows how to add some validation rules to a presentation model.

---!!!--- TODO include EmailComposerDialog.png ---!!!---

The screenshot shows a dialog for composing an email. The "Send" button is disabled because the "Recipients" field is not valid. In this case it is invalid because the value is empty but mandatory.

package wiki.email;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.StringTokenizer;

import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import net.miginfocom.swing.MigLayout;

import org.beanfabrics.ModelProvider;
import org.beanfabrics.Path;
import org.beanfabrics.model.AbstractPM;
import org.beanfabrics.model.OperationPM;
import org.beanfabrics.model.PMManager;
import org.beanfabrics.model.TextPM;
import org.beanfabrics.support.Operation;
import org.beanfabrics.support.Validation;
import org.beanfabrics.swing.BnButton;
import org.beanfabrics.swing.BnTextArea;
import org.beanfabrics.swing.BnTextField;

public class EmailComposerDialog extends JDialog {

    public static void main(String[] args) {

        // Create GUI and show it
        EventQueue.invokeLater( new Runnable() {
            public void run() {
                EmailComposerDialog dlg = new EmailComposerDialog();
                dlg.addWindowListener( new WindowAdapter() {
                    public void windowClosing(WindowEvent e) {
                        System.exit(0);
                    }
                });
                dlg.setSize(400, 400);
                dlg.setLocationRelativeTo(null);
                dlg.setVisible(true);
            }
        });

    }

    public EmailComposerDialog() {
        // Compose GUI
        setTitle("Compose New Email Message");

        ModelProvider provider = new ModelProvider();
        BnTextField tfRecipients = new BnTextField(provider);
        BnTextField tfSubject = new BnTextField(provider);
        BnTextArea taContent = new BnTextArea(provider);
        taContent.setWrapStyleWord(true);
        taContent.setLineWrap(true);
        BnButton btnSend = new BnButton(provider);
        btnSend.setText("Send");

        JPanel panel = new JPanel( 
          new MigLayout("", "[fill][grow,fill]", "[][][grow,fill][]")
        );
        panel.add( new JLabel("Recipients"));
        panel.add(tfRecipients, "wrap");
        panel.add( new JLabel("Subject"));
        panel.add(tfSubject,"wrap");
        panel.add( new JScrollPane(taContent), "span");
        panel.add( btnSend, "wrap" );
        getContentPane().add(panel, BorderLayout.CENTER);

        // Bind GUI to model
        EmailComposerPM model = new EmailComposerPM();
        provider.setPresentationModel(model);

        tfRecipients.setPath(new Path("recipients"));
        tfSubject.setPath(new Path("subject"));
        taContent.setPath(new Path("content"));
        btnSend.setPath( new Path("send"));
    }

    class EmailComposerPM extends AbstractPM {
        RecipientsPM recipients = new RecipientsPM();
        TextPM subject = new TextPM();
        TextPM content = new TextPM();
        OperationPM send = new OperationPM();

        public EmailComposerPM() {
            recipients.setMandatory(true);
            PMManager.setup(this);
        }

        // This adds this method as a execution method to the "send" operation
        @Operation 
        public void send() {
            // TODO replace dummy impl. with the real thing
            System.out.println("Sending email to: "+recipients.getText());
            System.out.println("Subject: "+subject.getText());
            System.out.println("Content: "+content.getText());
        }
        // This adds this method as validation rule to the "send" operation
        @Validation(path="send") 
        boolean canSend() {
            return isValid();
        }
    }

    class RecipientsPM extends TextPM {
        public RecipientsPM() {
            PMManager.setup(this);
        }

        @Validation // <- adds this method as validation rule to this model
        boolean containsValidListOfEmailAddresses() {
            StringTokenizer st = new StringTokenizer(getText(),",;");
            while( st.hasMoreTokens()) {
                String token = st.nextToken();
                if ( isValidEmailAddress(token)==false) {
                    return false;
                }
            }
            return true;
        }

        private boolean isValidEmailAddress(String text) {
            // replace this with more accurate checks
            return ( text.contains("@"));
        }
    }
}

Validation is a standard feature of Beanfabrics. Any presentation model can have a set of validation rules that define the model's validation state when evaluated. The evaluation occurs, when aModel.revalidate() is invoked.

Beanfabrics invokes the validation automatically:

  • when this model or one of it's subnodes fires a change event
  • when this model is a subnode of a 'parent' model and when one of it's siblings fires a change event.

How to configure a validation rule?

For standard cases there are preconfigured validation rules available. For example:

recipients.setMandatory(true);

adds a validation rule to the "recipients" node that checks if the value is not empty.

Other preconfigured rules can be obtained by choosing a certain model class like IntegerPM, that uses a number format for validation, and DatePM, that uses a date format.

Of course you also can add custom valudation rules to any model. The rules are evaluated in the order they have been added to the model. When a rule indicates that the model is invalid, the evaluation stops.

To add a custom validation rule you can either

  • call aModel.getValidator().add( newRule) with newRule conforming to the ValidationRule interface
  • or use the @Validation annotation.

By tagging any boolean method with @Validation you instruct Beanfabrics to add it as a validation rule to the model, whereby returning "false" means that the model is invalid.

The code above contains two examples:

  1. "canSend" is a validation rule that contributes to the validation state of the "send" operation.
  2. "containsValidListOfEmailAddresses" contributes to the validation state of the RecipientsPM.

See Also

  • The tutorial provides a step-by-step tutorial for using the Beanfabrics framework.