Validating Data Entry Input
October 21, 2011
Your application should perform validation on all user-entered data to help prevent errors.
You Will Learn
- How to perform validation on Windows Phone.
- How to check for unsaved changes when the user presses the Back button.
Applies To
Performing Validation
In general, it is useful to constrain the data that users can enter. The following are approaches that can be used to constrain input from the user.
- Set TextBox properties that constrain input such as the TextBox.MaxLength property.
- Bind to properties and validate before saving.
- Use the INotifyDataErrorInfo interface to perform custom validation for your objects.
The INotifyDataErrorInfo interface could be used to implement custom validation of business rules. However, in Fuel Tracker, the first two options were used. First, you
- Set the TextBox.MaxLength property so that users can enter only the expected amount of text.
- Set the TextBox.InputScope property so that users can enter only the expected type of data (such as numerical data). For more information, see Using Controls.
Even with these constraints, it is usually still possible to enter values that are incorrectly formatted or invalid for some other reason. Validation requirements often vary, but typically you want to validate data to prevent incorrect values from being saved. In this case, bindings are set directly to the appropriate data classes, which have float properties. On save, the entered values persist only if they can be parsed as floats.
The following code shows the SaveButton_Click event handler in the FillupPage class.
private void SaveButton_Click(object sender, EventArgs e)
{
CommitTextBoxWithFocus();
if (string.IsNullOrWhiteSpace(OdometerTextBox.Text))
{
MessageBox.Show("The odometer reading is required.");
return;
}
if (string.IsNullOrWhiteSpace(FuelQuantityTextBox.Text))
{
MessageBox.Show("The gallons value is required.");
return;
}
if (string.IsNullOrWhiteSpace(PricePerUnitTextBox.Text))
{
MessageBox.Show("The price per gallon value is required.");
return;
}
float val;
if (!float.TryParse(OdometerTextBox.Text, out val))
{
MessageBox.Show("The odometer reading could not be converted to a number.");
return;
};
if (!float.TryParse(FuelQuantityTextBox.Text, out val))
{
MessageBox.Show("The gallons value could not be converted to a number.");
return;
};
if (!float.TryParse(PricePerUnitTextBox.Text, out val))
{
MessageBox.Show("The price per gallon value could not be converted to " +
"a number.");
return;
};
SaveResult result = CarDataStore.SaveFillup(_currentFillup,
delegate
{
MessageBox.Show("There is not enough space on your phone to " +
"save your fill-up data. Free some space and try again.");
});
if (result.SaveSuccessful)
{
Microsoft.Phone.Shell.PhoneApplicationService.Current
.State[Constants.FILLUP_SAVED_KEY] = true;
NavigationService.GoBack();
}
else
{
string errorMessages = String.Join(
Environment.NewLine + Environment.NewLine,
result.ErrorMessages.ToArray());
if (!String.IsNullOrEmpty(errorMessages))
{
MessageBox.Show(errorMessages,
"Warning: Invalid Values", MessageBoxButton.OK);
}
}
}
Private Sub SaveButton_Click(ByVal sender As Object, ByVal e As EventArgs)
CommitTextBoxWithFocus()
If String.IsNullOrWhiteSpace(OdometerTextBox.Text) Then
MessageBox.Show("The odometer reading is required.")
Return
End If
If String.IsNullOrWhiteSpace(FuelQuantityTextBox.Text) Then
MessageBox.Show("The gallons value is required.")
Return
End If
If String.IsNullOrWhiteSpace(PricePerUnitTextBox.Text) Then
MessageBox.Show("The price per gallon value is required.")
Return
End If
Dim val As Single
If Not Single.TryParse(OdometerTextBox.Text, val) Then
MessageBox.Show("The odometer reading could not be converted to a " &
"number.")
Return
End If
If Not Single.TryParse(FuelQuantityTextBox.Text, val) Then
MessageBox.Show("The gallons value could not be converted to a number.")
Return
End If
If Not Single.TryParse(PricePerUnitTextBox.Text, val) Then
MessageBox.Show("The price per gallon value could not be converted " &
"to a number.")
Return
End If
Dim result As SaveResult = CarDataStore.SaveFillup(_currentFillup,
Sub()
MessageBox.Show("There is not enough space on your phone " &
"to save your fill-up data. Free some space and try again.")
End Sub)
If result.SaveSuccessful Then
Microsoft.Phone.Shell.PhoneApplicationService.Current _
.State(Constants.FILLUP_SAVED_KEY) = True
NavigationService.GoBack()
Else
Dim errorMessages = String.Join(
Environment.NewLine & Environment.NewLine,
result.ErrorMessages.ToArray())
If Not String.IsNullOrEmpty(errorMessages) Then
MessageBox.Show(errorMessages,
"Warning: Invalid Values", MessageBoxButton.OK)
End If
End If
End Sub
If values have been entered into each TextBox, an attempt is made to parse the values into values of type float. If parsing fails, a MessageBox is displayed indicating the failure to the user. If parsing succeeds, the code calls the SaveFillup method, passing it an error callback. This delegate simply displays the error condition in a message box.
If the save is successful, the SaveButton_Click method uses the application-level state dictionary to store a value indicating that a new fill-up has just been saved. Next, the method navigates backward. After the navigation, if the SummaryPage finds the value stored in the state dictionary, it shows the first pivot item so that the user can immediately see the results of the fill-up. Otherwise, the SummaryPage will automatically display whichever pivot item was showing before the navigation to the FillupPage.
In addition, business rule validation is also implemented. For example, new fill-up data is not added to the FillupHistory collection until the user taps the Save button. Before it is saved, however, the user can review the data to make sure it is accurate. When the user taps the Save button, a validation routine performs one last check for mistakes and alerts the user to any issues.
The following code shows a simple validation method used in the Fillup class.
public IList<string> Validate()
{
var validationErrors = new List<string>();
if (OdometerReading <= 0)
{
validationErrors.Add(
"The odometer value must be a number greater than zero.");
}
if (DistanceDriven <= 0)
{
validationErrors.Add(
"The odometer value must be greater than the previous value.");
}
if (FuelQuantity <= 0)
{
validationErrors.Add("The fuel quantity must be greater than zero.");
}
if (PricePerFuelUnit <= 0)
{
validationErrors.Add("The fuel price must be greater than zero.");
}
return validationErrors;
}
Public Function Validate() As IList(Of String)
Dim results = New List(Of String)
If OdometerReading <= 0 Then _
results.Add("The odometer value must be a number greater than zero.")
If DistanceDriven <= 0 Then _
results.Add("The odometer value must be greater than the previous value.")
If FuelQuantity <= 0 Then _
results.Add("The fuel quantity must be greater than zero.")
If PricePerFuelUnit <= 0 Then _
results.Add("The fuel price must be greater than zero.")
Return results
End Function
The following illustration shows an example of the message that is displayed when the Save button is tapped and the specified odometer reading is less than the previous value.
This code simply checks the values of the Fillup properties and returns error strings for any invalid values. However, not all of the values being checked are supplied by the user, so some initialization must occur beforehand. Specifically, the distance driven must be calculated by comparing the Fillup.OdometerReading value to the previous odometer reading. However, the current Fillup object does not have access to the previous reading, so this is performed by the CarDataStore.SaveFillup method instead, as shown in the following code.
public static SaveResult SaveFillup(Fillup fillup, Action errorCallback)
{
var lastReading =
Car.FillupHistory.Count > 0 ?
Car.FillupHistory.First().OdometerReading :
Car.InitialOdometerReading;
fillup.DistanceDriven = fillup.OdometerReading - lastReading;
var saveResult = new SaveResult();
var validationResults = fillup.Validate();
if (validationResults.Count() > 0)
{
saveResult.SaveSuccessful = false;
saveResult.ErrorMessages = validationResults;
}
else
{
Car.FillupHistory.Insert(0, fillup);
saveResult.SaveSuccessful = true;
SaveCar(delegate {
saveResult.SaveSuccessful = false;
errorCallback(); });
}
return saveResult;
}
Public Shared Function SaveFillup(ByVal fillup As Fillup,
ByVal errorCallback As Action) As SaveResult
Dim lastReading = If(
Car.FillupHistory.Count > 0,
Car.FillupHistory.First().OdometerReading,
Car.InitialOdometerReading)
fillup.DistanceDriven = fillup.OdometerReading - lastReading
Dim saveResult = New SaveResult
Dim validationResults = fillup.Validate
If validationResults.Count > 0 Then
saveResult.SaveSuccessful = False
saveResult.ErrorMessages = validationResults
Else
Car.FillupHistory.Insert(0, fillup)
saveResult.SaveSuccessful = True
SaveCar(Sub()
saveResult.SaveSuccessful = False
errorCallback()
End Sub)
End If
Return saveResult
End Function
The CarDataStore.SaveFillup method accepts a Fillup object to save and an Action delegate for error handling. This method starts by subtracting the previous odometer reading from the current odometer reading and stores the results in the Fillup.DistanceDriven property so that validation can occur in the class. Next, the SaveFillup method calls the Fillup.Validate method and stores the results in a special SaveResult object, which it returns to the caller. If there are no validation errors, the SaveFillup method saves the fill-up and car data. Finally, the SaveFillup method returns the SaveResult object.
Note
Silverlight for Windows Phone provides basic, built-in binding validation through the Binding.ValidatesOnExceptions property, as documented in the Silverlight Data Binding topic. However, the phone versions of the controls do not provide validation templates. To support validation, one option is to re-template the controls to provide your own validation template. For example, you can customize the desktop version of the template provided in the TextBox Styles and Templates topic.
This section describes one approach to validating data entry input, but there are numerous ways to perform validation. For example, Silverlight provides support for complex validation scenarios. For more information, see Data Binding.
Checking for Unsaved Changes When Navigating Backward
By default, pressing the Back button causes backward navigation. If you have a data entry page with unsaved changes and the user presses the Back button, you should inform the user that he is about to lose unsaved data. To handle this scenario, you can override the PhoneApplicationPage.OnBackKeyPress method and check for unsaved changes. If there are unsaved changes, you can inform the user and potentially cancel the backward navigation.
Tip
Do not use an OnBackKeyPress override to cancel all backward navigation and entirely replace the Back button behavior. The option to cancel backward navigation is provided in order to perform necessary navigation-related operations such as seeking user confirmation before discarding data. The back button must be used correctly to pass certification. See section 5.2 in the Technical Certification Requirements.
The Fuel Tracker application has two data entry pages: CarDetailsPage and FillupPage. If there are unsaved changes on these pages and the user press Back, a confirmation message is displayed that allows them to cancel the navigation backward. The following illustration shows an example of the confirmation message.
Design Guideline: |
---|
Supply a Cancel button for actions that overwrite or delete data or are irreversible. |
The following code shows how Fuel Tracker overrides the PhoneApplicationPage.OnBackKeyPress method to display a confirmation message if there is unsaved data.
protected override void OnBackKeyPress(
System.ComponentModel.CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If there are no changes, do nothing.
if (!_hasUnsavedChanges) return;
var result = MessageBox.Show("You are about to discard your " +
"changes. Continue?", "Warning", MessageBoxButton.OKCancel);
e.Cancel = (result == MessageBoxResult.Cancel);
}
Protected Overrides Sub OnBackKeyPress(
ByVal e As System.ComponentModel.CancelEventArgs)
MyBase.OnBackKeyPress(e)
' If there are no changes, do nothing.
If Not _hasUnsavedChanges Then Return
Dim result = MessageBox.Show("You are about to discard your " &
"changes. Continue?", "Warning", MessageBoxButton.OKCancel)
e.Cancel = (result = MessageBoxResult.Cancel)
End Sub
First, this method checks whether there are any unsaved changes. If there are none, then the method does nothing and backward navigation occurs automatically. Otherwise, the method displays a confirmation message. If the user taps Cancel, then the backward navigation is canceled. Otherwise, the navigation proceeds normally.