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.
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.
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;
The ViewModel performs the checks on the strings. In bigger and more complex applications, the Model can do validation and perform checks as well.
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;
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;
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.
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.
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:
Add DeleteInvoiceItem in IInvoiceModelInterface in Model.Interfaces.
IInvoiceModelInterface = interface
...
procedure DeleteInvoiceItem (const delItemID: integer);
end;
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;
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;
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;
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.
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.
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;
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;
In Model.Declarations, add a field to TInvoiceItemsText.
TInvoiceItemsText = record
...
InvoiceDiscountFigure,
InvoiceTotalBalance: string;
end;
Develop the procedures declared in the Model.Invoice unit. Notice the auxiliary procedure, which provides the customer record based on customerID (GetCustomerFromID).
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;
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.
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;
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;
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.
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.
The ViewModel.Invoice unit manipulates the state of the View, accesses the Model, and implements the View logic.
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;
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.
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;
Finally, in View.MainForm, subscribe the form to the InvoiceForm’s provider and retrieve the total sales figure directly from the ViewModel.
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.