© John Kouraklis 2016

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

5. Converting the InvoiceForm

John Kouraklis

(1)London, UK

In the previous chapters, we developed a methodology to guide us in converting an application to follow the MVVM design. In summary, those steps are the following:

  1. Identify the different tasks for each procedure, class, and view the application performs.

  2. Identify which of those tasks are duties of the Model, the ViewModel, and the View, according to the MVVM paradigm.

  3. Consider moving procedures and functions from the View to the ViewModel and the Model.

  4. Build the required links to make the new code functional on the normal direction View-ViewModel-Model by developing the necessary properties, procedures, and functions.

  5. Add functionality to the new code to support two-way communication between the View-ViewModel-Model according to the design needs of the application.

The View of the InvoiceForm

This section starts the convertion of the InvoiceForm by parameterizing the labels and the button captions of the form, as we did with the main screen. Figure 5-1 shows how the View retrieves the captions of the labels and buttons from the ViewModel and the Model. The approach is similar to Figure 3-1. We will also use dummy label values as we did before (Figure 3-1).

A371064_1_En_5_Fig1_HTML.jpg
Figure 5-1. InvoiceForm in the MVVM design

Follow these steps:

  1. Open the project you developed in the previous chapter or load POSAppMVVMFullInterfaces from the code files that come with the book.

  2. Add a new form to the project and save it as View.InvoiceForm in the Views folder.

  3. Open the form in the IDE and add the components (labels, group boxes, and buttons) as you did in Figure 2-6 using “dummy” labels.

    Tip

    In the book’s code files, you will find the InvoiceForm files (View.InvoiceForm.fmx and View.InvoiceForm.pas) in the Thin Forms folder. For convenience, you can import them into your project instead of creating the form from scratch.

  4. Open View.MainForm, click on the ButtonInvoice button, and add the following code to the ButtonInvoiceClick event. You also need to add the View.InvoiceForm in the Implementation uses clause.

    unit View.MainForm;

    interface

    ...

    type
      TMainForm = class(TForm)
        ...
        procedure ButtonInvoiceClick(Sender: TObject);
        ...
      end;


    ...

    implementation

    uses
      ..., View.InvoiceForm;


    {$R *.fmx}

    { TMainForm }

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


    ...

    end.

If you compile and run the project at this stage, you can click on the Issue Invoice button in the main screen and get the invoice form with the “dummy” labels.

Tip

In the code files, you can open the POSAppMVVMStart project. This project implements the previous steps and you can use it to work on the changes introduced in the following pages. The POSAppMVVMInvoiceForm project includes the changes we will incorporate in this chapter.

The Model of the InvoiceForm

We now need to provide the captions and the text for InvoiceForm’s labels. As explained, we will use the same approach we implemented when we considered how to allow translations of the label captions in the MainScreen form.

Open the Model.Declarations unit and define a record to keep the labels’ values and the buttons’ captions. You can use Figure 5-1 as a guide.

unit Model.Declarations;

interface

...

type
  ...


  TInvoiceFormLabelsText = record
    Title,
    CustomerDetailsGroupText,
    CustomerText,
    CustomerDiscountRateText,
    CustomerOutstandingBalanceText,


    InvoiceItemsGroupText,
    InvoiceItemsText,
    InvoiceItemsQuantityText,
    InvoiceItemsAddItemButtonText,
    InvoiceItemsGridItemText,
    InvoiceItemsGridQuantityText,
    InvoiceItemsGridUnitPriceText,
    InvoiceItemsGridAmountText,


    BalanceGroupText,
    BalanceInvoiceBalanceText,
    BalanceDiscountText,
    BalanceTotalText,


    PrintInvoiceButtonText,
    PrintingText,
    CancelButtonText: string;
  end;


implementation
...


