© John Kouraklis 2016

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

7. Input Validation

John Kouraklis

(1)London, UK

The POSApp at this stage illustrates how we can use the MVVM pattern to capture user interaction. In this implementation, it doesn’t perform any checks on data entered by the users. For example, users could enter a negative number or even a character in the quantity field. This will generate an error and will block the application. This chapter explains how we can deal with this situation.

Checking Inputs

This section shows how to check the user-entered data against three validation rules: users must select an item before they press the Add button, the quantity field must not be empty, and the quantity must be a non-zero positive number.

  1. Use the project from the previous chapter (it’s also found in the code files as POSAppMVVMUserInteraction) and add new types. They will identify errors in Model.ProSu.InterfaceActions and declare a new class to handle notifications for errors in Model.Declarations.

    unit Model.ProSu.InterfaceActions;

    interface

    type
      ...
      TInterfaceError = (errInvoiceItemEmpty, errInvoiceItemQuantityEmpty,
                         errInvoiceItemQuantityNonPositive,
                         errInvoiceItemQuantityNotNumber, errNoError);
      TInterfaceErrors = set of TInterfaceError;


    Implementation

    end.

    unit Model.Declarations;

    interface
    ...


    type
      ...
      TErrorNotificationClass = class (TInterfacedObject, INotificationClass)
      private
        fActions: TInterfaceErrors;
        fActionMessage: string;
      public
        property Actions: TInterfaceErrors read fActions write fActions;
        property ActionMessage: string read fActionMessage write fActionMessage;
      end;


    implementation

    end.
  2. Add two procedures in the interface part of IInvoiceViewModelInterface in Model.Interfaces. Both procedures accept a string as an argument. We will pass whatever the popup box and the edit field provide. Remember that the View (InvoiceForm) is not (and should not be) aware of the type of data users enter; in other words, the form does not know that the quantity edit field must be a number.

    IInvoiceViewModelInterface = interface
        ...
        procedure ValidateItem (const newItem: string);
        procedure ValidateQuantity (const newQuantityText: string);
    end;
  3. The ViewModel performs the checks on the strings. In bigger and more complex applications, the Model can do validation and perform checks as well.

  4. Create a procedure in ViewModel.Invoice to send error messages to subscribers.

    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
        procedure SendErrorNotification (const errorType: TInterfaceErrors;
                                             const errorMessage: string);
      public
        ...
      end;
    ...


    procedure TInvoiceViewModel.SendErrorNotification (const errorType: TInterfaceErrors;
                                             const errorMessage: string);
    var
      tmpErrorNotificationClass: TErrorNotificationClass;
    begin
      tmpErrorNotificationClass:=TErrorNotificationClass.Create;
      try
        tmpErrorNotificationClass.Actions:=errorType;
        tmpErrorNotificationClass.ActionMessage:=errorMessage;
      fProvider.NotifySubscribers(tmpErrorNotificationClass);
      finally
        tmpErrorNotificationClass.Free;
      end;
    end;
  5. Write the following code in the validation procedures.

    procedure TInvoiceViewModel.ValidateItem(const newItem: string);
    begin
      if trim(newItem)='' then
        SendErrorNotification([errInvoiceItemEmpty], 'Please choose an item')
      else
        SendErrorNotification([errNoError], '');
    end;


    procedure TInvoiceViewModel.ValidateQuantity(const newQuantityText: string);
    var
      value,
      code: integer;
    begin
      if trim(newQuantityText)='' then
      begin
        SendErrorNotification([errInvoiceItemQuantityEmpty], 'Please enter quantity');
        Exit;
      end;


      Val(trim(newQuantityText), value, code);
      if code<>0 then
      begin
        SendErrorNotification([errInvoiceItemQuantityNotNumber], 'Quantity must be a number');
        Exit;
      end;


      if trim(newQuantityText).ToInteger<=0 then
      begin
        SendErrorNotification([errInvoiceItemQuantityNonPositive],
                                          'The quantity must be positive number');
        Exit;
      end;


      SendErrorNotification([errNoError], '');
    end;
  6. Now the only thing we need to do is process the error signals in View.InvoiceForm. We already have a procedure to manage signals from the provider (NotificationFromProvider); thus, we just update it accordingly.

    A371064_1_En_7_Figa_HTML.gif
  7. In this design, the validation check reports on any errors by sending out an action that indicates an error (errInvoiceItemEmpty, errInvoiceItemQuantityEmpty, errInvoiceItemQuantityNonPositive, or errInvoiceItemQuantityNotNumber) or errNoError to show there is no error. The process takes place in the NotificationFromProvider procedure, so we must not add the item in the ButtonAddClick event. The process of adding the item is now handled by the NotificationFromProvider method.

    A371064_1_En_7_Figb_HTML.gif

