openxava / documentation / Lesson 20: Behavior & business logic

Course: 1. Getting started | 2. Basic domain model (1) | 3. Basic domain model (2) | 4. Refining the user interface | 5. Agile development6. Mapped superclass inheritance | 7. Entity inheritance | 8. View inheritance | 9. Java properties | 10. Calculated properties | 11. @DefaultValueCalculator in collections | 12. @Calculation and collections totals | 13. @DefaultValueCalculator from file | 14. Manual schema evolution | 15. Multi user default value calculation | 16. Synchronize persistent and computed propierties | 17. Logic from database | 18. Advanced validation | 19. Refining the standard behavior | 20. Behavior & business logic | 21. References & collections | A. Architecture & philosophy | B. Java Persistence API | C. Annotations | D. Automated testing

Table of contents

Lesson 20: Behavior & business logic
Business logic in detail mode
Creating an action for custom logic
Writing the real business logic in the entity
Write less code using Apache Commons BeanUtils
Application exceptions
Validation from action
On change event to hide/show an action programmatically
Business logic from list mode
List action with custom logic
Business logic in the model over several entities
Showing a dialog
Using showDialog()
Define the dialog actions
Closing dialog
Plain view instead of dialog
Summary
OpenXava is not just a CRUD framework, but a framework for developing full-fledged business applications. Until now we have learned how to create and enhance a data management application. We will now improve the application further by giving the user the possibility to perform specific business logic.
In this lesson we'll see how to add business logic to a model and call this logic from custom actions. In this way you can transform a database management application into a useful tool for the everyday work of your user.

Business logic in detail mode

We'll start with the simplest case: a button in the detail mode that executes some concrete logic. In this case we'll add a button for creating an invoice from an order:
business-logic-behavior_en010.png
This shows how this new action takes the current order and creates an invoice from it. It just copies all the order data to the new invoice, including the detail lines. A message is shown and the INVOICE tab of the order will display the recently created invoice. Let's see how to implement this.

Creating an action for custom logic

As you already know the first step towards having a custom action in your module is defining a controller for that action. Let's edit controllers.xml, to add such a controller. Here you have the Order controller definition:
<controller name="Order">
    <extends controller="Invoicing"/> <!-- In order to have the standard actions -->
    
    <action name="createInvoice" mode="detail"
        class="com.yourcompany.invoicing.actions.CreateInvoiceFromOrderAction"/>
    <!-- mode="detail" : Only in detail mode -->
    
</controller>
Since we follow the convention of giving the controller the same name as the entity and the module, you automatically have this new action available for Order. Order controller extends Invoicing controller. Remember that we created Invoicing controller in the previous lesson. It is a refinement of the Typical controller.
Now we have to write the Java code for the action:
package com.yourcompany.invoicing.actions; // In 'actions' package

import org.openxava.actions.*;
import com.yourcompany.invoicing.model.*;

public class CreateInvoiceFromOrderAction
    extends ViewBaseAction { // To use getView()

    public void execute() throws Exception {
        Order order = (Order) getView().getEntity(); // Order entity displayed in the view (1)    
        order.createInvoice(); // The real work is delegated to the entity (2)
        getView().refresh(); // In order to see the created invoice in 'Invoice' tab (3)
        addMessage("invoice_created_from_order", // Confirmation message (4)
            order.getInvoice());
    }
}
Really simple. We get the Order entity (1), call the createInvoice() method (2), refresh the view (3) and display a message (4). Note how the action is a mere intermediary between the view (the user interface) and the model (the business logic).
Remember to add the message text to the Invoicing-messages_en.properties file in i18n folder, as following:
invoice_created_from_order=Invoice {0} created from current order
However, just "as is" the message is not shown nicely, because we pass an Invoice object as argument. We need a toString() for Invoice and Order useful to the user. We'll overwrite the toString() of CommercialDocument (the parent of Invoice and Order) to achieve this. You can see this toString() method here:
abstract public class CommercialDocument extends Deletable {

    ...

    public String toString() {
        return year + "/" + number;
    }
}
Year and number are perfect to identify an invoice or order from the user perspective.
That's all for the action. Let's see the missing piece, the createInvoice() method of the Order entity.

Writing the real business logic in the entity