end.
  1. Create a new unit and save it as Model.Invoice.pas under the Models folder. This hosts the Model of the InvoiceForm.

  2. Load the Model.Interfaces unit and declare a new interface for the Model of the invoice form. For now, we need only one function, which provides access to the labels of the form.

    unit Model.Interfaces;

    interface

    ...

    type
      ...


      IInvoiceModelInterface = interface
        ['{A286914B-7979-4726-8D9C-18865B47CD12}']
        function GetInvoiceFormLabelsText: TInvoiceFormLabelsText;
      end;


    implementation

    end.
  3. In the Model.Invoice unit, add the following code. We define the class for the Model of the invoice form and a function to allow access.

    unit Model.Invoice;

    interface

    uses
      Model.Interfaces;


    function CreateInvoiceModelClass: IInvoiceModelInterface;

    implementation

    uses
      Model.Declarations;


    type
      TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
      private
        fInvoiceFormLabelsText: TInvoiceFormLabelsText;
      public
        function GetInvoiceFormLabelsText: TInvoiceFormLabelsText;
      end;


    function CreateInvoiceModelClass: IInvoiceModelInterface;
    begin
      result:=TInvoiceModel.Create;
    end;


    { TInvoiceModel }

    function TInvoiceModel.GetInvoiceFormLabelsText: TInvoiceFormLabelsText;
    begin
      fInvoiceFormLabelsText.Title:='Sales Invoice';
      fInvoiceFormLabelsText.CustomerDetailsGroupText:='Customer Details';
      fInvoiceFormLabelsText.CustomerText:='Customer:';
      fInvoiceFormLabelsText.CustomerDiscountRateText:='Discount Rate:';
      fInvoiceFormLabelsText.CustomerOutstandingBalanceText:='Outstanding Balance:';


      fInvoiceFormLabelsText.InvoiceItemsGroupText:='Invoice Items';
      fInvoiceFormLabelsText.InvoiceItemsText:='Item:';
      fInvoiceFormLabelsText.InvoiceItemsQuantityText:='Quantity:';
      fInvoiceFormLabelsText.InvoiceItemsAddItemButtonText:='Add Item';


      fInvoiceFormLabelsText.InvoiceItemsGridItemText:='Item';
      fInvoiceFormLabelsText.InvoiceItemsGridQuantityText:='Quantity';
      fInvoiceFormLabelsText.InvoiceItemsGridUnitPriceText:='Unit Price';
      fInvoiceFormLabelsText.InvoiceItemsGridAmountText:='Amount';


      fInvoiceFormLabelsText.BalanceGroupText:='Balance';
      fInvoiceFormLabelsText.BalanceInvoiceBalanceText:='Invoice Balance:';
      fInvoiceFormLabelsText.BalanceDiscountText:='Discount';
      fInvoiceFormLabelsText.BalanceTotalText:='Total:';


      fInvoiceFormLabelsText.PrintInvoiceButtonText:='Print Invoice';
      fInvoiceFormLabelsText.PrintingText:='Printing Invoice…';
      fInvoiceFormLabelsText.CancelButtonText:='Cancel';


      result:=fInvoiceFormLabelsText;
    end;


    end.

The ViewModel of the InvoiceForm

We have created the two side components of the design (Model and View). Now we need to bring these two together. This is the job of the ViewModel of the invoice form, which will fetch the labels and the captions from the Model and feed them to the View. In order for this to happen, we need access to the Model and a way to retrieve the labels. We will do the last part by declaring a property.

  1. Open the Model.Interfaces unit and declare the appropriate interface.

    unit Model.Interfaces;

    interface
    ...


    type
      IInvoiceViewModelInterface = interface
        ['{87D2F27E-8B33-46C5-B44C-DBFC58A871BC}']
        function GetModel: IInvoiceModelInterface;
        procedure SetModel(const newModel: IInvoiceModelInterface);
        function GetLabelsText: TInvoiceFormLabelsText;
        property Model: IInvoiceModelInterface read GetModel write SetModel;
        property LabelsText: TInvoiceFormLabelsText read GetLabelsText;
      end;


    implementation

    end.
  2. Create a new unit called ViewModel.Invoice and develop the ViewModel as per the following code.

    unit ViewModel.Invoice;

    interface

    uses
      Model.Interfaces;


    function CreateInvoiceViewModelClass: IInvoiceViewModelInterface;

    implementation

    uses
      Model.Declarations;


    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        fModel: IInvoiceModelInterface;
        fLabelsText: TInvoiceFormLabelsText;
        function GetModel: IInvoiceModelInterface;
        procedure SetModel(const newModel: IInvoiceModelInterface);
        function GetLabelsText: TInvoiceFormLabelsText;
      public
        property Model: IInvoiceModelInterface read GetModel write SetModel;
        property LabelsText: TInvoiceFormLabelsText read GetLabelsText;
      end;


    function CreateInvoiceViewModelClass: IInvoiceViewModelInterface;
    begin
      result:=TInvoiceViewModel.Create;
    end;


    { TInvoiceViewModel }

    function TInvoiceViewModel.GetLabelsText: TInvoiceFormLabelsText;
    begin
        result:=fModel.GetInvoiceFormLabelsText;
    end;


    function TInvoiceViewModel.GetModel: IInvoiceModelInterface;
    begin
      result:=fModel;
    end;


    procedure TInvoiceViewModel.SetModel(const newModel: IInvoiceModelInterface);
    begin
      fModel:=newModel;
    end;


    end.

