Showing posts with label validation. Show all posts
Showing posts with label validation. Show all posts

Wednesday, 12 November 2014

Form validation based on Material Design

I’ve extended the Material Design to include some form validation.

I’m undecided about using a valid icon or whether a lack of validation error message implies validity.  Perhaps I’m swayed by years of rubbish UX on the web that I think I’m currently in favour of using an icon.  And if an icon is to be used, how subtle should be it? 

material-design-with-validation-rules

Wednesday, 22 October 2014

Mail Chimp UX

Generally I like Mail Chimp as a company and their UX so I might add to this blog post over time.

Validation

image

Nothing ground breaking here and it could be more responsive to the user as they insert data but I quite liked the green tick on green background to indicate a valid password.  Perhaps the errors could be in a similar style.

Another data entry example

image

Home Page

image

Saturday, 10 May 2014

Angular Validation with ngMessage

Here is a screenshot of a business form I have written in Angular:

angularvalidation

For the live demo click here

For the source code click here

The validation is coded using the new ngMessage directive which displays the error messages should any be present.  Various rules are shown as an example: required, min/max length, regex and a faked database lookup.

ngMessages has only been introduced since AngularJS 1.3.0-beta.8

This example was originally based on a blog post by Year of Moo with a few of my own enhancements to match the behaviour of a previous form I had created.

I’ve customised the CSS to show the user what fields are required, along with what fields have been updated and are valid.  Each control only updates when the user leaves the control rather than on every keypress which is the default Angular behaviour. 

I also use ngCloak to ensure no output is displayed before Angular has compiled the page.

Thursday, 19 April 2012

Silverlight 5 Validation

Moving forward with .NET 4.5 the validation story will be played out through the INotifyDataErrorInfo interface.

http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifydataerrorinfo(v=vs.110).aspx

Here is an example of a Silverlight 5 grid and form using INDEI for its validation:

This demo can also be accessed online here.

Tricks

This example shows the following tricks when validating user input in Silverlight:

  • MVVM with independent validation rules allowing for contextual validation with injection into view model via the constructor
  • No validation is applied when first loading up the form
  • Validation is applied when each control is updated, with form wide validation being applied when the user clicks the Ok button
  • DatePicker control is loaded with no date and also prevents user textual input along with its validation
  • Fluent Validation uses length, email and regex rules to validate properties
  • View model implements INotifyPropertyChanged, INotifyDataErrorInfo and IEditableObject interfaces
  • INotifyPropertyChanged and INotifyDataErrorInfo implementations are stored within an abstract view model base class
  • Model implements bespoke generic ICloneable interface
  • Cancel (by pressing escape) on grid reverts the data and controls back to original state
  • RelayCommand has been used for the command buttons

Class diagram

image

Styles

This example explicitly contains all styles used in this solution, within the app.xaml file.

Screenshots

Description viewer

Used to show if a field is required and/or any of other relevant info, when the user hovers over the little circle with the i for information to the right of the control.

image

Validation Summary

Lists all validation errors on the grid and form.

image

image

Validation Tooltips

image

image

image

image

Validator code

using System;
using FluentValidation;

namespace SilverlightValidation
{
public class UserModelValidator : AbstractValidator<IUserModel>
{
public UserModelValidator()
{
RuleFor(x => x.Username)
.Length(3, 8)
.WithMessage("Must be between 3-8 characters.");

RuleFor(x => x.Password)
.Matches(@"^\w*(?=\w*\d)(?=\w*[a-z])(?=\w*[A-Z])\w*$")
.WithMessage("Must contain lower, upper and numeric chars.");

RuleFor(x => x.Email)
.EmailAddress()
.WithMessage("A valid email address is required.");

RuleFor(x => x.DateOfBirth)
.Must(BeAValidDateOfBirth)
.WithMessage("Must be within 100 years of today.");
}

private bool BeAValidDateOfBirth(DateTime? dateOfBirth)
{
if (dateOfBirth == null) return false;
if (dateOfBirth.Value > DateTime.Today || dateOfBirth < DateTime.Today.AddYears(-100))
return false;
return true;
}
}
}


