Step 8 - Saving customer data and error handling
Now you're ready to save the data when you click the Save button. In this step we want to handle the following:
- Capture the POSTed form data
- Bind the form data back to our business object
- Validate the data
- Display any error information
- Save the data to the database
Capturing raw POST data
In its simplest form you can use Request.Form()
, Request.FormDate()
, Request.FormChecked()
and Request.FormSelected()
to capture raw input variables:
IF Request.IsPostback()
poCustomer.oData.Company = Request.Form("Company")
poCustomer.oData.Entered = Request.FormDate("Entered")
poCustomer.oData.IsActive = Request.FormChecked("Active")
...
ENDIF
While this works there are a few problems with this. First, if there is some sort of binding error and the value cannot be found or converted you don't get notified. And secondly - this process is tedious if you're dealing with a large form and has to be maintained everytime a new field is added.
There's a better way.
Using Request.UnbindFormVars()
So rather you can use bulk unbinding using the Request.UnbindFormVars() which unbinds form variables to an object and provides you with error information. We can then use that error information to display errors on the individual controls as well as an error summary.
FUNCTION EditCustomer()
*** Querystring and Form Vars
lcId = Request.Params("id")
loCustBus = CREATEOBJECT("cCustomer")
IF !loCustBus.Load(lcId)
*** if no customer exists lets create a new one
loCustBus.New()
ENDIF
PRIVATE poErrors, poCustomer
*** Error display handler for binding errors and messages
poError = CREATEOBJECT("HtmlErrorDisplayConfig")
poCustomer = loCustBus.oData
IF (Request.IsPostBack())
poError.Errors = Request.UnbindFormVars(loCustBus.oData)
IF !loCustBus.Validate()
poError.Errors.AddErrors( loCustBus.oValidationErrors )
ENDIF
IF poError.Errors.Count < 1
IF !loCustBus.Save()
poError.Message = loCustBus.cErrorMsg
ENDIF
ENDIF
IF (poError.Errors.Count > 0)
poError.Message = poError.Errors.ToHtml()
poError.Header = "Please fix the following form entry errors"
ELSE
poError.Message = "Customer saved."
poError.Icon = "info"
*** display message then reload list
Response.AppendHeader("Refresh","2;url=customerlist.ctd")
ENDIF
ENDIF
Response.ExpandScript()
ENDFUNC
The interesting stuff in this code happens in between the IF Request.IsPostback()
block. The code starts by unbinding the form variables into the loCustBus.oData object (same as poCustomer instance) using Request.UnbindFormVars()
. This function matches form variables with the same name to object properties and return a collection of binding errors which can occur if you unbind values and there's a type conflict (trying to unbind characters into a number or an invalid date). UnbindFormVars unbinds only simple values, and child objects are not unbound. For child objects you can use additional UnbindFormVars() calls using object prefixes in the varnames (ie. Customer.Address.Street).
Business Object Validation
Next the code tries to validate the updated values on .oData
member the business object by calling the .Validate()
method. By default this method is empty and no validation occurs.
But... you can override the .Validate()
method to create validation rules so it's easy to verify whether the object can be saved to disk. Here are a few simple ruless to apply in our cCustomer
class:
************************************************************************
* cCustomer :: Validate
****************************************
FUNCTION Validate()
DODEFAULT()
*** Always clear errors first in case you're reusing
this.oValidationErrors.Clear()
IF EMPTY(this.oData.Company)
this.AddValidationError("Company can't be left empty.","Company")
ENDIF
IF EMPTY(this.oData.LastName)
this.AddValidationError("Lastname must be provided.","LastName")
ENDIF
IF EMPTY(this.oData.FirstName)
this.AddValidationError("Firstname must be provided.","FirstName")
ENDIF
*** Date over 20 years old not allowed
IF this.oData.Entered < DATE() - 20 * 365
this.AddValidationError("Entered date can't be longer than 20 years ago","Entered")
ENDIF
IF THIS.oValidationErrors.Count > 0
this.SetError( this.oValidationErrors.ToString() )
RETURN .F.
ENDIF
RETURN .T.
You run any code to validate the values of the object. This can be any code and it could make queries to the database to validate more complex rules. You essentially use .AddValidationErrors()
to add errors. You provide an error message and a field name that it applies to. The field name can then be mapped to a control on the page.
Once this is inplace you can then validate the business object simply with:
IF !loCustBus.Validate()
poError.AddErrors( loCustBus.oValidationErrors )
ENDIF
This assigns the Validation Errors to the poError.Errors
collection which is a collection of wwValidationError objects. The two collections are simply combined in this call.
If the error count for the combined collection is greater than 0 there are errors and we can't save the Customer, but rather display an error message, so the code sets the poError.Message
and .Header
properties to cause an error message to be displayed.
The form then displays the binding errors as a summary as well as associated with the individual controls that have an error:
You can then fix the errors and resubmit the form to save the customers.
When all the information is valid, this simple code saves the customer information to the database:
IF poError.Errors.Count < 1
IF !loCustBus.Save()
poError.Message = loCustBus.cErrorMsg
ENDIF
ENDIF
In good keeping with separation of concerns your business logic related to saving data should be wrapped up in the business logic so that you don't have to handle the data access in this code. The .Save()
method by default saves the current oData member to to the database via GATHER NAME
in FoxPro, which in this case is all we need. In more complex scenarios you probably will override the .Save()
method to add additional logic for saving child records or other related information.
Handling Confirmation and Error Display
Once we've either saved or determined there's an error we have to notify the user so the last block of code in the postback block deals with that:
IF (poError.Errors.Count > 0)
poError.Message = poError.Errors.ToHtml()
poError.Header = "Please fix the following form entry errors"
ELSE
poError.Message = "Customer saved."
poError.Icon = "info"
*** display message then reload list
Response.AppendHeader("Refresh","2;url=customerlist.ctd")
ENDIF
This code sets properties on the poError object depending on success or failure. On failure we set the Message detail to a list of errors - both binding errors and validation errors serialized into an HTML list - as well as adding a header. That gives the display you see in the last screen shot with the error box on top.
This is then displayed with the simple:
<%= HtmlErrorDisplay(poError) %>
which takes the Message, the Header and Icon properties to render the alert box at the top of the page. Each of the binding or validation errors is displayed via the < %= HtmlBindingError() % >
expressions in the page after each field.
<div class="col-sm-7">
<%= HtmlTextBox("FirstName",poCustomer.FirstName,
[class="form-control" placeholder="Enter the first name"]) %>
<%= HtmlBindingError("FirstName",poError.Errors) %>
</div>
Success Message and Redirect
When there are no errors and the customer info has been saved, we still want to display a quick note that the save operation worked, and then navigate back to the customer page. To do this I first display the page with the success message by setting the .Message
and .Icon
properties of the poError
object to display the success. Then I use an HTTP header to force the page to automatically refresh after a couple of seconds:
poError.Message = "Customer saved."
poError.Icon = "info"
*** reload list after 2 seconds
Response.AppendHeader("Refresh","2;url=customerlist.ctd")
And voila, we've saved our data.
© West Wind Technologies, 1996-2024 • Updated: 01/18/16
Comment or report problem with topic