This code follows the design patterns we developed in the previous chapters—it uses interfaces, hides the class implementation inside the units, and creates loose connections between the View, the ViewModel, and the Model.

Retrieving the Labels from the ViewModel

We are now ready to update the invoice form with the labels and the captions as they are provided by the ViewModel. Switch to the View.InvoiceForm unit and add the following code.

A371064_1_En_5_Figa_HTML.gif
A371064_1_En_5_Figb_HTML.gif

This code will change the labels and the captions of the components of the form but only when we make the form, the ViewModel, and the Model aware of each other. In the View.MainForm unit, update the ButtonInvoiceClick event to create the different elements.

A371064_1_En_5_Figc_HTML.gif

If you execute POSApp, you should be able to see the correct labels and captions in the invoice form. There are a few points in the code to note:

  • The ButtonInvoiceClick event declares the Model, the ViewModel, and the View of the invoice form as local variables. There are many developers who think that the Model and the ViewModel should be declared as private variables of the form. There is no doubt this can also be done. My choice is to use local variables because I prefer to have organized and neat code whenever the scope of the variables is limited, as it is in this case.

  • In the same event, we independently create the Model, the ViewModel, and the View and then continue with the assignments. This is one approach to the issue of the order of creation. In the MVVM community, there are discussions regarding which part of the pattern you create next (View-ViewModel-Model). For example, in this case, you could create the ViewModel and assign it to the form. The ViewModel would then create the Model and assign it to itself. This approach has its place in MVVM coding, but it is not always easy to implement.

  • The task of retrieving the values of the labels and the captions from the Model is quite trivial. As you can observe, in this case, the ViewModel just passes the values from the Model to the View without any processing. In the broader community of developers (not only among Delphi programmers), many feel that in cases like this, the MVVM pattern generates additional coding without offering any significant advantages. Sometimes, they refer to such situations with the term boilerplate code. It may be true that there are simple cases in which MVVM does not appear to increase efficiency. In general, MVVM thrives whenever significant manipulation of data and information is required to generate loosely coupled, complex outputs and sophisticated user interfaces. The pattern performs well in such cases because it offers a middle layer (ViewModel) that gives space to developers to maneuver according to their needs without touching the Model or the View. For example, in the case with the labels, imagine you have two separate models that offer English and Greek labels but that the Greek translation is incomplete. The ViewModel could retrieve the Greek labels and fill in the gaps from the English model.

Setting Up the Invoice Form

Following the current methodology, before we break up the different parts of the form to meet the MVVM design rules, we need to identify which steps the form is implementing. Looking at the FormCreate event, you can see that there are two major components that are being initialized when the form is created—the classes and the graphical elements of the form (see Figure 5-2).

A371064_1_En_5_Fig2_HTML.gif
Figure 5-2. Initial set up of InvoiceForm

