Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Doing GUI's Properly - MVP for Dummies
#1
As a first proper post I'll post this tutorial I posted elsewhere. Enjoy this post, post, posting, posted, post.

Been a while since I've posted a tutorial on a site, so as a pre-warning I can ramble a bit, but I'll try to keep it concise and to the point.

Now why did I write this? Basically going through the few open source scripts, various bot sources, etc I've noticed that UI's are not that well designed and structured. If the developer wanted to refactor or redesign their UI they'd have to change more code than they should. This is where using a design pattern can really help.

What is MVP?
MVP or Model-View-Presenter is a UI design pattern designed to keep data and the view separate. Why is this useful? Well it allows you to change logic, data representation or how data is stored with minimal changes in the code making small and large projects easier to maintain. Sound useful yet? It also allows for testing, mainly unit testing. I won't cover unit testing because that's a whole other thing in itself, but undeniably useful, have a look here for some more info on it. But splitting your code into the Model, the View and the Presenter allows you to test how the data is stored and manipulating it separately. This makes it easier to maintain as you don't have to search through a clusterfuck of code to find the problem, if you change something you can locate it easier. So lets look at the different parts of the MVP model:
  • Model - The model is the data store. In a script for example you could think of this as the place where settings are stored.
  • View - The view is the actual UI, this would be where the user would input data.
  • Presenter - The presenter is the core of the design pattern. It manipulates both the model and the view; taking input from the view, manipulating it and putting it back into the model and taking data from the model, formatting it for representation in the view.
MVP works in a circular fashion. So inputting data into the View notifies the Presenter which manipulates the data and puts it in the Model. The Model then notifies the Presenter that it has changed and the Presenter updates the View.

So how do you implement it?
Being the upstanding scholar that I am, and the fact that I already have a base knocking around on my github, I've done some of the leg work for you. I'll break down each base class and try to explain how it works.

Model
Code:
package io.aiecee.mvp.model.base;

import java.util.LinkedList;
import java.util.List;

/**
* Date: 04/09/2014
* Time: 10:26
*
* @author Matt Collinge
*/
public abstract class ModelBase {

   private List<IModelChangedListener> listeners = new LinkedList<IModelChangedListener>();

   public void addModelChangedListener(IModelChangedListener listener) {
       listeners.add(listener);
   }

   public void removeModelChangedListener(IModelChangedListener listener) {
       listeners.remove(listener);
   }

   protected void onModelChanged(String variable) {
       ModelChangedEvent event = new ModelChangedEvent(this, variable);
       listeners.stream().forEach((listener) -> listener.onModelChanged(event));
   }

}
As previously mentioned the Model is what contains our data, to help the Presenter know when the Model has updated I've implemented a simple event system. In any setter after setting the value we would call onModelChanged and pass in the name of the variable we've just updated.

View
Code:
package io.aiecee.mvp.view.base;

import io.aiecee.mvp.presenter.base.PresenterBase;

/**
* Date: 04/09/2014
* Time: 10:40
*
* @author Matt Collinge
*/
public abstract class ViewBase<P extends PresenterBase> {

   private P presenter;

   public P getPresenter() {
       return presenter;
   }

   public void setPresenter(P presenter) {
       this.presenter = presenter;
   }

   public abstract void show();

   public abstract void hide();

   public abstract void dispose();

}
This is the View base class, our View should know about the presenter as it needs to send the data to it for entry into the model. I've also added 3 abstract methods show(), hide() and dispose() to help with controlling the view from where we are using it.

Presenter
Code:
package io.aiecee.mvp.presenter.base;

import io.aiecee.mvp.model.base.IModelChangedListener;
import io.aiecee.mvp.model.base.ModelBase;
import io.aiecee.mvp.view.base.ViewBase;

/**
* Date: 04/09/2014
* Time: 10:39
*
* @author Matt Collinge
*/
public abstract class PresenterBase<V extends ViewBase, M extends ModelBase> implements IModelChangedListener {

   private V view;
   private M model;

   public PresenterBase(V view, M model) {
       this.view = view;
       this.model = model;
       model.addModelChangedListener(this);
   }

   public V getView() {
       return view;
   }

   public M getModel() {
       return model;
   }

}
This is our Presenter base. It implements IModelChangedListener from the off so you don't have to. It knows about both the View and the Model.

Putting this to use!
Using this framework I've made a very simple UI to show you how it works. It's based around user info where you enter in first name, last name and the age and this is shown as full name and age elsewhere in the UI. It's again made of 3 parts, the User, the UserView and the UserPresenter.

User
Code:
package io.aiecee.mvp.model.impl;

import io.aiecee.mvp.model.base.ModelBase;

/**
* Date: 04/09/2014
* Time: 10:34
*
* @author Matt Collinge
*/
public class User extends ModelBase {

   private String firstName, lastName;
   int age;

   public User(String firstName, String lastName, int age) {
       this.firstName = firstName;
       this.lastName = lastName;
       this.age = age;
   }

   public User() {
       firstName = "";
       lastName = "";
       age = 0;
   }

   public String getFirstName() {
       return firstName;
   }

   public void setFirstName(String firstName) {
       this.firstName = firstName;
       onModelChanged("firstName");
   }

   public String getLastName() {
       return lastName;
   }

   public void setLastName(String lastName) {
       this.lastName = lastName;
       onModelChanged("lastName");
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
       onModelChanged("age");
   }

}
As you can see the User extends our ModelBase class and it's just a very simple data structure with no logic in it. Logic can be added, but really most of the data manipulation should be done in the presenter. VERY IMPORTANT to note that in the setters onModelChanged is always called, this means the Presenter is notified when the User is updated.

