© John Kouraklis 2016

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

6. User Interaction

John Kouraklis

(1)London, UK

Views (including forms, the console, and other means of data presentation) are created to present information and data to users and to allow them to interact with the software. User interaction is an integral part of every design pattern, including MVVM. As presented in Chapter 4, the way this project implements two-way communication between the different elements of the pattern is by using the Provider-Subscriber (ProSu) framework. In this chapter, we learn how to put ProSu into action and implement user interaction.

Selecting a Customer

When the user selects a customer from the popup box, POSApp retrieves the discount rate and the outstanding balance from the database and updates the relevant fields in the InvoiceForm. It enables the group boxes for the invoice items and the balances, clears the grid of any items left, and resets the discount check box.

  1. Following Figure 4-2, the View (InvoiceForm) works as the subscriber and the ViewModel is the provider. Use the project we developed in the previous chapter or open POSAppMVVMInvoiceForm from the code that comes with the book. Go to Model.Interfaces and declare a property to hold the provider class and a getter method. We also need to add the relevant units in the uses section.

    unit Model.Interfaces;

    interface

    uses
      ..., Model.ProSu.Interfaces;


    type
      ...
      IInvoiceViewModelInterface = interface
        ...
        function GetProvider: IProviderInterface;
        ...
        property Provider: IProviderInterface read GetProvider;


      end;
  2. In the ViewModel.Invoice unit, add the Model.ProSu.Interfaces and Model.ProSu.Provider unit references, declare a private variable in the TInvoiceViewModel class to hold the provider class, and develop the method declared in the interface of the class. In addition, add the code in the constructor to initiate the provider class.

    unit ViewModel.Invoice;

    interface

    ...

    implementation

    uses
      ...
      Model.ProSu.Interfaces, Model.ProSu.Provider;


    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
        fProvider: IProSuProviderInterface;
        ...
        function GetProvider: IProviderInterface;
      public
        …
      end;
    ...


    constructor TInvoiceViewModel.Create;
    begin
       ...
      fProvider:=CreateProSuProviderClass;
    end;


    function TInvoiceViewModel.GetProvider: IProviderInterface;
    begin
      result:=fProvider;
    end;


    ...
    end.
  3. In the View.InvoiceForm unit, add the following code.

    unit View.InvoiceForm;

    interface

    ...

    type
      TSalesInvoiceForm = class(TForm)
        ...
      private
        ...
        fSubscriber: ISubscriberInterface;
        ...
      public
        ...
      end;


    implementation

    ...

    procedure TSalesInvoiceForm.SetViewModel(
      const newViewModel: IInvoiceViewModelInterface);
    begin
      fViewModel:=newViewModel;
      if not Assigned(fViewModel) then
        raise Exception.Create('Sales Invoice View Model is required');
      fSubscriber:=CreateProSuSubscriberClass;
      fViewModel.Provider.Subscribe(fSubscriber);
      ...
    end;

    We have now a communication channel that starts from the ViewModel and ends to the View. Next, we need to retrieve the details of the customer who is selected in the customer popup box.

  4. Go to Model.Declarations and create this record.

    unit Model.Declarations

    ...

    interface
    ...


    type
      ...
      TCustomerDetailsText = record
        DiscountRate,
        OutstandingBalance: string;
      end;


    implementation

    end.
  5. In Model.Interfaces, declare the GetCustomer procedure.

    unit Model.Interfaces

    ...

    interface

    ...
    type
      ...
     IInvoiceModelInterface = interface
        ...
        procedure GetCustomer (const customerName: string; var customer: TCustomer);
      end;


      IInvoiceViewModelInterface = interface
        ...
        procedure GetCustomerDetails (const customerName: string; var customerDetails: TCustomerDetailsText);
      end;


    implementation

    end.
  6. In Model.Invoice, develop the actual procedure.

    unit Model.Invoice;

    interface

    ...

    implementation

    uses
      ..., System.SysUtils;


    type
      TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
      private
        ...
      public
       ...
       procedure GetCustomer (const customerName: string; var customer: TCustomer);
      end;


    ...

    procedure TInvoiceModel.GetCustomer(const customerName: string;
      var customer: TCustomer);
    begin
      if trim(customerName)='' then
        customer:=nil
      else
      begin
        customer.ID:=fDatabase.GetCustomerFromName(trim(customerName)).ID;
        customer.Name:=fDatabase.GetCustomerFromName(trim(customerName)).Name;
    customer.DiscountRate:=fDatabase.GetCustomerFromName(trim(customerName)).DiscountRate;customer.Balance:=fDatabase.GetCustomerFromName(trim(customerName)).Balance;


        fInvoice.CustomerID:=customer.ID;
      end;
    end;


    end.
  7. In ViewModel.Invoice, retrieve the customer class and convert the data according to the View logic.

    A371064_1_En_6_Figa_HTML.gif
  8. Getting the labels for the components in the customer detail group box in View.InvoiceForm is very similar to initializing the text and captions of the InvoiceForm components. We write a new UpdateCustomerDetails procedure, which is initially called in SetupGUI to reset the fields. For this to work, I have declared a private fCustomerDetailsText field and move the uses declaration in the interface section. In addition, a method to reset the string grid is introduced (CleanInvoiceGrid).

    A371064_1_En_6_Figb_HTML.gif

