© John Kouraklis 2016

John Kouraklis, MVVM in Delphi, 10.1007/978-1-4842-2214-0_2

2. Setting Up the POSApp

John Kouraklis

(1)London, UK

As described in the introduction, in this book we will work on how to convert an application to follow the MVVM pattern. The example application is a POS client for purchasing clothes. The first version will use an approach that mixes the user interface and the model of the application (the business logic). This type of design is referred as monolithic and it is pretty much what you have when you deal with so-called legacy code, especially if your application was created in the 1980s and 1990s.

Note

You come across monolithic design in old applications (legacy) but you can also end up with such a structure if you follow tightly the development workflow in Rapid Application Development (RAD) environments, such as the IDE that comes with Delphi. RAD makes very easy to write code for an event generated by a GUI element and access data-aware components (non-GUI elements) in one line. This ease of coding creates a mindset of tight design between the model and the presentation of an application. In turn, this contradicts the philosophy of the presentation patterns. For more details about the pros and cons of RAD environments and the implied methodologies, see Begel (2007) and Gerber (2007).

Let’s call this application POSApp. The requirements for POSApp are the following:

  • The user can create an invoice for a sale.

  • The user can choose customers by name and see their outstanding balances and discount rates (based on predefined rates per customer). There is also a “Retail Customer” for general (anonymous) sales.

  • The user can add and remove items in the invoice for each sale and adjust the quantities.

  • The user can apply the relevant discount rate.

  • The user can see the total amount of sales.

POSApp Forms

POSApp has two screens. The main screen (see Figure 2-1) shows the Total Sales and includes a button that allows users to issue an invoice.

A371064_1_En_2_Fig1_HTML.jpg
Figure 2-1. The main screen of the POSApp

The second screen is more complicated and can be seen in Figure 2-2. It includes a popup menu to allow the users to select a customer. They can then see the outstanding balance of the customer and the discount rate that he or she is entitled to enjoy.

A371064_1_En_2_Fig2_HTML.jpg
Figure 2-2. The invoice screen of POSApp

There is also a region in the form where the user selects the items to sell and sets the quantity. If users want to delete an item from the invoice, a popup is revealed when they right-click on the items list (see Figure 2-3). The current balance is shown; users can print the invoice or cancel it.

A371064_1_En_2_Fig3_HTML.jpg
Figure 2-3. Right-click on an invoice item to reveal a popup menu

If you think that the screens are not the best in terms of user experience and interface design, you are right. The point of the application is not to present a nice, optimized user interface but to provide a workable application that will demonstrate the MVVM framework.

Let’s start building POSApp.

  1. Open Delphi IDE and create a new blank multi-device project (choose File ➤ New ➤ Multi-Device Application ➤ Blank Application).

  2. Your project has only one form (Unit1.pas). Rename the form MainForm and change the caption to POSApp. Save the form and give it the name MainScreenForm.pas.

  3. In the right-side bar, you can see the Project Manager. If you can’t see the Project Manager, use the View menu and select the Project Manager menu item.

  4. Right-click on the Project1.exe label, then click Save and enter the name POSApp.

  5. Right-click on the ProjectGroup1 label in the Project Manager and select Save Project Group. Save the project under the name POSAppProjectGroup. It is very likely that you’ll see a different number in the ProjectGroup label. This doesn’t affect the code here and you can continue with the next steps.

  6. Add three TLabel components and one TButton to the MainForm.

  7. Rename the components and edit their properties according to Figure 2-4.

    A371064_1_En_2_Fig4_HTML.jpg
    Figure 2-4. Components and their properties of the MainScreenForm
  8. Add a second multi-device form to the project. An alternative way to do this instead of the process in Step 1 is to right-click on the POSApp.exe element in the Project Manager (see Figure 2-5) and choose HD Form from the wizard.

    A371064_1_En_2_Fig5_HTML.jpg
    Figure 2-5. The popup menu to add a form to the project
  9. Name the form SalesInvoiceForm and save it as InvoiceForm.pas. Then use Figure 2-6 to add components, rename them, and adjust their properties.

    A371064_1_En_2_Fig6a_HTML.jpgA371064_1_En_2_Fig6b_HTML.jpgA371064_1_En_2_Fig6c_HTML.jpgA371064_1_En_2_Fig6d_HTML.jpg
    Figure 2-6. The Components of the InvoiceForm and their properties