Bits and Pieces

We have now completed the major tasks in the InvoiceForm. There are a few left before we have a fully rewritten version of the initial monolithic design of POSApp. These last tasks include a way to delete items from an invoice, apply discount to the total amount of the invoice, print the invoice and close the form.

Deleting an Item from the Invoice

When the user right-clicks on the invoice item list, a popup menu appears with an option to delete the selected item. In order to implement this functionality, follow the next steps:

  1. Add DeleteInvoiceItem in IInvoiceModelInterface in Model.Interfaces.

      IInvoiceModelInterface = interface
        ...
        procedure DeleteInvoiceItem (const delItemID: integer);
      end;
  2. Develop the procedure in Model.Invoice.

    implementation

    ...

    type
      TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
      private
        ...
      public
        ...
        procedure DeleteInvoiceItem (const delItemID: integer);
        ...
      end;


    ...

    procedure TInvoiceModel.DeleteInvoiceItem(const delItemID: integer);
    var
      i: integer;
    begin
      if delItemID<=0 then
        Exit;
      for i := 0 to fCurrentInvoiceItems.Count-1 do
      begin
        if fCurrentInvoiceItems.Items[i].ID=delItemID then
        begin
          fCurrentInvoiceItems.Delete(i);
          break;
        end;
      end;
    end;
  3. Back in Model.Interfaces, add a similar DeleteInvoiceItem for the ViewModel. Notice that this time, the procedure gets text as an argument because this is what the View can feed in to the ViewModel as it gets data from a string grid.

      IInvoiceViewModelInterface = interface
        ...
        procedure DeleteInvoiceItem (const delItemIDAsText: string);
      end;
  4. In ViewModel.Invoice, add the code to DeleteInvoiceItem.

    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
      public
        ...
        procedure DeleteInvoiceItem (const delItemIDAsText: string);
      end;
    ...


    procedure TInvoiceViewModel.DeleteInvoiceItem(const delItemIDAsText: string);
    begin
      if (trim(delItemIDAsText)='') then
        Exit;
      fModel.DeleteInvoiceItem(delItemIDAsText.ToInteger);
      SendNotification([actInvoiceItemsChanges]);
    end;
  5. In View.Invoice, we only need to call the DeleteInvoiceItem from the ViewModel. Then, the ViewModel will notify the View that there is a change to the invoice items and the balances.

  6. Add an event handler to the MenuItemDeleteItem menu item of the PopupMenuItems popup menu component in View.InvoiceForm.

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


    ...

    procedure TSalesInvoiceForm.MenuItemDeleteItemClick(Sender: TObject);
    begin


      if (StringGridItems.Selected>=0) and
          (StringGridItems.Selected<=StringGridItems.RowCount-1) then
        fViewModel.DeleteInvoiceItem(StringGridItems.Cells[4, StringGridItems.Selected]);
    end;

Applying Discounts to the Invoices