ViewModelBase code

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace SilverlightValidation
{
public class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
#region INotifyPropertyChanged method plus event

public event PropertyChangedEventHandler PropertyChanged = delegate { };

protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

#endregion

#region INotifyDataErrorInfo methods and helpers

private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();

public void SetError(string propertyName, string errorMessage)
{
if (!_errors.ContainsKey(propertyName))
_errors.Add(propertyName, new List<string> { errorMessage });

RaiseErrorsChanged(propertyName);
}

protected void ClearError(string propertyName)
{
if (_errors.ContainsKey(propertyName))
_errors.Remove(propertyName);

RaiseErrorsChanged(propertyName);
}

protected void ClearAllErrors()
{
var errors = _errors.Select(error => error.Key).ToList();

foreach (var propertyName in errors)
ClearError(propertyName);
}

public void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { };

public IEnumerable GetErrors(string propertyName)
{
return _errors.ContainsKey(propertyName)
? _errors[propertyName]
: null;
}

public bool HasErrors
{
get { return _errors.Count > 0; }
}

#endregion
}
}


UserListViewModel code

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using SilverlightValidation.Commands;
using SilverlightValidation.Models;
using SilverlightValidation.Validators;
using SilverlightValidation.Views;
using GalaSoft.MvvmLight.Messaging;
using SilverlightValidation.Messages;

namespace SilverlightValidation.ViewModels
{
public class UserListViewModel
{
UserView window;

public UserListViewModel(IList<UserModel> models, UserModelValidator validator)
{
Data = new ObservableCollection<UserViewModel>();

foreach (var model in models)
Data.Add(new UserViewModel(model, validator));

AddCommand = new RelayCommand(AddCommandExecute);
DeleteCommand = new RelayCommand(DeleteCommandExecute);

Messenger.Default.Register<UserViewResponseMessage>(this, UserViewResponseMessageReceived);
}

private void UserViewResponseMessageReceived(UserViewResponseMessage userViewResponseMessage)
{
if (userViewResponseMessage.UserViewModel != null)
Data.Add(userViewResponseMessage.UserViewModel);
window.Close();
}

#region Properties

public ObservableCollection<UserViewModel> Data { get; set; }

public UserViewModel SelectedItem { get; set; }

#endregion

#region Commands

public ICommand AddCommand { get; set; }
public ICommand DeleteCommand { get; set; }

private void AddCommandExecute(object obj)
{
window = new UserView();
window.Show();
}

private void DeleteCommandExecute(object obj)
{
if (SelectedItem!=null)
Data.Remove(SelectedItem);
}

#endregion
}
}


UserViewModel

using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using FluentValidation;
using SilverlightValidation.Interfaces;
using SilverlightValidation.Validators;
using SilverlightValidation.Models;
using SilverlightValidation.Commands;
using GalaSoft.MvvmLight.Messaging;
using SilverlightValidation.Messages;