At this stage, we have created the user interface for your application. Use the Project ➤ Options menu option and click Forms. Then, select the InvoiceForm from the left side list and use the relevant button to transfer it to the list on the right, as shown in Figure 2-7. This will prevent the application from creating the InvoiceForm automatically. You can also remove the following code from InvoiceForm.pas. We will create the forms manually.

A371064_1_En_2_Figa_HTML.gif
A371064_1_En_2_Fig7_HTML.jpg
Figure 2-7. The form options for the POSApp project

Switch to the MainScreenForm.pas unit. We will now add code to the ButtonInvoice in order to open the InvoiceForm. Select the ButtonInvoice, click on the OnClick event (or click on the button itself), and add the following code. You also need to make sure that you declare the InvoiceForm in the unit’s uses clause.

uses
  ..., InvoiceForm;


procedure TMainForm.ButtonInvoiceClick(Sender: TObject);
var
  tmpInvoiceForm: TSalesInvoiceForm;
begin
  tmpInvoiceForm:=TSalesInvoiceForm.Create(self);
  tmpInvoiceForm.ShowModal;
end;

Compile and run the application. Click on the Issue Invoice button. You should now be able to see the invoice form.

Mixing Business and Presentation

In this section, we will develop the POSApp in such way that mixes business logic and presentation layers. We will follow the typical design that evolves when RAD tools are used and developers do not consider any design patterns (called monolithic design). Then, in the rest of the book, we will refactor this application to the MVVM pattern.

We need the following entities (classes) for POSApp:

  • TCustomer: A class that represents a customer and keeps track of the name of the customer, the current outstanding balance, the discount rate the customer is eligible for, and the ID of the customer.

  • TItem: A class that holds an item to be sold. It has a field for the description of the item, its price, and ID number.

  • TInvoice: A class that represents an invoice. It has an ID field and a sequence number (the invoice no.).

  • TInvoiceItem: Each invoice includes items for a specific transaction. This class keeps the following data for each invoice item: ID (rank number), itemID, invoiceID (the invoice the item belongs to), the price per unit item, and the quantity of the item.

Declaration of Classes

To keep things organized in the code, we will keep all the declarations of the classes in one unit. Add a new unit to the project (you can use the Add New ➤ Unit option from the popup menu, as shown in Figure 2-5) and save it as Declarations.pas. Then, add the following code to the unit.

unit Declarations;

interface

type
  TCustomer = class
  private
    fID: Integer;
    fName: string;
    fDiscountRate: Double;
    fBalance: Currency;
  public
    property ID: integer read fID write fID;
    property Name: string read fName write fName;
    property DiscountRate: double read fDiscountRate write fDiscountRate;
    property Balance: Currency read fBalance write fBalance;
  end;


  TItem = class
  private
    fID: Integer;
    fDescription: string;
    fPrice: Currency;
  public
    property ID: integer read fID write fID;
    property Description: string read fDescription write fDescription;
    property Price: Currency read fPrice write fPrice;
  end;


  TInvoice = class
  private
    fID: integer;
    fNumber: integer;
    fCustomerID: integer;
  public
    property ID: Integer read fID write fID;
    property Number: Integer read fNumber write fNumber;
    property CustomerID: Integer read fCustomerID write fCustomerID;
  end;