FormCreate initializes the required classes to track an invoice (the database, invoice, and invoice items classes). Then, the SetupGUI procedure is called, which changes the title of the form, initializes the popup boxes (the customer and items), disables the invoice items (the group box, the balance group box, and the Print Invoice button), and initializes the invoice balance label. The procedure then retrieves the customer and items lists from the database, populates the lists in the relevant popup boxes, and sets the initial values of those elements. It sets the quantity to one, clears the grid and, then hides the animated indicator and the label with the “Printing...” text.

Next, we need to clarify which components of the MVVM pattern are responsible for performing these tasks. This may be straightforward in some cases, but in other situations a more complicated approach may be required. Figure 5-3 inspects the FormCreate event and identifies that the event defines three classes that are part of the business logic (the Model) of the invoice form. Therefore, we need to declare and initialize these classes in the Model.

A371064_1_En_5_Fig3_HTML.gif
Figure 5-3. The FormCreate event in the MVVM design

Open the Model.Invoice unit and declare the following variables in the private section of the TInvoiceModel class. You also need to add System.Generics.Collections in the Implementation uses clause and implement the constructor and the destructor of the class.

unit Model.Invoice;

interface

...

implementation

uses
  ..., System.Generics.Collections;


type
  TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
  private
    ...
    fDatabase: IDatabaseInterface;
    fInvoice: TInvoice;
    fCurrentInvoiceItems: TObjectList<TInvoiceItem>;
  public
    ...
    constructor Create;
    destructor Destroy; override;
  end;


constructor TInvoiceModel.Create;
begin
  fDatabase:=CreateDatabaseClass;
  fInvoice:=TInvoice.Create;
  fInvoice.ID:=1;
  fInvoice.Number:=Random(3000);
  fCurrentInvoiceItems:=TObjectList<TInvoiceItem>.Create;
end;


destructor TInvoiceModel.Destroy;
begin
  fCurrentInvoiceItems.Free;
  fInvoice.Free;
  inherited;
end;
Note

In the Create event, we used an interfaced class for the TDatabase but a normal class for the TInvoice. This is because we decided in Chapter 4 to keep that class (and a few others) in their normal form.

You may also notice that we use a new database class in the TInvoiceModel class. This means that every time an invoice form is created, a new database class is going to be instantiated as well. Admittedly, this is not optimal design for a line-of-business application. In such cases, you typically have a separate class that provides access to the database and you inject it into every class, component, or procedure that requires access to the database. In some cases, you may keep the database connection open during the lifetime of the application. In POSApp, the database class is very generic and limited in scope, but it allows us to focus on the design pattern.

FormCreate calls SetupGUI procedure, which changes the title label of the invoice form to include the invoice number (see Figure 5-4). Changing the title is something that falls under the View and the invoice number comes from the Model. The choice of including the invoice number in the title is a very simple example of what is called View state and it is performed by the ViewModel.

A371064_1_En_5_Fig4_HTML.jpg
Figure 5-4. The first part of SetupGUI in MVVM

Open the Model.Interfaces unit and add the following function and property to the declarations of IInvoiceModelInterface and IInvoiceViewModelInterface.

type
  ...
  IInvoiceModelInterface = interface
    ...
    procedure SetInvoice(const newInvoice: TInvoice);
    procedure GetInvoice(var invoice: TInvoice);
  end;
  ...
  IInvoiceViewModelInterface = interface
    ...
    function GetTitleText: string;
    ...
    property TitleText: string read GetTitleText;
  end;

Move to the Model.Invoice unit and develop the GetInvoice and SetInvoice procedures as in the following code. Then, open ViewModel.Invoice and create the GetTitleText.

unit Model.Invoice;

interface

...

implementation

...

type
  TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
  ...
  public
    ...
    procedure SetInvoice(const newInvoice: TInvoice);
    procedure GetInvoice(var invoice: TInvoice);
  end;


...

procedure TInvoiceModel.GetInvoice(var invoice: TInvoice);
begin
  invoice:=fInvoice;
end;


procedure TInvoiceModel.SetInvoice(const newInvoice: TInvoice);
begin
  fInvoice:=newInvoice;
end;


...

end.

unit ViewModel.Invoice;

interface

...

implementation

...