The Model is the part of the design that accesses the persistent medium. Neither the ViewModel nor the View are aware how and from which sources datasets are retrieved. If you check the code of the GetCustomerDetails in the ViewModel, you can easily see that this is the part where we decide how information is going to be presented in the View. In other words, we have encapsulated the View logic of the information in the ViewModel. Similarly, the UpdateCustomerDetails method in the form is agnostic about the actual content of the fields to be presented and which group boxes should be activated, deactivated, or disabled; this is done at the ViewModel level.

Adding an Item to the Invoice

After a customer is selected, the user needs to add items in the invoice. We have a popup box menu with the items, a field for the quantity, and a button to add the item to the invoice. The added item appears in the string grid.

The Model

Managing the items of the invoice is part of the business logic and, thus, is the duty of the Model. Therefore, we need procedures to add an item to the invoice, to delete items, to retrieve the number of the items in an invoice, and to calculate the total amount of the invoice.

  1. Add the following code in the IInvoiceModelInterface declaration in the Model.Interfaces unit.

      IInvoiceModelInterface = interface
        ...
        procedure AddInvoiceItem(const itemDescription: string; const quantity: integer);
        procedure GetInvoiceItems (var itemsList: TObjectList<TInvoiceItem>);
        procedure DeleteAllInvoiceItems;
        procedure CalculateInvoiceAmounts;
        function GetInvoiceRunningBalance:Currency;
        function GetNumberOfInvoiceItems: integer;
        property InvoiceRunningBalance: Currency read GetInvoiceRunningBalance;
        property NumberOfInvoiceItems: integer read GetNumberOfInvoiceItems;
     end;
  2. Develop the code in the Model.Invoice unit.

    type
      TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
      private
        ...
        fRunningBalance: Currency;
        function GetInvoiceRunningBalance:Currency;
        function GetNumberOfInvoiceItems: integer;
      public
        ...
        procedure AddInvoiceItem(const itemDescription: string; const quantity: integer);
        procedure GetInvoiceItems (var itemsList: TObjectList<TInvoiceItem>);
        procedure GetInvoiceItemFromID (const itemID: Integer; var item: TItem);
        procedure DeleteAllInvoiceItems;
        procedure CalculateInvoiceAmounts;
     end;


    ...

    procedure TInvoiceModel.AddInvoiceItem(const itemDescription: string; const quantity: integer);
    var
      tmpInvoiceItem: TInvoiceItem;
      tmpItem: TItem;
    begin
      if trim(itemDescription)='' then
        Exit;


      tmpItem:=fDatabase.GetItemFromDescription(trim(itemDescription));
      if not Assigned(tmpItem) then
        Exit;


      tmpInvoiceItem:=TInvoiceItem.Create;
      tmpInvoiceItem.ID:=tmpItem.ID;
      tmpInvoiceItem.InvoiceID:=fInvoice.ID;
      tmpInvoiceItem.UnitPrice:=tmpItem.Price;
      tmpInvoiceItem.Quantity:=quantity;


      fCurrentInvoiceItems.Add(tmpInvoiceItem);

    end;

    procedure TInvoiceModel.GetInvoiceItems(
      var itemsList: TObjectList<TInvoiceItem>);
    var
      tmpInvoiceItem: TInvoiceItem;
      i: integer;
    begin
      if not Assigned(itemsList) then
        Exit;
      itemsList.Clear;
      for i:=0 to fCurrentInvoiceItems.Count-1 do
      begin
        tmpInvoiceItem:=TInvoiceItem.Create;
        tmpInvoiceItem.ID:=fCurrentInvoiceItems.Items[i].ID;
        tmpInvoiceItem.InvoiceID:=fCurrentInvoiceItems.Items[i].InvoiceID;
        tmpInvoiceItem.ItemID:=fCurrentInvoiceItems.Items[i].ItemID;
        tmpInvoiceItem.UnitPrice:=fCurrentInvoiceItems.Items[i].UnitPrice;
        tmpInvoiceItem.Quantity:=fCurrentInvoiceItems.Items[i].Quantity;


        itemsList.Add(tmpInvoiceItem);
      end;
    end;


    procedure TInvoiceModel.GetInvoiceItemFromID(const itemID: Integer;
      var item: TItem);
    var
      tmpItem: TItem;
    begin
      if not Assigned(item) then
        Exit;
      tmpItem:=fDatabase.GetItemFromID(itemID);
      if Assigned(tmpItem) then
      begin
        item.ID:=tmpItem.ID;
        item.Description:=tmpItem.Description;
        item.Price:=tmpItem.Price
      end;
    end;


    procedure TInvoiceModel.CalculateInvoiceAmounts;
    var
      tmpItem: TInvoiceItem;
    begin
      fRunningBalance:=0.00;
      for tmpItem in fCurrentInvoiceItems do
        fRunningBalance:=fRunningBalance+(tmpItem.Quantity*tmpItem.UnitPrice);
    end;


    function TInvoiceModel.GetInvoiceRunningBalance: Currency;
    begin
      CalculateInvoiceAmounts;
      Result:=fRunningBalance;
    end;


    procedure TInvoiceModel.DeleteAllInvoiceItems;
    begin
      fCurrentInvoiceItems.Clear;
    end;


    function TInvoiceModel.GetNumberOfInvoiceItems: integer;
    begin
      result:=fCurrentInvoiceItems.Count;
    end;