TInvoiceItem = class
  private
    fID: integer;
    fInvoiceID: integer;
    fItemID: integer;
    fUnitPrice: Currency;
    fQuantity: integer;
  public
    property ID: integer read fID write fID;
    property InvoiceID: integer read fInvoiceID write fInvoiceID;
    property ItemID: integer read fItemID write fItemID;
    property UnitPrice: Currency read fUnitPrice write fUnitPrice;
    property Quantity: Integer read fQuantity write fQuantity;
  end;


implementation

end.

The Database Unit

Create a new unit and save it as Database.pas. This unit represents the persistent medium of your application. In a real-life application, you are most likely to use local and/or remote databases but, in this case, we will use hard-coded data. We will generate customer records, balances, and discount rates manually in the Create event of the class.

Now we will create a TDatabase class to simulate the “persistent” data. In the interface section of the Database.pas unit, add the following code:

uses Declarations, System.Generics.Collections;

type
  TDatabase = class
  private
    fCustomers: TObjectList<TCustomer>;
    fItems: TObjectList<TItem>;
  public
    constructor Create;
    destructor Destroy; override;
  end;

The class we created does not offer a way to expose the private fields. At this stage, we will use the constructor to simulate the creation of records in a “persistent” medium, as described. Later in this chapter, we will develop the class further to provide access to the private fields according to our needs.

The code in the implementation section for the constructor and deconstructor is as follows. As you can see, we create a set of customers and items to resemble data retrieved from dynamic storage.

{ TDatabase }

constructor TDatabase.Create;
var
  tmpCustomer: TCustomer;
  tmpItem: TItem;
begin
  inherited;


  fCustomers:=TObjectList<TCustomer>.Create;  

  //Create mock customers
  tmpCustomer:=TCustomer.Create;
  tmpCustomer.ID:=1;
  tmpCustomer.Name:='John';
  tmpCustomer.DiscountRate:=12.50;
  tmpCustomer.Balance:=-Random(5000);
  fCustomers.Add(tmpCustomer);


  tmpCustomer:=TCustomer.Create;
  tmpCustomer.ID:=2;
  tmpCustomer.Name:='Alex';
  tmpCustomer.DiscountRate:=23.00;
  tmpCustomer.Balance:=-Random(2780);
  fCustomers.Add(tmpCustomer);


  tmpCustomer:=TCustomer.Create;
  tmpCustomer.ID:=3;
  tmpCustomer.Name:='Peter';
  tmpCustomer.DiscountRate:=0.0;
  tmpCustomer.Balance:=-Random(9000);
  fCustomers.Add(tmpCustomer);


  tmpCustomer:=TCustomer.Create;
  tmpCustomer.ID:=4;
  tmpCustomer.Name:='Retail Customer';
  tmpCustomer.DiscountRate:=0.0;
  tmpCustomer.Balance:=0.0;
  fCustomers.Add(tmpCustomer);


  fItems:=TObjectList<TItem>.Create;
  //Create mock items to sell
  tmpItem:=TItem.Create;
  tmpItem.ID:=100;
  tmpItem.Description:='T-shirt';
  tmpItem.Price:=13.55;
  fItems.Add(tmpItem);


  tmpItem:=TItem.Create;
  tmpItem.ID:=200;
  tmpItem.Description:='Trousers';
  tmpItem.Price:=23.45;
  fItems.Add(tmpItem);


  tmpItem:=TItem.Create;
  tmpItem.ID:=300;
  tmpItem.Description:='Coat';
  tmpItem.Price:=64.00;
  fItems.Add(tmpItem);


  tmpItem:=TItem.Create;
  tmpItem.ID:=400;
  tmpItem.Description:='Shirt';
  tmpItem.Price:=28.00;
  fItems.Add(tmpItem);
end;


destructor TDatabase.Destroy;
begin
  fCustomers.Free;
  fItems.Free;
  inherited;
end;

Total Sales