namespace SilverlightValidation.ViewModels
{
public class UserViewModel : ViewModelBase, IUserModel, IEditableObject
{
#region Fields

private readonly UserModelValidator _validator;
private UserModel _data;
private UserModel _backup;

#endregion

#region Constructor

public UserViewModel(UserModel model, UserModelValidator validator)
{
_validator = validator;
_data = model;
_backup = model.Clone();

OkCommand = new RelayCommand(OkCommandExecute);
CancelCommand = new RelayCommand(CancelCommandExecute);
}

#endregion

#region Methods

private void SetProperties(IUserModel source)
{
_data.Username = source.Username;
_data.Password = source.Password;
_data.Email = source.Email;
_data.DateOfBirth = source.DateOfBirth;
_data.Description = source.Description;
}

#endregion

#region Properties

private const string UsernameProperty = "Username";
public string Username
{
get { return _data.Username; }
set
{
if (_data.Username != value)
{
_data.Username = value;
RaisePropertyChanged(UsernameProperty);
IsChanged = true;
}

ClearError(UsernameProperty);
var validationResult = _validator.Validate(this, UsernameProperty);
if (!validationResult.IsValid)
validationResult.Errors.ToList().ForEach(x => SetError(UsernameProperty, x.ErrorMessage));
}
}

private const string PasswordProperty = "Password";
public string Password
{
get { return _data.Password; }
set
{
if (_data.Password != value)
{
_data.Password = value;
RaisePropertyChanged(PasswordProperty);
IsChanged = true;
}

ClearError(PasswordProperty);
var validationResult = _validator.Validate(this, PasswordProperty);
if (!validationResult.IsValid)
validationResult.Errors.ToList().ForEach(x => SetError(PasswordProperty, x.ErrorMessage));
}
}

private const string EmailProperty = "Email";
public string Email
{
get { return _data.Email; }
set
{
if (_data.Email != value)
{
_data.Email = value;
RaisePropertyChanged(EmailProperty);
IsChanged = true;
}

ClearError(EmailProperty);
var validationResult = _validator.Validate(this, EmailProperty);
if (!validationResult.IsValid)
validationResult.Errors.ToList().ForEach(x => SetError(EmailProperty, x.ErrorMessage));
}
}

private const string DateOfBirthProperty = "DateOfBirth";
public DateTime? DateOfBirth
{
get { return _data.DateOfBirth; }
set
{
if (_data.DateOfBirth != value)
{
_data.DateOfBirth = value;
RaisePropertyChanged(DateOfBirthProperty);
IsChanged = true;
}

ClearError(DateOfBirthProperty);
var validationResult = _validator.Validate(this, DateOfBirthProperty);
if (!validationResult.IsValid)
validationResult.Errors.ToList().ForEach(x => SetError(DateOfBirthProperty, x.ErrorMessage));
}
}

private const string DescriptionProperty = "Description";
public string Description
{
get { return _data.Description; }
set
{
if (_data.Description != value)
{
_data.Description = value;
RaisePropertyChanged(DescriptionProperty);
IsChanged = true;
}

ClearError(DescriptionProperty);
var validationResult = _validator.Validate(this, DescriptionProperty);
if (!validationResult.IsValid)
validationResult.Errors.ToList().ForEach(x => SetError(DescriptionProperty, x.ErrorMessage));
}
}

#endregion

#region Commands

public ICommand OkCommand { get; set; }
public ICommand CancelCommand { get; set; }

private void OkCommandExecute(object obj)
{
RefreshToViewErrors();

if (IsChanged && !HasErrors)
{
// save here
Messenger.Default.Send<UserViewResponseMessage>(
new UserViewResponseMessage() { UserViewModel = this });
}
}

// in case user hasn't touched the form
private void RefreshToViewErrors()
{
Username = _data.Username;
Password = _data.Password;
Email = _data.Email;
DateOfBirth = _data.DateOfBirth;
}

private void CancelCommandExecute(object obj)
{
Messenger.Default.Send<UserViewResponseMessage>(
new UserViewResponseMessage() { UserViewModel = null });
}

#endregion

private void ResetFormData()
{
SetProperties(_backup);
ClearAllErrors();
IsChanged = false;
}

public bool IsChanged { get; private set; }

#region IEditableObject for datagrid

private bool inEdit;
public void BeginEdit()
{
if (inEdit) return;
inEdit = true;
}

public void CancelEdit()
{
if (!inEdit) return;
inEdit = false;
ResetFormData();
}

public void EndEdit()
{
if (!inEdit) return;
}

#endregion
}
}


Model code

using System;
using System.ComponentModel;

namespace SilverlightValidation
{
public interface IUserModel
{
string Username { get; set; }
string Email { get; set; }
string Password { get; set; }
DateTime? DateOfBirth { get; set; }
string Description { get; set; }
}

public interface ICloneable<T>
{
T Clone();
}

public class UserModel : IUserModel, ICloneable<UserModel>
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime? DateOfBirth { get; set; }
public string Description { get; set; }

public static UserModel Create()
{
return new UserModel() { Username = "", Email = "", Password = "", DateOfBirth = null, Description = "" };
}

public UserModel Clone()
{
return (UserModel) this.MemberwiseClone();
}
}
}

UserListView code

<UserControl x:Class="SilverlightValidation.Views.UserListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
d:DesignHeight="400"
d:DesignWidth="725"
mc:Ignorable="d">

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="40" />
<RowDefinition Height="300" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="725" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<StackPanel Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button Width="60"
Command="{Binding AddCommand}"
Content="Add"
Style="{StaticResource ButtonStyle}" />
<Button Width="60"
Command="{Binding DeleteCommand}"
Content="Delete"
Style="{StaticResource ButtonStyle}" />
</StackPanel>

<controls:DataGrid Grid.Row="2"
Grid.Column="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Data}"
SelectionMode="Single"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<controls:DataGrid.Columns>
<controls:DataGridTextColumn Width="125"
Binding="{Binding Username,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}"
Header="Username" />
<controls:DataGridTemplateColumn Width="125" Header="Password">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<PasswordBox Password="{Binding Password, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
<controls:DataGridTextColumn Width="150"
Binding="{Binding Email,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}"
Header="Email" />