UserView
Code:
package io.aiecee.mvp.view.impl;

import io.aiecee.mvp.presenter.impl.UserPresenter;
import io.aiecee.mvp.view.base.ViewBase;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

/**
* Date: 04/09/2014
* Time: 10:53
*
* @author Matt Collinge
*/
public class UserView extends ViewBase<UserPresenter> {

   private JFrame frame;

   // Setter
   private JTextField firstName, lastName;
   private JSpinner ageSpinner;

   // Getter
   private JLabel fullName;
   private JLabel ageLabel;

   public UserView() {
       frame = new JFrame("User Example");
       frame.setLayout(new BorderLayout());

       // Setter
       {

           JPanel setterPanel = new JPanel();
           setterPanel.setBorder(new TitledBorder("Setter"));
           setterPanel.setLayout(new BoxLayout(setterPanel, BoxLayout.Y_AXIS));

           firstName = new JTextField();
           firstName.setColumns(25);
           firstName.addKeyListener(new KeyListener() {
               @Override
               public void keyTyped(KeyEvent e) {
                   getPresenter().setFirstName(((JTextField) e.getSource()).getText());
               }

               @Override
               public void keyPressed(KeyEvent e) {
               }

               @Override
               public void keyReleased(KeyEvent e) {
               }
           });
           setterPanel.add(firstName);

           lastName = new JTextField();
           lastName.setColumns(25);
           lastName.addKeyListener(new KeyListener() {
               @Override
               public void keyTyped(KeyEvent e) {
                   getPresenter().setLastName(((JTextField) e.getSource()).getText());
               }

               @Override
               public void keyPressed(KeyEvent e) {
               }

               @Override
               public void keyReleased(KeyEvent e) {
               }
           });
           setterPanel.add(lastName);

           ageSpinner = new JSpinner();
           ageSpinner.setValue(0);
           ageSpinner.addChangeListener(e -> getPresenter().setAge((Integer) ((JSpinner) e.getSource()).getValue()));
           setterPanel.add(ageSpinner);

           frame.add(setterPanel, BorderLayout.LINE_START);

       }

       // Getter
       {

           JPanel getterPanel = new JPanel();
           getterPanel.setBorder(new TitledBorder("Getter"));
           getterPanel.setLayout(new BoxLayout(getterPanel, BoxLayout.Y_AXIS));

           fullName = new JLabel();
           getterPanel.add(fullName);

           ageLabel = new JLabel();
           getterPanel.add(ageLabel);

           frame.add(getterPanel, BorderLayout.LINE_END);

       }
   }

   public void setFullName(String fullName) {
       this.fullName.setText(fullName);
   }

   public void setAge(int age) {
       this.ageLabel.setText(age + "");
   }

   @Override
   public void show() {
       frame.pack();
       frame.setVisible(true);
   }

   @Override
   public void hide() {
       frame.setVisible(false);
   }

   @Override
   public void dispose() {
       frame.dispose();
   }
}
The UserView extends our View base class so it knows about the correct Presenter. As you can see all it basically does is listen for user input and relay this back to the Presenter as well as have two methods (setFullName and setAge) to set how the data to be shown in the UI.

UserPresenter
Code:
package io.aiecee.mvp.presenter.impl;

import io.aiecee.mvp.model.base.ModelChangedEvent;
import io.aiecee.mvp.model.impl.User;
import io.aiecee.mvp.presenter.base.PresenterBase;
import io.aiecee.mvp.view.impl.UserView;

/**
* Date: 04/09/2014
* Time: 10:53
*
* @author Matt Collinge
*/
public class UserPresenter extends PresenterBase<UserView, User> {

   public UserPresenter() {
       super(new UserView(), new User());
       getView().setPresenter(this);
   }

   @Override
   public void onModelChanged(ModelChangedEvent event) {
       switch (event.getVariableName().toLowerCase()) {
           case "firstname":
           case "lastname":
               updateFullName();
               break;
           case "age":
               updateAge();
       }
   }

   public void setFirstName(String firstName) {
       getModel().setFirstName(firstName);
   }

   public void setLastName(String lastName) {
       getModel().setLastName(lastName);
   }

   public void setAge(int age) {
       getModel().setAge(age);
   }

   private void updateFullName() {
       String fullName = getModel().getFirstName() + " " + getModel().getLastName();
       getView().setFullName(fullName);
   }

   private void updateAge() {
       getView().setAge(getModel().getAge());
   }

}
This is the UserPresenter and extends our Presenter base. As you can see the set methods are a direct route from the View to the Model, you can add some logic in here if needed, but in such a simple case it's not. In the update methods you can see there is logic to combine the first and last names of the user into the full name before setting the value in the View.

Conclusion
This about wraps up the tutorial, we've made a nice MVP framework base that can be expanded on and implemented a very simple application using it. This can be used in scripts to help setup the script by using the Model as a kind of settings store, you can also make the Model serialisable so it can be re-loaded next run.

The whole project can be found here for you to read over at your leisure. Any problems with it let me know and I'll update it. I'll add an FAQ of any questions if anyone has any at the bottom of the post.
Reply
#2
Very good tutorial! Extremely in depth and explains pretty much everything needed, good job!
Reply
 


Forum Jump:


Users browsing this thread: 1 Guest(s)

About The Bytecode Club

We're a community focused on Reverse Engineering, we try to target Java/Android but we also include other langauges/platforms. We pride ourselves in supporting and free and open sourced applications.

Website