The ViewModel

  1. Add and develop the following procedures in the Model.Interface and ViewModel.Invoice units.

    unit Model.Interface;

    implementation

    ...

    type
      ...
      IInvoiceViewModelInterface = interface
        ...
        procedure AddInvoiceItem(const itemDescription: string; const quantity: integer);
        procedure DeleteAllInvoiceItems;
      end;


    implementation

    end.

    unit ViewModel.Invoice;

    interface
    ...


    implementation

    ...

    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
      public
        ...
        procedure AddInvoiceItem(const itemDescription: string; const quantity: integer);
        procedure DeleteAllInvoiceItems;
      end;


    procedure TInvoiceViewModel.AddInvoiceItem(const itemDescription: string;
      const quantity: integer);
    begin
      fModel.AddInvoiceItem(itemDescription, quantity);
    end;


    procedure TInvoiceViewModel.DeleteAllInvoiceItems;
    begin
      fModel.DeleteAllInvoiceItems;
    end;
  2. The ViewModel is doing all the required work to prepare the data into a form suitable for presentation. Suitable in this case means that the ViewModel should present the data to the View in such way that it could be shown in the string grid of the form. This illustrates the flexibility of the MVVM pattern; we use the ViewModel layer to adjust and manipulate the presentation state of the data according to the requirements of the View without the need to change the structural elements of the View (or the Model).

  3. The string grid has five columns and receives strings. There are many ways to prepare data for this constellation. I will use a set of arrays that map to the columns of the string grid. Admittedly, this is not the best way to achieve this effect, but in this case, it is adequate as a demonstration of the functionality of the ViewModel.

  4. Open Model.Declarations and add the following record.

    unit Model.Declarations;

    interface

    ...

    type
      ...


      TInvoiceItemsText = record
        DescriptionText,
        QuantityText,
        UnitPriceText,
        PriceText,
        IDText: array of string;
        InvoiceRunningBalance,
        InvoiceTotalBalance: string;
      end;


    implementation

    end.
  5. Declare a property and a getter function in the Model.Interfaces unit and write the code for the procedure in ViewModel.Invoice.

    unit Model.Interfaces;

    interface

    ...

    type
      ...
      IInvoiceViewModelInterface = interface
        ...
        function GetInvoiceItemsText: TInvoiceItemsText;


        ...
        property InvoiceItemsText: TInvoiceItemsText read GetInvoiceItemsText;
      end;


    implementation

    end.

    unit ViewModel.Invoice;

    interface

    ...

    implementation

    uses
      ...


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


    ...

    function TInvoiceViewModel.GetInvoiceItemsText: TInvoiceItemsText;
    var
      tmpRunning: Currency;
      tmpInvoiceItems: TObjectList<TInvoiceItem>;
      i, tmpLen: integer;
      tmpItem: TItem;
    begin
      tmpLen:=0;
      SetLength(fInvoiceItemsText.DescriptionText,tmpLen);
      SetLength(fInvoiceItemsText.QuantityText,tmpLen);
      SetLength(fInvoiceItemsText.UnitPriceText,tmpLen);
      SetLength(fInvoiceItemsText.PriceText,tmpLen);
      SetLength(fInvoiceItemsText.IDText, tmpLen);
      tmpRunning:=0.00;


        tmpInvoiceItems:=TObjectList<TInvoiceItem>.Create;
      fModel.GetInvoiceItems(tmpInvoiceItems);
      for i := 0 to tmpInvoiceItems.Count-1 do
      begin
        tmpLen:=Length(fInvoiceItemsText.DescriptionText)+1;
        SetLength(fInvoiceItemsText.DescriptionText,tmpLen);
        SetLength(fInvoiceItemsText.QuantityText,tmpLen);
        SetLength(fInvoiceItemsText.UnitPriceText,tmpLen);
        SetLength(fInvoiceItemsText.PriceText,tmpLen);
        SetLength(fInvoiceItemsText.IDText, tmpLen);


        tmpItem:=TItem.Create;
        fModel.GetInvoiceItemFromID(tmpInvoiceItems.Items[i].ID, tmpItem);
        fInvoiceItemsText.DescriptionText[tmpLen-1]:=tmpItem.Description;
        tmpItem.Free;


        fInvoiceItemsText.QuantityText[tmpLen-1]:=tmpInvoiceItems.Items[i].Quantity.ToString;
        fInvoiceItemsText.UnitPriceText[tmpLen-1]:=format('%10.2f',[tmpInvoiceItems.Items[i].UnitPrice]);
        fInvoiceItemsText.PriceText[tmpLen-1]:=
             format('%10.2f',[tmpInvoiceItems.Items[i].UnitPrice*tmpInvoiceItems.items[i].Quantity]);
        fInvoiceItemsText.IDText[tmpLen-1]:=tmpInvoiceItems.Items[i].ID.ToString;
      end;
      tmpInvoiceItems.Free;


      tmpRunning:=fModel.InvoiceRunningBalance;

      fInvoiceItemsText.InvoiceRunningBalance:=Format('%10.2f', [tmpRunning]);
      fInvoiceItemsText.InvoiceTotalBalance:=Format('%10.2f', [tmpRunning]);


      fPrintButtonEnabled:=fModel.NumberOfInvoiceItems > 0;

      Result:=fInvoiceItemsText;
    end;
  6. The last procedure demonstrates the typical manipulation of data from the Model at the level of the ViewModel to represent the View logic. It also changes the status of the Print button to synchronize the View state with the state of the data.