<controls:DataGridTemplateColumn Width="150" Header="Date of Birth">
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<sdk:DatePicker KeyDown="DatePicker_KeyDown" SelectedDate="{Binding DateOfBirth, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
<controls:DataGridTextColumn Width="150"
Binding="{Binding Description,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}"
Header="Description" />
</controls:DataGrid.Columns>
</controls:DataGrid>
</Grid>
</UserControl>


UserView code

<c:ChildWindow x:Class="SilverlightValidation.Views.UserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
Title="Add User"
Width="500"
Height="400">

<Grid x:Name="LayoutRoot" Background="White">

<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="50" />
<RowDefinition Height="120" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="300" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>

<TextBlock Grid.Row="1"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="Username:" />

<TextBox x:Name="tbUsername"
Grid.Row="1"
Grid.Column="2"
Style="{StaticResource TextBoxStyle}"
Text="{Binding Username,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}" />

<sdk:DescriptionViewer Grid.Row="1"
Grid.Column="3"
Width="20"
Description="Required"
Target="{Binding ElementName=tbUsername}" />

<TextBlock Grid.Row="2"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="Password:" />

<PasswordBox x:Name="tbPassword"
Grid.Row="2"
Grid.Column="2"
Password="{Binding Password,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}"
Style="{StaticResource PasswordBoxStyle}" />

<sdk:DescriptionViewer Grid.Row="2"
Grid.Column="3"
Width="20"
Description="Required"
Target="{Binding ElementName=tbPassword}" />

<TextBlock Grid.Row="3"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="Email:" />

<TextBox x:Name="tbEmail"
Grid.Row="3"
Grid.Column="2"
Style="{StaticResource TextBoxStyle}"
Text="{Binding Email,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}" />

<sdk:DescriptionViewer Grid.Row="3"
Grid.Column="3"
Width="20"
Description="Required"
Target="{Binding ElementName=tbEmail}" />

<TextBlock Grid.Row="4"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="Date of Birth:" />

<sdk:DatePicker x:Name="dpDateOfBirth"
Grid.Row="4"
Grid.Column="2"
KeyDown="DatePicker_KeyDown"
SelectedDate="{Binding DateOfBirth,
Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True}"
Style="{StaticResource DatePickerStyle}" />
<sdk:DescriptionViewer Grid.Row="4"
Grid.Column="3"
Width="20"
Description="Required"
Target="{Binding ElementName=dpDateOfBirth}" />

<TextBlock x:Name="tbDescription"
Grid.Row="5"
Grid.Column="1"
Style="{StaticResource LabelStyle}"
Text="Description:" />

<TextBox Grid.Row="5"
Grid.Column="2"
Style="{StaticResource TextBoxStyle}"
Text="{Binding Description}" />
<StackPanel Grid.Row="6"
Grid.Column="2"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button Command="{Binding OkCommand}"
Content="OK"
Style="{StaticResource ButtonStyle}" />
<Button Command="{Binding CancelCommand}"
Content="Cancel"
Style="{StaticResource ButtonStyle}" />
</StackPanel>

<sdk:ValidationSummary Grid.Row="7"
Grid.Column="1"
Grid.ColumnSpan="2"
Style="{StaticResource ValidationSummaryStyle}" />

</Grid>
</c:ChildWindow>

Download the source


http://stevenhollidge.com/blog-source-code/SilverlightValidation.zip

Saturday, 15 October 2011

jQuery Validation

Here is a great jQuery library for flexible client side validation on user data entry. 

Each bubble appears after the user has left the control and on form submission and can check:

  • Required fields, data type, regular expressions, dates, min/max, call out to JavaScript functions etc

image

You can check out the full range of validation online here:  http://stevenhollidge.com/html5demos/validation/

Thursday, 13 October 2011

HTML5 IndexedDB

With the present inconsistent state of HTML5 across browsers, this example is best viewed in using Google’s Chrome browser.

I’ve been playing around with the IndexedDB object storage in HTML5.

Here we have a simple form to enter data into our IndexedDB. 

As an aside, the form also uses the HTML5 “required” attributes, with CSS to display a red star when an input field is invalid and green thumbs up when valid.

ScreenShot023ScreenShot024

Here’s a link to the online example: http://stevenhollidge.com/html5demos/indexeddb

The HTML and CSS are pretty basic, all the magic happens within the JavaScript.

HTML code

JavaScript Code