The discount check box signals POSApp to apply the customer discount, which then appears in the top part of the form. We will implement this functionality by declaring a property in the ViewModel and the Model and changing the ViewModel’s property from the View.

  1. Declare a property in Model.Interfaces for the IInvoiceModelInterface and IInvoiceViewModelInterface.

    IInvoiceModelInterface = interface
        ...
        function GetInvoiceDiscount: Currency;
        ...
        property InvoiceDiscount: Currency read GetInvoiceDiscount;
      end;


    IInvoiceViewModelInterface = interface
        ...
        procedure SetDiscountApplied (const discount: boolean);
        function GetDiscountApplied: boolean;
        ...
        property DiscountApplied: boolean read GetDiscountApplied write SetDiscountApplied;
    end;
  2. Add the code for the procedure and the function in the ViewModel.Invoice unit.

    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
        fDiscountChecked: boolean;
        ...
        procedure SetDiscountApplied (const discount: boolean);
        function GetDiscountApplied: boolean;
      public
        ...
      end;
    ...


    function TInvoiceViewModel.GetDiscountApplied: boolean;
    begin
      result:=fDiscountChecked;
    end;


    procedure TInvoiceViewModel.SetDiscountApplied(const discount: boolean);
    begin
      fDiscountChecked:=discount;
    end;
  3. In Model.Declarations, add a field to TInvoiceItemsText.

    TInvoiceItemsText = record
        ...
        InvoiceDiscountFigure,
        InvoiceTotalBalance: string;
      end;
  4. Develop the procedures declared in the Model.Invoice unit. Notice the auxiliary procedure, which provides the customer record based on customerID (GetCustomerFromID).

    A371064_1_En_7_Figc_HTML.gif
  5. In ViewModel.Invoice, we need to develop the getter and setter of the DiscountApplied property. We also need to update the GetInvoiceItemsText function to include the discount in the calculations.

    type
      TInvoiceViewModel = class(TInterfacedObject, IInvoiceViewModelInterface)
      private
        ...
        procedure SetDiscountApplied (const discount: boolean);
        function GetDiscountApplied: boolean;
      public
        ...
      end;


      ...

    function TInvoiceViewModel.GetDiscountApplied: boolean;
    begin
      result:=fDiscountChecked;
    end;


    procedure TInvoiceViewModel.SetDiscountApplied(const discount: boolean);
    begin
      fDiscountChecked:=discount;
    end;


    function TInvoiceViewModel.GetInvoiceItemsText: TInvoiceItemsText;
    var
      ...
      tmpDiscount: Currency;
    begin
      ...
      tmpRunning:=0.00;
      tmpDiscount:=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;

      if fDiscountChecked then
        tmpDiscount:=fModel.InvoiceDiscount;


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


      fPrintButtonEnabled:=fModel.NumberOfInvoiceItems > 0;

      Result:=fInvoiceItemsText;
    end;
  6. Move to View.InvoiceForm and write the change event of the CheckBoxDiscount. Then update the UpdateBalances to include the discount figure and the status of the check box.

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


    procedure TSalesInvoiceForm.CheckBoxDiscountChange(Sender: TObject);
    begin
      fViewModel.DiscountApplied:=CheckBoxDiscount.IsChecked;
      fInvoiceItemsText:=fViewModel.InvoiceItemsText;
      UpdateBalances;
    end;


    procedure TSalesInvoiceForm.UpdateBalances;
    begin
      ...
      LabelDiscount.Text:=fInvoiceItemsText.InvoiceDiscountFigure;
      CheckBoxDiscount.IsChecked:=fViewModel.DiscountApplied;
    end;

Compile the project and execute it. Add a customer who has a discount, add a few items, and check and uncheck the discount check box. You should be able to see that the amount is updated.

Printing the Invoice and Closing the Form