The main screen of POSApp has a field that shows the total sales figure. If we want to implement this, we need to have a way to store the sales that we generate every time we issue an invoice. As before, in real-life applications, this figure is typically stored in a database or calculated on-the-fly by accessing a database. Here, I use a normal text file to keep track of the invoice amounts. This is, admittedly, a naïve and poor approach, but it will serve our purposes. Delphi provides a very handy way to meet our needs, by providing the TIniFile class.

Note

If you are not familiar with the TIniFile class in Delphi, check out this resource (DocWiki, 2016a).

In the TDatabase class, add one private field to hold the path and the filename of the file and two procedures to retrieve (GetTotalSales) the total amount and save (SaveCurrentSales) each invoice’s sales amount. You also need to update the constructor and the uses clause in the implementation section.

A371064_1_En_2_Figb_HTML.gif
A371064_1_En_2_Figc_HTML.gif
Note

When I present code, I attempt to use good programming practices whenever the scope justifies the complexity. In the previous code, I used the try...finally block to make sure the INI object is properly managed even if an exception is thrown. You can learn more about how to manage exceptions in this link (DocWiki, 2016b).

We initialize the value of fFullFileName to point to the folder of the executable file. If your target is set to Win32 and the build configuration is set to Debug, the POSApp.data file will be created in:

{the path you store the project files}Win32Debug

GetTotalSales opens the INI file and loads the Total Sales value from the Sales section of the file. SaveCurrentSales takes the amount of the current invoice and adds it to the saved sales figure.

The Main Form

The last thing we need to implement is a way for the main screen to update the total amount of sales. Return to the MainScreenForm unit and create a private procedure entitled UpdateTotalSales.

uses
  ..., Database;


type
  TMainForm = class(TForm)
    ...
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    procedure UpdateTotalSales;
  public
    { Public declarations }
  end;

Enter the following code in the OnCreate event and, then, develop UpdateTotalSales (click anywhere in the class declaration and press Ctrl+Shift+C. Delphi will automatically create the skeleton of the procedure).

procedure TMainForm.FormCreate(Sender: TObject);
begin
  UpdateTotalSales;
end;


procedure TMainForm.UpdateTotalSales;
var
  tmpSales: Currency;
  tmpDatabase: TDatabase;
begin
  tmpSales:=0.00;


  tmpDatabase:=TDatabase.Create;
  try
    tmpSales:=tmpDatabase.GetTotalSales;
  finally
    tmpDatabase.Free;
  end;


  LabelTotalSalesFigure.Text:=Format('%10.2f',[tmpSales]);
end;

POSApp updates the total sales label when the form is created. You’ll also want this to happen when you complete an invoice in the InvoiceForm screen. You do this by adding a call to UpdateTotalSales in the OnClick event of the ButtonInvoice.

procedure TMainForm.ButtonInvoiceClick(Sender: TObject);
var
  ...
begin
  ...
  UpdateTotalSales;
end;

The Sales Invoice Form

Let’s move to the InvoiceForm unit. Open the form and go to the Code panel. In order to implement the invoice functionality, we need a number of variables to keep track of the invoice. The following fields are introduced in the private section of the TSalesInvoiceForm. You also need to update the uses part of the unit.

A371064_1_En_2_Figd_HTML.gif

We need to initialize the classes and free them when the form closes. Add the following code to the OnCreate and OnDestroy events of the form.

A371064_1_En_2_Fige_HTML.gif

An inspection of the InvoiceForm shows that there is a workflow that defines which fields the user can access and when. For example, there is no point to add items to an invoice before the user selects a customer (even a generic anonymous retail customer) or to apply a discount. This means that we need an initial setup of the properties of the components. Once the form is created, we can set up the interface. Add the following procedure to the private section of the form and complete the code.

A371064_1_En_2_Figf_HTML.gif

First, we set up the labels, clear the popup boxes, and disable the group boxes. Then we load the customer list (GetCustomerList) and the items (GetItems) from the database and update the relevant popup boxes.

Retrieving Data

Switch back to the Database unit and add the following functions to the TDatabase class. Now, we will add methods to expose the private fields we declared earlier.