type
  TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
  private
    ...
    function GetTitleText: string;
  public
    ...
  end;


...

function TInvoiceViewModel.GetTitleText: string;
var
  tmpInvoice: TInvoice;
begin
  fModel.GetInvoice(tmpInvoice);
  result:=fModel.GetInvoiceFormLabelsText.Title+' #'+IntToStr(tmpInvoice.Number)


end;

...

end.

GetTitleText creates the correct title for the InvoiceForm by accessing the invoice Model. The last step we need to implement is to call GetTitleText from the InvoiceForm. In the View.InvoiceForm, create a new private procedure called SetupGUI. In the initial version of the invoice form, SetupGUI is called in the constructor of the form. In this implementation, this will not work because we assign the ViewModel of the invoice form after we create the form. The appropriate location to call SetupGUI is at the end of the assignment of the ViewModel (SetViewModel). In addition, according to Figure 5-4, SetupGUI clears the two popup boxes and the string grid and sets the default value of the quantity edit field.

A371064_1_En_5_Figd_HTML.gif

Run POSApp and open an invoice form. You can now see the number of the invoice at the top of the form.

Disabling and Hiding Elements

The rest of the actions in the original SetupGUI follow the pattern used to update the title of the InvoiceForm. This section looks at how we can disable and hide elements of the form. Disabling and hiding are two states of the View components. Therefore, they represent view state and, as a consequence, they must be controlled by the ViewModel. It is this ViewModel which determines the View state by looking at data from the Model.

Open Model.Interfaces and declare the following properties and functions in IInvoiceViewModelInterface.

...

IInvoiceViewModelInterface = interface
    ...
    function GetGroupBoxInvoiceItemsEnabled: Boolean;
    function GetGroupBoxBalanceEnabled: Boolean;
    function GetButtonPrintInvoiceEnabled: Boolean;
    function GetAniIndicatorProgressVisible: Boolean;
    function GetLabelPrintingVisible: Boolean;


    ...
    property GroupBoxInvoiceItemsEnabled: boolean read GetGroupBoxInvoiceItemsEnabled;
    property GroupBoxBalanceEnabled: boolean read GetGroupBoxBalanceEnabled;
    property ButtonPrintInvoiceEnabled: Boolean read GetButtonPrintInvoiceEnabled;
    property AniIndicatorProgressVisible: Boolean read GetAniIndicatorProgressVisible;
    property LabelPrintingVisible: Boolean read GetLabelPrintingVisible;
  end;

Then, write the implementations of these functions in the ViewModel.Invoice unit. In this unit, you need to create the constructor of the class in order to set up the initial state of the components.

unit ViewModel.Invoice;

interface

...

implementation

...

type
  TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
  private
    ...
    fInvoiceItemsEnabled,
    fBalanceEnabled,
    fPrintButtonEnabled,
    fAniIndicatorVisible,
    fPrintingLabelVisible: boolean;
    ...
    function GetGroupBoxInvoiceItemsEnabled: Boolean;
    function GetGroupBoxBalanceEnabled: Boolean;
    function GetButtonPrintInvoiceEnabled: Boolean;
    function GetAniIndicatorProgressVisible: Boolean;
    function GetLabelPrintingVisible: Boolean;
  public
    constructor Create;
    ...
  end;


...

{ TInvoiceViewModel }

constructor TInvoiceViewModel.Create;
begin
  fInvoiceItemsEnabled:=false;
  fBalanceEnabled:=false;
  fPrintButtonEnabled:=false;
  fAniIndicatorVisible:=false;
  fPrintingLabelVisible:=false;
end;


function TInvoiceViewModel.GetAniIndicatorProgressVisible: Boolean;
begin
  result:=fAniIndicatorVisible;
end;


function TInvoiceViewModel.GetButtonPrintInvoiceEnabled: Boolean;
begin
  result:=fPrintButtonEnabled;
end;


function TInvoiceViewModel.GetGroupBoxBalanceEnabled: Boolean;
begin
  result:=fBalanceEnabled;
end;


function TInvoiceViewModel.GetGroupBoxInvoiceItemsEnabled: Boolean;
begin
  Result:=fInvoiceItemsEnabled;
end;


function TInvoiceViewModel.GetLabelPrintingVisible: Boolean;
begin
  result:=fPrintingLabelVisible;
end;
...


end.

View.InvoiceForm includes two procedures—UpdateGroups and UpdatePrinting. This gives us the flexibility to update the printing labels independently of the groups. The initial call of the procedures is done in SetViewModel.

unit View.InvoiceForm;

interface

...

type
  TSalesInvoiceForm = class(TForm)
    ...
  private
    ...
    procedure UpdateGroups;
    procedure UpdatePrintingStatus;
  public
    ...
  end;


implementation

...

procedure TSalesInvoiceForm.SetViewModel(
  const newViewModel: IInvoiceViewModelInterface);
begin
  ...
  UpdateGroups;
  UpdatePrintingStatus;
end;


procedure TSalesInvoiceForm.UpdateGroups;
begin
  GroupBoxInvoiceItems.Enabled:=fViewModel.GroupBoxInvoiceItemsEnabled;
  GroupBoxBalance.Enabled:=fViewModel.GroupBoxBalanceEnabled;
  ButtonPrintInvoice.Enabled:=fViewModel.ButtonPrintInvoiceEnabled;
end;


procedure TSalesInvoiceForm.UpdatePrintingStatus;
begin
  AniIndicatorProgress.Visible:=fViewModel.AniIndicatorProgressVisible;
  LabelPrinting.Visible:=fViewModel.LabelPrintingVisible;
end;


...

end.

Getting the Customer and Items Lists

We follow similar steps as before in order to get the lists of the customers and the items. We start from the interface of the Model.Invoice unit, where we expose procedures to retrieve data from the database. Then, we declare new properties and procedures in the ViewModel.Invoice interface and develop the relevant code in the units.

unit Model.Interfaces
...


IInvoiceModelInterface = interface
    ['{A286914B-7979-4726-8D9C-18865B47CD12}']
    function GetInvoiceFormLabelsText: TInvoiceFormLabelsText;
    procedure SetInvoice(const newInvoice: TInvoice);
    procedure GetInvoice(var invoice: TInvoice);
    procedure GetCustomerList(var customers: TObjectList<TCustomer>);
    procedure GetItems(var items: TObjectList<TItem>);
  end;


  IInvoiceViewModelInterface = interface
    procedure GetCustomerList(var customers: TObjectList<TCustomer>);
    procedure GetItems(var items: TObjectList<TItem>);
    ...
   end;
...
end.


unit Model.Invoice;

interface

...

implementation

...

type
  TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
  private
    ...
  public
    ...
    procedure GetCustomerList(var customers: TObjectList<TCustomer>);
    procedure GetItems(var items: TObjectList<TItem>);
  end;


...

procedure TInvoiceModel.GetCustomerList(var customers: TObjectList<TCustomer>);
begin
  customers:=fDatabase.GetCustomerList
end;


procedure TInvoiceModel.GetItems(var items: TObjectList<TItem>);
begin
  items:=fDatabase.GetItems
end;


...

end.

unit ViewModel.Invoice;

interface

...

implementation

...

type
  TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
  private
    ...
    procedure GetCustomerList(var customers: TObjectList<TCustomer>);
    procedure GetItems(var items: TObjectList<TItem>);
  public
    ...
  end;


...
procedure TInvoiceViewModel.GetCustomerList(
  var customers: TObjectList<TCustomer>);
begin
  if not Assigned(fModel) then
    Exit;
  fModel.GetCustomerList(customers);
end;


procedure TInvoiceViewModel.GetItems(var items: TObjectList<TItem>);
begin
  if not Assigned(fModel) then
    Exit;
  fModel.GetItems(items);
end;


end.

Summary

In this chapter, we started bringing the tools and skills developed in the previous chapters together. We formalized a methodology to convert code to the MVVM pattern and started applying it to the InvoiceForm. This transformation looked at content that does not change according to user interactions. In the following chapter, we will learn how to make the MVVM responsive to user events.

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

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