L'un des modèles de conception que je trouve le plus difficile à saisir dans la "vraie vie Swing" est le modèle MVC. J'ai parcouru pas mal de messages sur ce site qui discutent du modèle, mais je ne pense toujours pas avoir une compréhension claire de la façon de tirer parti du modèle dans mon Java = Application Swing.
Disons que j'ai un JFrame qui contient un tableau, quelques champs de texte et quelques boutons. J'utiliserais probablement un TableModel pour "relier" la JTable avec un modèle de données sous-jacent. Cependant, toutes les fonctions responsables de l'effacement des champs, de la validation des champs, du verrouillage des champs ainsi que des actions des boutons iraient généralement directement dans le JFrame. Cependant, cela ne mélange-t-il pas le contrôleur et la vue du motif?
Pour autant que je puisse voir, j'arrive à obtenir le modèle MVC "correctement" implémenté en regardant la JTable (et le modèle), mais les choses deviennent boueuses quand je regarde l'ensemble du JFrame dans son ensemble.
J'aimerais vraiment savoir comment les autres s'y prennent à ce sujet. Comment procédez-vous lorsque vous devez afficher un tableau, quelques champs et quelques boutons pour un utilisateur utilisant le modèle MVC?
Un livre que je vous recommanderais fortement pour MVC en swing serait "Head First Design Patterns" par Freeman et Freeman. Ils ont une explication très complète de MVC.
Bref résumé
Vous êtes l'utilisateur - vous interagissez avec la vue. La vue est votre fenêtre sur le modèle. Lorsque vous faites quelque chose à la vue (comme cliquez sur le bouton Lecture), la vue indique au contrôleur ce que vous avez fait. C'est le travail du contrôleur de gérer cela.
Le contrôleur demande au modèle de changer son état. Le contrôleur prend vos actions et les interprète. Si vous cliquez sur un bouton, c'est le travail du contrôleur de comprendre ce que cela signifie et comment le modèle doit être manipulé en fonction de cette action.
Le contrôleur peut également demander à la vue de changer. Lorsque le contrôleur reçoit une action de la vue, il peut être nécessaire de dire à la vue de changer en conséquence . Par exemple, le contrôleur peut activer ou désactiver certains boutons ou éléments de menu dans l'interface.
Le modèle informe la vue lorsque son état a changé. Lorsque quelque chose change dans le modèle, en fonction d'une action que vous avez effectuée (comme cliquer sur un bouton) ou un autre changement interne (comme la prochaine chanson de la playlist a commencé), le modèle informe la vue que son état a changé.
La vue demande l'état au modèle. La vue obtient l'état qu'elle affiche directement à partir du modèle. Par exemple, lorsque le modèle notifie à la vue qu'une nouvelle chanson a commencé à jouer, la vue demande le nom de la chanson au modèle et l'affiche. La vue peut également demander l'état du modèle à la suite d'une demande de modification de la vue par le contrôleur.
Source (Au cas où vous vous demandez ce qu'est un "contrôleur crémeux", pensez à un cookie Oreo, le contrôleur étant le centre crémeux, la vue étant le biscuit supérieur et le modèle étant le biscuit inférieur) .)
Au cas où vous seriez intéressé, vous pouvez télécharger une chanson assez divertissante sur le modèle MVC depuis ici !
Un problème que vous pouvez rencontrer avec la programmation Swing implique la fusion des threads SwingWorker et EventDispatch avec le modèle MVC. Selon votre programme, il se peut que votre vue ou votre contrôleur doive étendre SwingWorker et remplacer la méthode doInBackground()
où la logique gourmande en ressources est placée. Cela peut être facilement fusionné avec le modèle MVC typique, et est typique des applications Swing.
EDIT # 1 :
De plus, il est important de considérer MVC comme une sorte de composite de différents modèles. Par exemple, votre modèle peut être implémenté à l'aide du modèle Observateur (nécessitant que la vue soit enregistrée en tant qu'observateur auprès du modèle) tandis que votre contrôleur peut utiliser le modèle Stratégie.
EDIT # 2 :
Je voudrais en outre répondre spécifiquement à votre question. Vous devez afficher vos boutons de table, etc. dans la vue, ce qui implémenterait évidemment un ActionListener. Dans votre méthode actionPerformed()
, vous détectez l'événement et l'envoyez à une méthode associée dans le contrôleur (rappelez-vous que la vue contient une référence au contrôleur). Ainsi, lorsqu'un bouton est cliqué, l'événement est détecté par la vue, envoyé à la méthode du contrôleur, le contrôleur peut demander directement à la vue de désactiver le bouton ou quelque chose. Ensuite, le contrôleur interagira avec le modèle et le modifiera (qui aura principalement des méthodes getter et setter, et quelques autres pour enregistrer et notifier les observateurs, etc.). Dès que le modèle est modifié, il appellera une mise à jour sur les observateurs inscrits (ce sera la vue dans votre cas). Par conséquent, la vue va maintenant se mettre à jour.
Je n'aime pas l'idée que la vue soit celle qui est notifiée par le modèle lorsque ses données changent. Je déléguerais cette fonctionnalité au contrôleur. Dans ce cas, si vous modifiez la logique d'application, vous n'avez pas besoin d'interférer avec le code de la vue. La tâche de la vue est uniquement pour les composants des applications + la mise en page rien de plus rien de moins. La mise en page en swing est déjà une tâche prolixe, pourquoi la laisser interférer avec la logique des applications?
Mon idée de MVC (avec laquelle je travaille actuellement, jusqu'ici tout va bien) est la suivante:
La vue :
Comme je l'ai dit, la création de la vue est déjà détaillée, alors créez votre propre implémentation :)
interface View{
JTextField getTxtFirstName();
JTextField getTxtLastName();
JTextField getTxtAddress();
}
Il est idéal pour interfacer les trois à des fins de testabilité. J'ai seulement fourni mon implémentation du modèle et du contrôleur.
Le modèle :
public class MyImplementationOfModel implements Model{
...
private SwingPropertyChangeSupport propChangeFirer;
private String address;
private String firstName;
private String lastName;
public MyImplementationOfModel() {
propChangeFirer = new SwingPropertyChangeSupport(this);
}
public void addListener(PropertyChangeListener prop) {
propChangeFirer.addPropertyChangeListener(prop);
}
public void setAddress(String address){
String oldVal = this.address;
this.address = address;
//after executing this, the controller will be notified that the new address has been set. Its then the controller's
//task to decide what to do when the address in the model has changed. Ideally, the controller will update the view about this
propChangeFirer.firePropertyChange("address", oldVal, address);
}
...
//some other setters for other properties & code for database interaction
...
}
Le controlle :
public class MyImplementationOfController implements PropertyChangeListener, Controller{
private View view;
private Model model;
public MyImplementationOfController(View view, Model model){
this.view = view;
this.model = model;
//register the controller as the listener of the model
this.model.addListener(this);
setUpViewEvents();
}
//code for setting the actions to be performed when the user interacts to the view.
private void setUpViewEvents(){
view.getBtnClear().setAction(new AbstractAction("Clear") {
@Override
public void actionPerformed(ActionEvent arg0) {
model.setFirstName("");
model.setLastName("");
model.setAddress("");
}
});
view.getBtnSave().setAction(new AbstractAction("Save") {
@Override
public void actionPerformed(ActionEvent arg0) {
...
//validate etc.
...
model.setFirstName(view.getTxtFName().getText());
model.setLastName(view.getTxtLName().getText());
model.setAddress(view.getTxtAddress().getText());
model.save();
}
});
}
public void propertyChange(PropertyChangeEvent evt){
String propName = evt.getPropertyName();
Object newVal = evt.getNewValue();
if("address".equalsIgnoreCase(propName)){
view.getTxtAddress().setText((String)newVal);
}
//else if property (name) that fired the change event is first name property
//else if property (name) that fired the change event is last name property
}
}
Le Main, où le MVC est installé:
public class Main{
public static void main(String[] args){
View view = new YourImplementationOfView();
Model model = new MyImplementationOfModel();
...
//create jframe
//frame.add(view.getUI());
...
//make sure the view and model is fully initialized before letting the controller control them.
Controller controller = new MyImplementationOfController(view, model);
...
//frame.setVisible(true);
...
}
}
Le modèle MVC est un modèle de la façon dont une interface utilisateur peut être structurée. Il définit donc les 3 éléments Modèle, Vue, Contrôleur:
Exemple
Lorsque vous cliquez sur Button
, il appelle ActionListener
. ActionListener
ne dépend que d'autres modèles. Il utilise certains modèles comme entrée et d'autres comme résultat ou sortie. C'est comme des arguments de méthode et des valeurs de retour. Les modèles avertissent l'interface utilisateur lorsqu'ils sont mis à jour. Il n'est donc pas nécessaire que la logique du contrôleur connaisse le composant ui. Les objets modèles ne connaissent pas l'interface utilisateur. La notification est effectuée par un modèle d'observateur. Ainsi, les objets du modèle savent seulement qu'il y a quelqu'un qui veut être averti si le modèle change.
Dans Java swing, il y a aussi des composants qui implémentent un modèle et un contrôleur. Par exemple javax.swing.Action . Il implémente un modèle ui (propriétés: enablement, petite icône, nom, etc.) et est un contrôleur car il étend ActionListener .
A explication détaillée, exemple d'application et code source : https://www.link-intersystems.com/blog/2013/07/ 20/le-modèle-mvc-implémenté-avec-swing-Java / .
Bases MVC en moins de 240 lignes:
public class Main {
public static void main(String[] args) {
JFrame mainFrame = new JFrame("MVC example");
mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
mainFrame.setSize(640, 300);
mainFrame.setLocationRelativeTo(null);
PersonService personService = new PersonServiceMock();
DefaultListModel searchResultListModel = new DefaultListModel();
DefaultListSelectionModel searchResultSelectionModel = new DefaultListSelectionModel();
searchResultSelectionModel
.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
Document searchInput = new PlainDocument();
PersonDetailsAction personDetailsAction = new PersonDetailsAction(
searchResultSelectionModel, searchResultListModel);
personDetailsAction.putValue(Action.NAME, "Person Details");
Action searchPersonAction = new SearchPersonAction(searchInput,
searchResultListModel, personService);
searchPersonAction.putValue(Action.NAME, "Search");
Container contentPane = mainFrame.getContentPane();
JPanel searchInputPanel = new JPanel();
searchInputPanel.setLayout(new BorderLayout());
JTextField searchField = new JTextField(searchInput, null, 0);
searchInputPanel.add(searchField, BorderLayout.CENTER);
searchField.addActionListener(searchPersonAction);
JButton searchButton = new JButton(searchPersonAction);
searchInputPanel.add(searchButton, BorderLayout.EAST);
JList searchResultList = new JList();
searchResultList.setModel(searchResultListModel);
searchResultList.setSelectionModel(searchResultSelectionModel);
JPanel searchResultPanel = new JPanel();
searchResultPanel.setLayout(new BorderLayout());
JScrollPane scrollableSearchResult = new JScrollPane(searchResultList);
searchResultPanel.add(scrollableSearchResult, BorderLayout.CENTER);
JPanel selectionOptionsPanel = new JPanel();
JButton showPersonDetailsButton = new JButton(personDetailsAction);
selectionOptionsPanel.add(showPersonDetailsButton);
contentPane.add(searchInputPanel, BorderLayout.NORTH);
contentPane.add(searchResultPanel, BorderLayout.CENTER);
contentPane.add(selectionOptionsPanel, BorderLayout.SOUTH);
mainFrame.setVisible(true);
}
}
class PersonDetailsAction extends AbstractAction {
private static final long serialVersionUID = -8816163868526676625L;
private ListSelectionModel personSelectionModel;
private DefaultListModel personListModel;
public PersonDetailsAction(ListSelectionModel personSelectionModel,
DefaultListModel personListModel) {
boolean unsupportedSelectionMode = personSelectionModel
.getSelectionMode() != ListSelectionModel.SINGLE_SELECTION;
if (unsupportedSelectionMode) {
throw new IllegalArgumentException(
"PersonDetailAction can only handle single list selections. "
+ "Please set the list selection mode to ListSelectionModel.SINGLE_SELECTION");
}
this.personSelectionModel = personSelectionModel;
this.personListModel = personListModel;
personSelectionModel
.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
ListSelectionModel listSelectionModel = (ListSelectionModel) e
.getSource();
updateEnablement(listSelectionModel);
}
});
updateEnablement(personSelectionModel);
}
public void actionPerformed(ActionEvent e) {
int selectionIndex = personSelectionModel.getMinSelectionIndex();
PersonElementModel personElementModel = (PersonElementModel) personListModel
.get(selectionIndex);
Person person = personElementModel.getPerson();
String personDetials = createPersonDetails(person);
JOptionPane.showMessageDialog(null, personDetials);
}
private String createPersonDetails(Person person) {
return person.getId() + ": " + person.getFirstName() + " "
+ person.getLastName();
}
private void updateEnablement(ListSelectionModel listSelectionModel) {
boolean emptySelection = listSelectionModel.isSelectionEmpty();
setEnabled(!emptySelection);
}
}
class SearchPersonAction extends AbstractAction {
private static final long serialVersionUID = 4083406832930707444L;
private Document searchInput;
private DefaultListModel searchResult;
private PersonService personService;
public SearchPersonAction(Document searchInput,
DefaultListModel searchResult, PersonService personService) {
this.searchInput = searchInput;
this.searchResult = searchResult;
this.personService = personService;
}
public void actionPerformed(ActionEvent e) {
String searchString = getSearchString();
List<Person> matchedPersons = personService.searchPersons(searchString);
searchResult.clear();
for (Person person : matchedPersons) {
Object elementModel = new PersonElementModel(person);
searchResult.addElement(elementModel);
}
}
private String getSearchString() {
try {
return searchInput.getText(0, searchInput.getLength());
} catch (BadLocationException e) {
return null;
}
}
}
class PersonElementModel {
private Person person;
public PersonElementModel(Person person) {
this.person = person;
}
public Person getPerson() {
return person;
}
@Override
public String toString() {
return person.getFirstName() + ", " + person.getLastName();
}
}
interface PersonService {
List<Person> searchPersons(String searchString);
}
class Person {
private int id;
private String firstName;
private String lastName;
public Person(int id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
public int getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
class PersonServiceMock implements PersonService {
private List<Person> personDB;
public PersonServiceMock() {
personDB = new ArrayList<Person>();
personDB.add(new Person(1, "Graham", "Parrish"));
personDB.add(new Person(2, "Daniel", "Hendrix"));
personDB.add(new Person(3, "Rachel", "Holman"));
personDB.add(new Person(4, "Sarah", "Todd"));
personDB.add(new Person(5, "Talon", "Wolf"));
personDB.add(new Person(6, "Josephine", "Dunn"));
personDB.add(new Person(7, "Benjamin", "Hebert"));
personDB.add(new Person(8, "Lacota", "Browning "));
personDB.add(new Person(9, "Sydney", "Ayers"));
personDB.add(new Person(10, "Dustin", "Stephens"));
personDB.add(new Person(11, "Cara", "Moss"));
personDB.add(new Person(12, "Teegan", "Dillard"));
personDB.add(new Person(13, "Dai", "Yates"));
personDB.add(new Person(14, "Nora", "Garza"));
}
public List<Person> searchPersons(String searchString) {
List<Person> matches = new ArrayList<Person>();
if (searchString == null) {
return matches;
}
for (Person person : personDB) {
if (person.getFirstName().contains(searchString)
|| person.getLastName().contains(searchString)) {
matches.add(person);
}
}
return matches;
}
}
Vous pouvez créer un modèle dans une classe distincte et simple Java, et un contrôleur dans une autre.
Ensuite, vous pouvez avoir des composants Swing en plus de cela. JTable
serait l'une des vues (et le modèle de table de facto ferait partie de la vue - il se traduirait uniquement du "modèle partagé" en JTable
) .
Chaque fois que la table est éditée, son modèle de table indique au "contrôleur principal" de mettre à jour quelque chose. Cependant, le contrôleur ne doit rien savoir de la table. Ainsi, l'appel devrait ressembler davantage à: updateCustomer(customer, newValue)
, pas updateCustomer(row, column, newValue)
.
Ajoutez une interface d'écoute (observateur) pour le modèle partagé. Certains composants (par exemple votre table) pourraient l'implémenter directement. Un autre observateur pourrait être le contrôleur qui coordonne la disponibilité des boutons, etc.
C'est une façon de le faire, mais bien sûr, vous pouvez le simplifier ou l'étendre si c'est une exagération pour votre cas d'utilisation.
Vous pouvez fusionner le contrôleur avec le modèle et avoir les mêmes mises à jour de processus de classe et maintenir la disponibilité des composants. Vous pouvez même faire du "modèle partagé" un TableModel
(bien que s'il n'est pas uniquement utilisé par la table, je recommanderais au moins de fournir une API plus conviviale qui ne laisse pas fuir les abstractions de la table)
En revanche, vous pouvez avoir des interfaces complexes pour les mises à jour (CustomerUpdateListener
, OrderItemListener
, OrderCancellationListener
) et un contrôleur dédié (ou médiateur) uniquement pour la coordination des différentes vues.
Cela dépend de la complexité de votre problème.
Si vous développez un programme avec un GUI , mvc pattern est presque là mais flou.
Désactiver le modèle, la vue et le code du contrôleur est difficile, et n'est normalement pas seulement une tâche de refactorisation.
Vous savez que vous l'avez lorsque votre code est réutilisable. Si vous avez correctement implémenté MVC, il devrait être facile d'implémenter un TUI ou un CLI = ou un RWD ou un premier design mobile avec la même fonctionnalité. Il est facile de le voir faire que de le faire réellement, d'ailleurs sur un code existant.
En fait, les interactions entre le modèle, la vue et le contrôleur se produisent en utilisant d'autres modèles d'isolement (comme observateur ou écouteur)
Je suppose que ce post l'explique en détail, du modèle direct non MVC (comme vous le ferez sur un Q&D ) à l'implémentation réutilisable finale:
Pour une séparation correcte, vous auriez généralement une classe de contrôleur à laquelle la classe Frame déléguerait. Il existe différentes manières de configurer les relations entre les classes - vous pouvez implémenter un contrôleur et l'étendre avec votre classe de vue principale, ou utiliser une classe de contrôleur autonome que le Frame appelle lorsque des événements se produisent. La vue recevrait généralement des événements du contrôleur en implémentant une interface d'écoute.
Parfois, une ou plusieurs parties du modèle MVC sont triviales, ou si "minces" qu'elles ajoutent une complexité inutile pour les séparer. Si votre contrôleur est plein d'appels d'une ligne, le faire dans une classe distincte peut finir par obscurcir le comportement sous-jacent. Par exemple, si tous les événements que vous gérez sont liés à un TableModel et sont de simples opérations d'ajout et de suppression, vous pouvez choisir d'implémenter toutes les fonctions de manipulation de table dans ce modèle (ainsi que les rappels nécessaires pour l'afficher dans le JTable). Ce n'est pas vrai MVC, mais cela évite d'ajouter de la complexité là où elle n'est pas nécessaire.
Quelle que soit l'implémentation, n'oubliez pas de JavaDoc vos classes, méthodes et packages afin que les composants et leurs relations soient correctement décrits!
J'ai trouvé des articles intéressants sur l'implémentation de modèles MVC, qui pourraient résoudre votre problème.