The View

In the InvoiceForm, we need to retrieve the updated data from the ViewModel and present it in the form.

In View.InvoiceForm, declare a private variable called fInvoiceItemsText and add code to the click event of the Add button. We also need a procedure to update the items in the string grid (UpdateInvoiceGrid) and a procedure to update the total balances of the invoice (UpdateBalances). The following code (indicated in bold) also updates the SetViewModel procedure to call UpdateBalances in order to initialize the labels with the invoice’s balances.

A371064_1_En_6_Figc_HTML.gif
A371064_1_En_6_Figd_HTML.gif

The simplest way to update the grid with the invoice items is to call UpdateInvoiceGrid in the ButtonAddItemClick event. We are not going to follow this approach. Instead, we will ask the ViewModel to inform the View that there is a change to the invoice items and, therefore, it’s time to refresh the string grid.

In order to achieve this effect, we will use the ProSu framework developed in Chapter 4.

  1. Add an action (actInvoiceItemsChanged) in Model.ProSu.InterfaceActions to signify the need to update the grid with the invoice items.

    unit Model.ProSu.InterfaceActions;

    interface

    type
      TInterfaceAction = (actUpdateTotalSalesFigure, actInvoiceItemsChanged);
      TInterfaceActions = set of TInterfaceAction;


    implementation

    end.
  2. In View.InvoiceForm, declare a new procedure (NotificationFromProvider) that will be used to trigger actions from the message provider, register it with the provider, and write code to update the grid.

    A371064_1_En_6_Fige_HTML.gif
  3. In ViewModel.Invoice, create a new procedure to send out messages to subscribers (SendNotification). Call it from the AddInvoiceItem and DeleteAllInvoiceItems, as follows.

    A371064_1_En_6_Figf_HTML.gif