interface
 ...


type
  TDatabase = class
  private
    ...
  public
    ...
    function GetCustomerList: TObjectList<TCustomer>;
    function GetItems: TObjectList<TItem>;
    ...
  end;


implementation

...

function TDatabase.GetCustomerList: TObjectList<TCustomer>;
begin
  result:=fCustomers;
end;


function TDatabase.GeTItems: TObjectList<TItem>;
begin
  result:=fItems;
end;

These functions simply return the lists from the TDatabase class. Back to InvoiceForm, the SetupGUI procedure cleans the edit field and the string grid and makes the animated progress indicator and the label at the bottom invisible. If you execute POSApp and open an invoice, you should be able to select a customer from the popup box.

The last thing that is left to do in the Customer Detail group box is to update the Discount Rate and the Outstanding Balance fields that appear when the user selects a customer name in the popup box. In the Database unit, the customer list is stored as an object list of TCustomer classes. When the user makes a selection in the popup box for the customers, we only have the name of the customer. Therefore, we need a way to get the TCustomer class from the name. Go back to the Database unit and add the following function.

interface

...

type
  TDatabase = class
  private
    ...
  public
    ...
    function GetCustomerFromName(const nameStr: string): TCustomer;
    ...
  end;


implementation

function TDatabase.GetCustomerFromName(const nameStr: string): TCustomer;
var
  tmpCustomer: TCustomer;
begin
  if not Assigned(fCustomers) then Exit;
  result:=nil;
  for tmpCustomer in fCustomers do
  begin
    if tmpCustomer.Name=nameStr then
    begin
      result:=tmpCustomer;
      exit;
    end;
  end;
end;

Now we have a way to retrieve the TCustomer class from the customer name. Switch to the InvoiceForm unit, click on the OnChange event of the PopupBoxCustomer component, and add the following code:

A371064_1_En_2_Figg_HTML.gif

The code is straightforward; after retrieving the appropriate TCustomer class, it updates the user interface accordingly and enables the relevant parts. The code also cleans the string grid when the customer popup menu changes.

Similar to the need we had earlier to retrieve the customer class from the name, we need to be able to get the item’s class from its description. In the Database unit, add the following method.

type
  TDatabase = class
  private
    ...
  public
    ...
    function GetItemFromDescription(const desc: string): TItem;
    ...
  end;
...


implementation
...


function TDatabase.GetItemFromDescription(const desc: string): TItem;
var
  tmpItem: TItem;
begin
  result:=nil;
  if not Assigned(fItems) then Exit;
  for tmpItem in fItems do
  begin
    if tmpItem.Description=desc then
    begin
      result:=tmpItem;
      exit;
    end;
  end;
end;

Updating the Form

The PopupBoxItems menu is enabled when the user selects a customer. We will add the code to update the list with the selected item and the quantity. Click on the Add Item button and add the following code.

A371064_1_En_2_Figh_HTML.gif
A371064_1_En_2_Figi_HTML.gif

The method applies a number of validation checks and adds the new invoice item to the fCurrentInvoiceItems list. We update the user interface (the string grid and the label with the total invoice amount) using the two procedures at the end of the previous method. You can develop the two procedures with the following code.

A371064_1_En_2_Figj_HTML.gif
A371064_1_En_2_Figk_HTML.gif

We need to add the code for the GetItemFromID procedure (Database unit), which appears in UpdateInvoiceGrid, as indicated previously.

type
  TDatabase = class
  private
    ...
  public
    ...
    function GetItemFromID(const id: Integer): TItem;
    ...
  end;
...


implementation
...


function TDatabase.GetItemFromID(const id: Integer): TItem;
var
  tmpItem: TItem;
begin
  result:=nil;
  if not Assigned(fItems) then Exit;
  for tmpItem in fItems do
  begin
    if tmpItem.ID=id then
    begin
      result:=tmpItem;
      exit;
    end;
  end;
end;