When the user attempts to print an invoice, the ViewModel changes the visibility of the animated indicator and the printing label and pushes the request to the Model. Then, the ViewModel informs the View that the process is complete. A confirmation message appears and the invoice form sends a message to the main form to update the sales figure. Eventually, the form closes. Once again, the starting point is the interface declarations.

  1. In Model.Interfaces, declare two identical procedures (PrintInvoice), one for the Model and one for the ViewModel.

    IInvoiceModelInterface = interface
        ...
        procedure PrintInvoice;
      end;


    IInvoiceViewModelInterface = interface
        ...
        procedure PrintInvoice;
    end;
  2. Implement PrintInvoice in the Model.Invoice unit.

    type
      TInvoiceModel = class (TInterfacedObject, IInvoiceModelInterface)
      private
        ...
        procedure PrintInvoice;
      public
        ...
      end;
    ...
    procedure TInvoiceModel.PrintInvoice;
    begin
      fDatabase.SaveCurrentSales(fRunningBalance-fDiscount);
    end;
  3. Based on the original POSApp developed in Chapter 2, we want the animated indicator and printing label to appear when the user prints an invoice. In the MVVM design, this means that the View needs to know when to update the status of the controls, which is received from the ViewModel. Once again, the ProSu framework developed earlier is handy here. The ViewModel will notify the View to update the status of the components.

  4. A similar behavior is expected after the invoice has been printed. We need to declare a new interface action in Model.ProSu.InterfaceActions, as follows.

    A371064_1_En_7_Figd_HTML.gif
  5. The ViewModel.Invoice unit manipulates the state of the View, accesses the Model, and implements the View logic.

    A371064_1_En_7_Fige_HTML.gif
  6. In View.Invoice, we process the notifications in the NotificationFromProvider procedure and write the PrintInvoice click event.

    type
      TSalesInvoiceForm = class(TForm)
        ...
        procedure ButtonPrintInvoiceClick(Sender: TObject);
      private
        ...
      end;
    ...


    procedure TSalesInvoiceForm.ButtonPrintInvoiceClick(Sender: TObject);
    begin
      fViewModel.PrintInvoice;
    end;
    ...


    procedure TSalesInvoiceForm.NotificationFromProvider(
      const notifyClass: INotificationClass);
    ...
    begin
      if notifyClass is TNotificationClass then
      begin
        tmpNotifClass:=notifyClass as TNotificationClass;
        if actInvoiceItemsChanges in tmpNotifClass.Actions then
          UpdateInvoiceGrid;


        if actPrintingStart in tmpNotifClass.Actions then
        begin
          AniIndicatorProgress.Visible:=fViewModel.AniIndicatorProgressVisible;
          LabelPrinting.Visible:=fViewModel.LabelPrintingVisible;
        end;


        if actPrintingFinish in tmpNotifClass.Actions then
        begin
          ShowMessage('Invoice Printed');
          AniIndicatorProgress.Visible:=fViewModel.AniIndicatorProgressVisible;
          LabelPrinting.Visible:=fViewModel.LabelPrintingVisible;
          self.Close;
        end;
      end;
    ...
    end;
  7. The last thing we need to do is update the total sales figure in the MainForm. This time, the InvoiceForm sends a message to the MainForm to perform the update. In the ProSu design, the InvoiceForm is the provider and the MainForm plays the role of the subscriber.

  8. In View.InvoiceForm, add the following code. We also need to declare Model.ProSu.Provider in the uses clause.

    type
      TSalesInvoiceForm = class(TForm)
        ...
      private
        ...
        fProvider: IProviderInterface
        ...
        procedure UpdateMainBalance;
      public
        property Provider: IProviderInterface read fProvider;
      end;
    ...
    implementation


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


    procedure TSalesInvoiceForm.NotificationFromProvider(
      const notifyClass: INotificationClass);
    ...
    begin
      if notifyClass is TNotificationClass then
      begin
        tmpNotifClass:=notifyClass as TNotificationClass;
        if actInvoiceItemsChanges in tmpNotifClass.Actions then
          UpdateInvoiceGrid;


        if actPrintingStart in tmpNotifClass.Actions then
        begin
          AniIndicatorProgress.Visible:=fViewModel.AniIndicatorProgressVisible;
          LabelPrinting.Visible:=fViewModel.LabelPrintingVisible;
        end;


        if actPrintingFinish in tmpNotifClass.Actions then
        begin
          ShowMessage('Invoice Printed');
          AniIndicatorProgress.Visible:=fViewModel.AniIndicatorProgressVisible;
          LabelPrinting.Visible:=fViewModel.LabelPrintingVisible;
          UpdateMainBalance;
          self.Close;
        end;
      end;
    ...
    end;
    ...


    procedure TSalesInvoiceForm.SetViewModel(
      const newViewModel: IInvoiceViewModelInterface);
    begin
      ...
      fProvider:=CreateProSuProviderClass;
    end;
    procedure TSalesInvoiceForm.UpdateMainBalance;
    var
      tmpNotificationClass: TNotificationClass;
    begin
      tmpNotificationClass:=TNotificationClass.Create;
      tmpNotificationClass.Actions:=[actUpdateTotalSalesFigure];
      if Assigned(fProvider) then
       fProvider.NotifySubscribers(tmpNotificationClass);
      tmpNotificationClass.Free;
    end;
  9. Finally, in View.MainForm, subscribe the form to the InvoiceForm’s provider and retrieve the total sales figure directly from the ViewModel.

    A371064_1_En_7_Figf_HTML.gif
    A371064_1_En_7_Figg_HTML.gif

The final touch is to create the event for the Cancel button. This is a straightforward call to close the View.InvoiceForm.

type
  TSalesInvoiceForm = class(TForm)
    ...
    procedure ButtonCancelClick(Sender: TObject);
  private
    ...
  end;
...


procedure TSalesInvoiceForm.ButtonCancelClick(Sender: TObject);
begin
  self.Close;
end;

Summary

We have now completed the development of POSApp under the MVVM approach. In this chapter, we moved one step ahead from user interaction and saw how the MVVM paradigm, the ProSu pattern, and the bi-directional exchange of messages among the components of MVVM can help validate user input. We implemented different types of validations and evaluated how the View and ViewModel can sync when we need to complete processes involving different steps, such as when printing an invoice.

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

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