The business logic for creating the new Invoice is defined in the Order entity, not in the action. This is just the natural way to go. This is the natural way to go in accordance with the essential principle behind Object-Orientation where the objects are not just data, but data and logic. The most beautiful code is that whose objects contain the logic for managing their own data. If your entities are mere data containers (simple wrappers around database tables), and your actions contain all the logic for manipulating them, your code is a perversion of the original goal of Object-Orientation.
Apart from the spiritual reason, to put the logic for creating an Invoice inside the Order entity is a very pragmatic approach, because in this way we can use this logic from other actions, batch processes, web services, etc.
Let's see the code of the createInvoice() method of the Order class:
public class Order extends CommercialDocument {

    ...
    
    public void createInvoice() throws Exception { // throws Exception is just
                                                   // to get simpler code for now
        Invoice invoice = new Invoice(); // Instantiates an Invoice (1)
        BeanUtils.copyProperties(invoice, this); // and copies the state (2)
                                                 // from the current Order
        invoice.setOid(null); // To let JPA know this entity does not exist yet
        invoice.setDate(LocalDate.now()); // The date for the new invoice is today
        invoice.setDetails(new ArrayList<>(getDetails())); // Clones the details collection
        XPersistence.getManager().persist(invoice);
        this.invoice = invoice; // Always after persist() (3)
    }
}
The logic consists of creating a new Invoice object (1), copying the data from the current Order to it (2) and assigning the resulting entity to the invoice reference in the current Order (3).
There are three subtle details here. First, you have to write invoice.setOid(null), otherwise the new Invoice will get the same identity as the source Order. Moreover, JPA does not like to persist objects with the autogenerated id pre-filled. Second, you have to assign the new Invoice to the current Order (this.invoice = invoice) after your call to persist(invoice), if not you get a error from JPA (something like "object references an unsaved transient instance". Third, we have to wrap the details collection with a new ArrayList(), so it is a new collection but with the same elements, because JPA don't want the same collection assigned to two entities.

Write less code using Apache Commons BeanUtils

Note how we have used BeanUtils.copyProperties() to copy all properties from the current Order to the new Invoice. This method copies all properties with the same name from one object to another, even if the objects belong to different classes. This utility is from the Commons BeanUtils project from Apache. The jar for this utility, commons-beanutils.jar, is already included in your project.
The next snippet shows how using BeanUtils you actually write less code:
BeanUtils.copyProperties(invoice, this);
// Is the same as
invoice.setOid(getOid());
invoice.setYear(getYear());
invoice.setNumber(getNumber());
invoice.setDate(getDate());
invoice.setDeleted(isDeleted());
invoice.setCustomer(getCustomer());
invoice.setVatPercentage(getVatPercentage());
invoice.setVat(getVat());
invoice.setTotalAmount(getTotalAmount());
invoice.setRemarks(getRemarks());
invoice.setDetails(getDetails());
However, the main advantage of using BeanUtils is not to save some typing, but that you have code more resilient to changes. Because, if you add, remove or rename some property of ComercialDocument (the parent of Invoice and Order) you don't need to change your code, while if you're copying the properties manually you must change the code manually.

Application exceptions

Remember the phrase: "The exception that proves the rule". Rules, life and software are full of exceptions. And our createInvoice() method is not an exception. We have written the code to work in the most common cases. But, what happens if the order is not ready to be invoiced, or if there is some problem accessing the database? Obviously, in these cases we need to take different paths.
This is to say that the simple throws Exception we have written for createInvoice() method is not enough to ensure a robust behavior. Instead we should use our own exception. Let's create it:
package com.yourcompany.invoicing.model; // In model package

import org.openxava.util.*;

public class CreateInvoiceException extends Exception { // Not RuntimeException

    public CreateInvoiceException(String message) {
        // The XavaResources is to translate the message from the i18n entry id
        super(XavaResources.getString(message)); 
    }
	
}
Now we can use our CreateInvoiceException instead of Exception in the createInvoice() method of Order:
public void createInvoice()
    throws CreateInvoiceException // An application exception (1)
{
    if (this.invoice != null) { // If an invoice is already present we cannot create one
        throw new CreateInvoiceException( 
            "order_already_has_invoice"); // Allows an i18n id as argument
    }
    if (!isDelivered()) { // If the order is not delivered we cannot create the invoice
        throw new CreateInvoiceException("order_is_not_delivered");
    }
    try {
        Invoice invoice = new Invoice(); 
        BeanUtils.copyProperties(invoice, this); 
        invoice.setOid(null); 
        invoice.setDate(LocalDate.now()); 
        invoice.setDetails(new ArrayList<>(getDetails())); 
        XPersistence.getManager().persist(invoice);
        this.invoice = invoice; 
    }
    catch (Exception ex) { // Any unexpected exception (2)
        throw new SystemException( // A runtime exception is thrown (3)
            "impossible_create_invoice", ex);
    }
}
Now we declare explicitly which application exceptions this method throws (1). An application exception is a checked exception that indicates a special but expected behavior of the method. An application exception is related to the method's business logic. You could create an application exception for every possible case, such as an OrderAlreadyHasInvoiceException and an OrderNotDeliveredException. This enables you to handle each case differently in the calling code. This is not needed in our case, so we simply use our CreateInvoiceException, a generic application exception for this method.
Additionally, we have to deal with unexpected problems (2). Unexpected problems can be system errors (database access, net or hardware problems) or programmer errors (NullPointerException, IndexOutOfBoundsException, etc). When we find any unexpected problem we throw a runtime exception (3). In this instance we have thrown SystemException, a runtime exception included in OpenXava for convenience, but you can throw any runtime exception you want.
You do not need to modify the action code. If your action does not catch the exceptions, OpenXava does it automatically. It displays the messages from the application exceptions to the user, and, for the runtime exceptions, shows a generic error message, and rolls back the transaction.
In order to be complete, we have to add the messages used for the exceptions in the i18n files. Edit the Invoicing-messages_en.properties file from Invoicing/i18n folder adding the next entries:
order_already_has_invoice=The order already has an invoice
order_is_not_delivered=The order is not delivered yet
impossible_create_invoice=Impossible to create invoice
There is some debate in the developer community regarding the correct way of using exceptions in Java. The approach in this section is the classic way to work with exceptions in the Java Enterprise world.

Validation from action

Usually the best place for validations is the model, i.e., the entities. However, sometimes it's necessary to put validation logic in the actions. For example, if you want to obtain the current state of the user interface, the validation must be done from the action.
In our case, if the user clicks on CREATE INVOICE when creating a new order, and this order is not yet saved, it will fail. It fails because it's impossible to create an invoice from an non-existent order. The user must first save the order.
For that add an condition at the begin of the execute() method of CreateInvoiceFromOrderAction to validate that the currently displayed order is saved:
public void execute() throws Exception {
    // Add the next condition       
    if (getView().getValue("oid") == null) { 
        // If oid is null the current order is not saved yet (1)
        addError("impossible_create_invoice_order_not_exist");
        return;
    }
    
    ...
    
}
The validation consists of verifying if the oid is null (1), in which case the user is entering a new order, but he did not save it yet. In this case a message is shown, and the creation of the invoice is aborted.
Here we also have a message to add to the i18n file. Edit the Invoicing-messages_en.properties file in the Invoicing/i18n folder adding the next entry:
impossible_create_invoice_order_not_exist=Impossible to create invoice: The order does not exist yet
Validations tell the user that he has done something wrong. This is needed, of course, but better still is to create an application that helps the user to avoid any wrong doings. Let's see one way to do so in the next section.

On change event to hide/show an action programmatically

Our current code is robust enough to prevent user slips from breaking data. We will go one step further, preventing the user to slip at all. We're going to hide the action for creating a new invoice, if the order is not valid to be invoiced.
OpenXava allows to hide and show actions programmatically. It also allows the execution of an action when some property is changed by the user on the screen. We can use these two techniques to show the button only when the action is ready to be used.
Remember that an invoice can be generated from an order only if the order has been delivered and it does not yet have an invoice. So, we have to monitor the changes in the invoice reference and delivered property of the Order entity. First, we'll create an action to show or hide the action for creating an invoice from order, ShowHideCreateInvoiceAction, with this code:
package com.yourcompany.invoicing.actions; // In the 'actions' package

import org.openxava.actions.*; // Needed to use OnChangePropertyAction,

public class ShowHideCreateInvoiceAction
    extends OnChangePropertyBaseAction { // Needed for @OnChange actions (1)

    public void execute() throws Exception {
        if (isOrderCreated() && isDelivered() && !hasInvoice()) { // (2)
            addActions("Order.createInvoice");
        }
        else {
            removeActions("Order.createInvoice");
        }
    }
	
    private boolean isOrderCreated() {
        return getView().getValue("oid") != null; // We read the value from the view
    }
	
    private boolean isDelivered() {
        Boolean delivered = (Boolean)
            getView().getValue("delivered"); // We read the value from the view
        return delivered == null?false:delivered;
    }

    private boolean hasInvoice() {
        return getView().getValue("invoice.oid") != null; // We read the value from the view
    } 	
}
Then we annotate invoice and delivered in Order with @OnChange so when the user changes the value of delivered or invoice in the screen, the ShowHideCreateInvoiceAction will be executed:
public class Order extends CommercialDocument {

    ...
    @OnChange(ShowHideCreateInvoiceAction.class) // Add this
    Invoice invoice;

    ...
    @OnChange(ShowHideCreateInvoiceAction.class) // Add this
    boolean delivered;

    ...
}
ShowHideCreateInvoiceAction is a conventional action with an execute() method, moreover it extends OnChangePropertyBaseAction (1). All the actions annotated with @OnChange must implement IOnChangePropertyAction, however it's easier to extend OnChangePropertyBaseAction which implements it. From this action you can use the getNewValue() and getChangedProperty(), although in this specific case we don't need them.
The execute() method asks if the displayed order is saved, delivered, and does not already have an invoice (2), in that case it shows the action with addActions("Order.createInvoice"), otherwise it hides the action with removeActions("Order.createInvoice"). Thus we hide or show the Order.createInvoice action, only showing it when it is applicable.The add/removeActions() methods allow to specify several actions to show or hide separated by commas.
Now when the user checks the delivered checkbox, or chooses an invoice, the action button is shown or hidden. Accordingly, when the user clicks on New button to create a new order the button for creating the invoice is hidden. However, if you choose to modify an already existing order, the button is always present, regardless if the prerequisites are fulfilled. This is because when an object is searched and displayed the @OnChange actions are not executed by default. We can change this with a little modification in SearchExcludingDeleteAction:
public class SearchExcludingDeletedAction
    // extends SearchByViewKeyAction {
    extends SearchExecutingOnChangeAction { // Use this as base class
The default search action, i.e., SearchByViewKeyAction does not execute the @OnChange actions, so we change our search action to extend from SearchExecutingOnChangeAction. SearchExecutingOnChangeAction behaves like SearchByViewKeyAction but executes the on-change events. This way, when the user selects an order, the ShowHideCreateInvoiceAction is executed.
A tiny detail remains to make all this perfect: when the user click on CREATE INVOICE, after the invoice has been created, the button should be hidden. It should not be possible to create the same invoice twice. We can implement this functionality by refining the CreateInvoiceFromOrderAction:
public void execute() throws Exception {

    ...

    // Everything worked fine, so we'll hide the action
    removeActions("Order.createInvoice"); 
}
As you can see we just add the removeActions("Order.createInvoice") at the end of the execute() method.
Showing and hiding actions is not a substitute for validation in the model. Validations are still necessary since the entities can be used from any other part of the application, not just from the CRUD module. However, the trick of hiding and showing actions improves the user experience.

Business logic from list mode

In the previous lesson you learned how to create list actions. List actions are very useful tools that provides the user with the ability to perform some specific logic on multiple objects at the same time. In our case, we can add an action in list mode to create a new invoice automatically from several selected orders in the list. We want this action to work this way:
business-logic-behavior_en020.png
This list action takes the selected orders and creates an invoice from them. It just copies the order data into the new invoice, adding the detail lines of all the orders in one unique invoice. Also a message is shown. Let's see how to code this behavior.

List action with custom logic

As you already know, the first step towards having a new custom action in your module is to add that action to a controller. So, let's edit controllers.xml adding a new action to the Order controller:
<controller name="Order">

    ...
    
    <!-- The new action -->
    <action name="createInvoiceFromSelectedOrders"
        mode="list"
        class="com.yourcompany.invoicing.actions.CreateInvoiceFromSelectedOrdersAction"/>
	<!-- mode="list": Only shown in list mode -->

</controller>
This is all that is needed to have this new action available for Order in list mode.
Now we have to write the Java code for the action:
package com.yourcompany.invoicing.actions;

import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*;
import org.openxava.model.*;
import com.yourcompany.invoicing.model.*;

public class CreateInvoiceFromSelectedOrdersAction
    extends TabBaseAction { // Typical for list actions. It allows you to use getTab() (1)

    public void execute() throws Exception {
        Collection<Order> orders = getSelectedOrders(); // (2)
        Invoice invoice = Invoice.createFromOrders(orders); // (3)
        addMessage("invoice_created_from_orders", invoice, orders); // (4)
    }

    private Collection<Order> getSelectedOrders() // (5)
        throws FinderException
    {
        Collection<Order> result = new ArrayList<>();
        for (Map key: getTab().getSelectedKeys()) { // (6)
            Order order = (Order) MapFacade.findEntity("Order", key); // (7)
            result.add(order);
        }
        return result;
    }
}
Really simple. We obtain the list of the checked orders in the list (2), call createFromOrders() static method (3) of Invoice and show a message (4). In this case we also put the real logic in the model class, not in the action. Since the logic applies to several orders and creates a new invoice the natural place to put it is a static method of Invoice class.
The getSelectedOrders() method (5) returns a collection containing the Order entities checked by the user in the list. This is easily achieved using getTab() (6), available from TabBaseAction (1), that returns an org.openxava.tab.Tab object. The Tab object allows you to manage the tabular data of the list. In this case we use getSelectedKeys() (6) that returns a collection with the keys of the selected rows. Since these keys are in Map format we use MapFacade.findEntity() (7) to convert them to Order entities.
As always, add the message text to the Invoicing-messages_en.properties file in i18n folder:
invoice_created_from_orders=Invoice {0} created from orders: {1}
That's all for the action. Let's see the missing piece, the createFromOrders() method of the Invoice class.

Business logic in the model over several entities

The business logic for creating a new Invoice from several Order entities is in the model layer, i.e., the entities, not in the action. We cannot put the method in Order class, because the process is done from several orders, not just one. We cannot use an instance method in Invoice because the invoice does not exist yet, in fact we want to create it. Therefore, we are going to create a static factory method in the Invoice class for creating a new invoice from several orders.
You can see this method here:
public class Invoice extends CommercialDocument {

    ...
	
    public static Invoice createFromOrders(Collection<Order> orders)
        throws CreateInvoiceException
    {
        Invoice invoice = null;
        for (Order order: orders) {
            if (invoice == null) { // The first order
                order.createInvoice(); // We reuse the logic for creating an invoice
                                       // from an order
                invoice = order.getInvoice(); // and use the created invoice
            }
            else { // For the remaining orders the invoice is already created
                order.setInvoice(invoice); // Assign the invoice
                order.copyDetailsToInvoice(); // A method of Order to copy the lines
            } 
        } 
        if (invoice == null) { // If there are no orders
            throw new CreateInvoiceException(
                "orders_not_specified");
        }
        return invoice;
    }
}
We use the first Order to create the new Invoice using the already existing createInvoice() method from Order. Then we call  to the copyDetailsToInvoice() method of Order to copy the lines from the remaining orders to the new Invoice and accumulates on it the vat and totalAmount. Moreover, we set the new Invoice as the invoice for the orders of the collection.
If invoice is null at the end of the process it's because the orders collection is empty. In this case we throw a CreateInvoiceException. Since the action does not catch the exceptions, OpenXava shows the exception message to the user. This is fine. If the user does not check any orders and he clicks on the button for creating an invoice, then this error message will be shown to him.
We still have to add the copyDetailsToInvoice() method to Order:
public class Order extends CommercialDocument {

    ...
	
    public void copyDetailsToInvoice() { 
        invoice.getDetails().addAll(getDetails()); // Copies the lines
        invoice.setVat(invoice.getVat().add(getVat())); // Accumulates the vat
        invoice.setTotalAmount( // and the total amount
            invoice.getTotalAmount().add(getTotalAmount())); 
    }
}
As you can see, It copies the details of the current order to the invoice and accumulates the vat and totalAmount.
Remember to add the message text to the Invoicing-messages_en.properties file in i18n folder:
orders_not_specified=Orders not specified
These are not the only errors the user can get. All previously written validations for Invoice and Order still apply automatically. This ensures that the user has to choose orders from the same customer, that are delivered, that lacks an invoice, etc. Model validation prevents the user from creating invoices from the wrong orders.

Showing a dialog

After creating an invoice from several orders, it would be practical for the user to see and possibly edit the newly created invoice. One way of achieving this is by showing a dialog that allows to view and edit that just created invoice. In this way:
business-logic-behavior_en030.png
Let's see how to implement this behavior.

Using showDialog()

The first step is to modify CreateInvoiceFromSelectedOrdersAction to show a dialog after creating the invoice, just adding a few lines at the end of execute():
public void execute() throws Exception {
    Collection<Order> orders = getSelectedOrders(); 
    Invoice invoice = Invoice.createFromOrders(orders); 
    addMessage("invoice_created_from_orders", invoice, orders); 

    // Add the next lines to show the dialog
    showDialog(); // (1)
    // From now on getView() is the dialog
    getView().setModel(invoice); // Display invoice in the dialog (2)
    getView().setKeyEditable(false); // To indicate that is an existing object (3)
    setControllers("InvoiceEdition"); // The actions of the dialog (4)
}
We call to showDialog() (1), it shows a dialog and after that moment when we use getView() it references to the view in the dialog no the main view of the module. After the showDialog() the dialog is blank, until we assign our invoice to the view with getView().setModel(invoice) (2), now the invoice is displayed in the dialog. The next line, getView().setKeyEditable(false) (3), is to indicate that the invoice is already saved, so afterwards the corresponding save action knows how to behaves. Finally, we use setControllers("InvoiceEdition") to define the controller with the actions present in the dialog, that is the buttons on bottom of the dialog. Note as setControllers() is an alternative to addActions().
Obviously, this will not work until we have the InvoiceEdition controller defined. We'll do this in the next section.

Define the dialog actions

The dialog allows the user to change the invoice and save the changes or just close the dialog after examining the invoice. These actions are defined in the InvoiceEdition controller in controllers.xml:
<controller name="InvoiceEdition">

    <action name="save"
        class="com.yourcompany.invoicing.actions.SaveInvoiceAction"
        keystroke="Control S"/>
		
    <action name="close"
        class="org.openxava.actions.CancelAction"/>
		
</controller>
The two actions of this controller represent the two buttons, SAVE and CLOSE you saw in the previous image.

Closing dialog

SaveInvoiceAction contains just a minor extension of the standard SaveAction of OpenXava:
package com.yourcompany.invoicing.actions;

import org.openxava.actions.*;

public class SaveInvoiceAction
    extends SaveAction { // Standard OpenXava action to save the view content
	 
    public void execute() throws Exception {
        super.execute(); // The standard saving logic (1)
        closeDialog(); // (2)
    }
}
The action extends SaveAction overwriting the execute() method to just call to the standard logic, with super.execute() (1), and then to close the dialog with closeDialog() (2). In this way, when the user clicks on SAVE, the invoice data is saved and the dialog is closed, so the application returns to the list of orders, ready to continue the creation of invoices from orders.
For the CLOSE button we use the CancelAction, an action included in OpenXava that simply closes the dialog.

Plain view instead of dialog

Sometimes instead of showing a dialog on top:
business-logic-behavior_en040.png
You could prefer to replace the current view with a new one, thus:
business-logic-behavior_en050.png
This can be useful when the amount of data to show is huge and in a dialog looks clumsy. Using a plain view instead of a dialog is as easy as changing this line of from your CreateInvoiceFromSelectedOrdersAction:
showDialog();
By this one:
showNewView();
No more changes are needed. Well, maybe changing the name of the "close" action to "return" in InvoiceEdition controller in controllers.xml.

Our work is done. If you try out the Order module, choose several orders, and click on the CREATE INVOICE FROM SELECTED ORDERS to see a dialog with the newly created invoice. Just as you saw in the image at the beginning of this section.

Summary

The salt of your application comes from the actions and entity methods. Thanks to them you can convert a simple data management application into a useful tool. In this lesson, for example, we provided the user with a way to automatically create invoices from orders.
You have learned how to create instance and static methods for business logic, and how to call them from actions in detail and list mode. Along the way you also saw how to hide and show actions, use exceptions, validating from actions, show dialogs and how to test all this.
We still have many interesting things to learn, in the next lesson for example we are going to refine the behavior of references and collections.

Download source code of this lesson

Any problem with this lesson? Ask in the forum Everything fine? Go to Lesson 21