Run the application. You should be able to add items in the invoice and then view the updated invoice amount.

There are a few things left. In the InvoiceForm, select the PopupMenuItems popup menu component, open the Items Editor, and open a TMenuItem labeled Delete Entry. Select the menu item you created and implement the OnClick event.

interface
...


type
  TSalesInvoiceForm = class(TForm)
    ...
    procedure MenuItemDeleteItemClick(Sender: TObject);
  private
    ...
  public
    { Public declarations }
  end;


...

implementation

...
procedure TSalesInvoiceForm.MenuItemDeleteItemClick(Sender: TObject);
var
  tmpInvoiceItem: TInvoiceItem;
begin
  if (StringGridItems.Selected>=0) and
    (StringGridItems.Selected<=StringGridItems.RowCount-1) then
  begin
    for tmpInvoiceItem in fCurrentInvoiceItems do
      if tmpInvoiceItem.ID=
        StringGridItems.Cells[4,StringGridItems.Selected].ToInteger then
        begin
          fCurrentInvoiceItems.RemoveItem(tmpInvoiceItem, TDirection.FromBeginning);
          break;
        end;
  end;
  UpdateInvoiceGrid;
  UpdateBalance;
end;

Select the CheckBoxDiscount check box and write the OnChange event to update the discounted amount, as you have already considered the discount in UpdateBalance.

interface
...


type
  TSalesInvoiceForm = class(TForm)
    ...
    procedure CheckBoxDiscountChange(Sender: TObject);
  private
    ...
  public
    { Public declarations }
  end;


...

implementation

...
procedure TSalesInvoiceForm.CheckBoxDiscountChange(Sender: TObject);
begin
  UpdateBalance;
end;

When the user is ready to issue an invoice, they select the Print Invoice button. If they want to cancel it, they simply select Cancel. The code behind the OnClick events of the two buttons looks like this:

A371064_1_En_2_Figl_HTML.gif

The Cancel button simply closes the form and the Print Invoice button shows a message and stores the invoice amount to our “database”.

At this point, we have completed the POSApp. As you see, this implementation provides the code that accesses our database and performs calculations (the business logic) next to the code that updates the user interface (the presentation). If you want to replace the form with another one, you need to spend a lot of time and effort developing all the procedures of the new form. In fact, it does not have to be a completely new form. You can face the same difficulty if you replace components in the form.

For example, in the SalesInvoiceForm unit, we run a few calculations in several places and update the GUI elements accordingly. If, at some point, we want to show the discount rate in a pie chart or add a track bar to allow users to define the quantities to the invoice items, we would need to visit all those places in the form and amend the code that retrieves or shows this information. These kinds of complications, which are prone to errors and bugs, are minimized and even avoided with the MVVM approach.

Summary

In this chapter, we created the POSApp application with code that mixes business logic, presentation, and view states. In other words, at this stage, POSApp is a tightly coupled application.

By the end of this book, we will have a version of the application that allows us to replace the view or the graphical elements with minimal effort. In the next chapter, we will visit the foundations of a MVVM architecture in terms of coding and we develop a methodology that allows us to convert POSApp to an MVVM application.

References

Begel, A. and Nagappan, N., 2007. “Usage and Perceptions of Agile Software Development in an Industrial Context: An Exploratory Study,” First International Symposium on Empirical Software Engineering and Measurement (ESEM 2007), pp.255–264.

DocWiki, E., 2016a. “Using TIniFile and TMemIniFile,” available at http://docwiki.embarcadero.com/RADStudio/Seattle/en/Using_TIniFile_and_TMemIniFile [Accessed 17/03/2016].

DocWiki, E., 2016b. “Writing Exception Handlers,” available at http://docwiki.embarcadero.com/RADStudio/Seattle/en/Writing_Exception_Handlers [Accessed 17/03/2016].

Gerber, A., Van Der Merwe, A., and Alberts, R., 2007. “Practical Implications of Rapid Development Methodologies”.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset