Search code examples
javamultithreadingmodel-view-controlleruser-interfaceobserver-pattern

How to update my table in my MVC pattern program


I have created a program using a version of MVC architecture. The purpose of the code is to scrape the h1 titles of a list of webpages and to return the results to a JTable.

So far I have the program working fine. It's returning the results just as I want them to but it doesn't update the table until the very end. I want it to update the table as the results come in. I want to do this in a way that take best practice principals into account as I am just learning.

I presume to get this to update as I want it I will have to change my code around a bit. I'm not sure of the best way to update the GUI dynamically(threads, observers, something else?). I'm not even sure if the question "where in my MVC pattern should this code sit?" makes sense?

Anyways here is my View:

public class SearchView extends JFrame{
//Components
private JLabel selectElementLabel = new JLabel("Element Selector:");
private JTextField selectElement = new JTextField("h1");;
private JComboBox<String> selectLocale; 

private DefaultTableModel tableModel = new DefaultTableModel();
private JTable resultTable = new JTable(tableModel);

private JLabel statusLabel;
private JButton runButton = new JButton("Run");
private JButton clearButton = new JButton("Clear");

private SearchModel s_model;

//Constructor
public SearchView(SearchModel model) {
    //Set the Logic here(model)
    s_model = model;    

    //Initialise Components here(model)
    selectLocale = new JComboBox<>(s_model.getLocales());
    selectLocale.setSelectedIndex(13);

    //Layout Components
    JPanel userInputPanel = new JPanel();
    userInputPanel.setLayout(new BoxLayout(userInputPanel, BoxLayout.X_AXIS));
    userInputPanel.add(selectElementLabel);
    userInputPanel.add(selectElement);
    userInputPanel.add(selectLocale);

    tableModel.addColumn("Page");
    tableModel.addColumn("Data");
    resultTable.setFillsViewportHeight(true);

    JScrollPane resultScroller = new JScrollPane(resultTable);
    resultScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
    resultScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    resultScroller.setAlignmentX(Component.LEFT_ALIGNMENT);

    JPanel controlButtons = new JPanel();
    controlButtons.setLayout(new FlowLayout(FlowLayout.RIGHT));
    controlButtons.add(statusLabel = new JLabel(s_model.getState()));
    controlButtons.add(clearButton);
    controlButtons.add(runButton);


    this.setTitle("Element Searcher");
    this.add(BorderLayout.NORTH, userInputPanel);
    this.add(BorderLayout.CENTER, resultScroller);
    this.add(BorderLayout.SOUTH, controlButtons);
    this.setExtendedState(Frame.MAXIMIZED_BOTH); 
    this.setMinimumSize(new Dimension(900, 600));
    this.setVisible(true);  
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);        
}

void reset(){
    tableModel.setRowCount(0);
}

String getSelectedElement(){
    return selectElement.getText();
}

String getSelectedLocale(){
    return selectLocale.getSelectedItem().toString();
}

void setResults(Object[] result){
    tableModel.addRow(result);
}

void addRunListener(ActionListener run){
    runButton.addActionListener(run);
}

void addClearListerner(ActionListener clear){
    clearButton.addActionListener(clear);
}

}

Controller:

public class SearchController {

private SearchModel s_model;
private SearchView s_view;

public SearchController(SearchModel model, SearchView view) {
    s_model = model;
    s_view = view;

    s_view.addRunListener(new RunListener());
    s_view.addClearListerner(new ClearListener());

}

class RunListener implements ActionListener{
    public void actionPerformed(ActionEvent e){
        String selectedLocale = null;
        try {
            selectedLocale = s_view.getSelectedLocale();
            s_model.setPageList(selectedLocale);
            for (String pageUrl : s_model.getPageList()){
                s_view.setResults(s_model.getResults(pageUrl));
            }
        } catch (Exception e1) {
            System.out.println(e1);
        }
    }
}

class ClearListener implements ActionListener{
    public void actionPerformed(ActionEvent e){
        s_model.reset();
        s_view.reset();
    }
}

}

and finally my model:

public class SearchModel {
//Constants
private static final String[] localeStrings = { "cs-cz", "da-dk", "de-at", "de-ch", "de-de", "el-gr", "en-ae", "en-au", "en-ca", "en-gb", "en-ie", "en-in", "en-nz", "en-us", "en-za", "es-cl", "es-co", "es-es", "es-mx", "fi-fi", "fr-be", "fr-ca", "fr-ch", "fr-fr", "hu-hu", "it-it", "ja-jp", "ko-kr", "nb-no", "nl-be", "nl-nl", "pl-pl", "pt-br", "pt-pt", "ru-ru", "sk-sk", "sv-se", "zh-hk", "zh-sg", "zh-tw" };
private static final String INITIAL_STATE = "idle";
private HashSet<String> pageList;
private Object[] scrapeResult;
private String locale = "en-us";

//Search State
private String searchState;

public SearchModel() {
    reset();
}

public void setPageList(String loc){
    locale = loc;
    ScrapeXML scraper = new ScrapeXML(locale);
    pageList = scraper.getUrls();
}

public void setResults(String page){
    ScrapeElements scraper = new ScrapeElements(page, locale);
    scrapeResult = scraper.getResults();
}

public void reset(){
    searchState = INITIAL_STATE;
}

public String[] getLocales(){
    return localeStrings;
}

public String getState(){
    return searchState;
}

public HashSet<String> getPageList(){
    return pageList;
}

public Object[] getResults(String page){
    setResults(page);
    return scrapeResult;
}

}

If you have any comments or suggestions for the code itself please let me know!

Thanks!


Solution

  • You cannot really make it MVC without double-dispatch, which is also known as the Listener Pattern. To the model, you need to add

     public void addListener(ModelListener listener) {
     }
    

    and (so you can stop listening to old models when "your" the model gets changed)

     public void removeListener(ModelListener listener) {
     }
    

    so you can have almost-unknown objects adding themselves to receive model updates, which are typically passed through the "Listener" interface.

     public interface ModelListener {
    
       public void modelChanged(ModelChangeEvent event);
    
     }
    

    where a ModelChangeEvent is typically something like

     public class ModelChangeEvent {
    
       private Model source;
    
       public ModelChangeEvent(Model source, <possibly other fields here>) {
         this.source = source;
       }
    
       public Model getSource() {
         return source;
       }
     }
    

    With this, a listener can act like so

     public ModelView implements ModelListener {
    
      ... stuff ...
    
       public void modelChanged(ModelChangeEvent event) {
         if (event.getSource() == model) {
           // this is my model!
           name = model.getName();
           age = model.getAge();
           ... and any other updates that this listener cares about ...
         }
       }
    

    there is some flexibility in how the listener does within the change handler, and the most important part is that flexibility is contained in scope to the listener, the model now can basically ignore what is listening, just concentrating on who is listening

      ... in the model class ...
    
      private void notifyListeners() {
        ModelChangeEvent event = new ModelChangeEvent(this);
        for (ModelListener listener : listeners) {
          listener.modelChanged(event);
        }
      }
    

    Again, there is a large degree of flexibility in how you notify listeners, but the key element is that all listening objects should receive a call when the "elements" of the model that are observable change. The reason it's typically put in a private method is because it makes it much easier to reuse the model, like so

      ... in the model class ...
      public void setName(String name) {
        this.name = name;
        notifyListeners();
      }
    

    and in each Listening class, it may (or may not) read the name out depending on what it is displaying.

      ... in a name and age sensitive listening class ...
       public void modelChanged(ModelChangeEvent event) {
         if (event.getSource() == model) {
           name = model.getName();
           age = model.getAge();
         }
       }
    
    
      ... in a name insensitive listening class ...
       public void modelChanged(ModelChangeEvent event) {
         if (event.getSource() == model) {
           // this is my model!
           age = model.getAge();
         }
       }
    

    Once you have something like this in place, your View should listen to your Model, so the controller doesn't have to do some sort of polling of your model's observable "elements" combined with periodic refresh of the View's presentational items of interest.

    Yes, this means that your view often has copies of the data in your model, which is a good thing; because, if you decide that you need to modify the presentation, you have a copy of the data to modify. In a "name displaying" view, you might perhaps want to enforce consistent capitalization, like so

      ... in a name and age sensitive listening class ...
      ... note that _name_ is the name in the _view_ not the model! ...
       public void modelChanged(ModelChangeEvent event) {
         if (event.getSource() == model) {
           name = capitalize(model.getName());
           age = model.getAge();
         }
       }
    

    Now that your models and views will always be in sync. This greatly simplifies the controller code, now it just needs to process commands. For each command it is likely to do one (or more) of the following:

    1. Call a method in the model (the view will autoupdate itself).
    2. Lookup and switch a view's model (like a button for "next customer", don't destroy the view, just set the view's model). This requires writing a setModel(Model) method in the View.
    3. Create or destroy views (possibly setting their models).