Compile POSApp and execute it. Choose a customer from the popup box. Select an item and try to add it to the invoice. You should be able to see that the grid updates the items and balances.

You may argue that there is no need to create the notification loop to get an update of the invoice items’ grid in this View. We could very easily retrieve fViewModel.InvoiceItemsText in the View and publish the data. This is correct and it would work very well, too. The reason I chose to use ProSu here is because I wanted to show how the ViewModel (or the Model) could initiate communication.

For example, in a real application, we may have a situation in which item prices change in real-time due to availability and demand. Because of the way we constructed this application, we could easily implement this scenario. The ViewModel would send an actInvoiceItemsChanged to report on any updates even if the user was in the middle of issuing an invoice.

Note

There is a small glitch in the GUI at this stage. If you select a customer and add a few items to the invoice, you can see the balance. Selecting another customer from the popup box clears the grid, but doesn't initialize the balance. This is because we don't delete the invoice items from the Model. To fix this, add the following lines in the PopupBoxCustomerChange procedure in View.InvoiceForm.

procedure TSalesInvoiceForm.PopupBoxCustomerChange(Sender: TObject);
begin


  fViewModel.GetCustomerDetails(PopupBoxCustomer.Text,fCustomerDetailsText);
  fViewModel.DeleteAllInvoiceItems;
  PopupBoxItems.ItemIndex:=-1;
  UpdateCustomerDetails;
end;

Summary

We took some big steps in this chapter. We converted the most important parts of InvoiceForm in a way that builds boundaries between business logic, view state, and view logic. We also saw how the methodology we developed in the previous chapter, along with the tools and concepts we learned earlier in the book, all fit together to serve the purpose of MVVM design.

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

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