Sunday, May 28, 2017

wpf, prism, inotifydataerrorinfo and fluent validation

Time for a technical post. Recently my team began work on a new product and the architecture that was selected was WPF, Prism with Unity. Yea I know ..... but we went with WPF because of the intensiveness of the graphics we need to use. The design patterns we choose was Prism with Unity and MVVM. The following samples were taken from efforts to collect data needed to create a connection string for a Maria DB.



As we began looking at a validation implementation, we looked at Fluent Validation. After tinkering around, it seemed very well like by the community and seemed like a great way to manage validation rules.

We started constructing our base class. We created our own ValidatableBindableBase class which which inherits from Prism's BindableBase and INotifyDataErrorInfo. With the INotifyDataErrorInfo, we're implementing the interface with a dictionary, adding and removing items to the dictionary when the DataErrorsChangedEventArgs fires.
public abstract class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
//Set ValidatesOnNotifyDataErrors=True binding on the control.
//Binding will call the GetErrors method of the INotifyDataErrorInfo when the property is set in the viewModel through view.
//Subscribes to the ErrorsChanged event in the interface.
//If the ErrorsChanged event is raised, it will re query the GetErrors method for the property for which the event is raised.
/// <summary>
/// Adds error to the dictionary. Accounts for multiple error messages. Raises the changed property event.
/// </summary>
/// <param name="propertyName"></param>
/// <param name="errorMessage"></param>
public void SetError(string propertyName, string errorMessage)
{
if (!_errors.ContainsKey(propertyName))
_errors.Add(propertyName, new List<string> { errorMessage });
RaiseErrorsChanged(propertyName);
}
/// <summary>
/// Clears the error for a property. Raised the changed property event.
/// </summary>
/// <param name="propertyName"></param>
protected void ClearError(string propertyName)
{
if (_errors.ContainsKey(propertyName))
_errors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
/// <summary>
/// Clears all errors set in the dictionary
/// </summary>
protected void ClearAllErrors()
{
var errors = _errors.Select(error => error.Key).ToList();
foreach (var propertyName in errors)
ClearError(propertyName);
}
/// <summary>
/// Raises the changed property event
/// </summary>
/// <param name="propertyName"></param>
public void RaiseErrorsChanged(string propertyName)
{
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
/// <summary>
/// Occurs when the validation errors have changed for a property or the entire model.
/// </summary>
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { return; };
/// <summary>
/// Gets the validation errors for a property of the entire model.
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
public IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!_errors.ContainsKey(propertyName)) return null;
return _errors[propertyName];
}
/// <summary>
/// Gets a value that indicates the model has errors.
/// </summary>
public bool HasErrors
{
get { return _errors.Any(x => x.Value != null && x.Value.Count > 0); }
}
}

So this is the model in which our classes will be built upon. Nothing special, just defining our properties in our model. Notice the model also inherits from our base class - ValidatableBindableBase.

public class MariaDBModel : ValidatableBindableBase
{
/// <summary>
/// Base model for data points
/// </summary>
private string _dsName;
public string DSName
{
get { return _dsName;}
set { SetProperty(ref _dsName, value); }
}
private string _description;
public string Description
{
get { return _description; }
set { SetProperty(ref _description, value);}
}
private string _dbType;
public string DBType
{
get { return _dbType; }
set { SetProperty(ref _dbType, value); }
}
private string _ipAddress;
public string IPAddress
{
get { return _ipAddress; }
set { SetProperty(ref _ipAddress, value); }
}
private string _username;
public string Username
{
get { return _username; }
set { SetProperty(ref _username, value); }
}
private string _password;
public string Password
{
get { return _password; }
set { SetProperty(ref _password, value); }
}
}
}
view raw model.cs hosted with ❤ by GitHub
Using Fluent Validation, we created our validation rules based on our model. Fluent Validation makes it incredibly easy to construct rulesets using lambda expressions. Just pass in the entity type to the AbstractValidator base class.

public class MariaDBValidator : AbstractValidator<MariaDBModel>
{
public MariaDBValidator()
{
// A Dataset's name should not be null or empty.
RuleFor(dataSet => dataSet.DSName)
.NotNull()
.NotEmpty()
.WithMessage("Invalid Name");
// A Dataset's description should not be null or empty.
RuleFor(dataSet => dataSet.Description)
.NotNull()
.NotEmpty()
.WithMessage("Invalid Description");
// A Dataset's IP address should match the regex and not be null or empty
// Regex matches valid IPv4 addresses or localhost
RuleFor(dataSet => dataSet.IPAddress)
.NotNull()
.NotEmpty()
.Matches(@"^(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4})$|^localhost$")
.WithMessage("Invalid IP Address");
// A Dataset's password should not be null or empty.
RuleFor(dataSet => dataSet.Password)
.NotNull()
.NotEmpty()
.WithMessage("Invalid Password");
// A Dataset's username should not be null or empty.
RuleFor(dataSet => dataSet.Username)
.NotNull()
.NotEmpty()
.WithMessage("Invalid Username");
}
}
}

Here I'm showing the view (a partial view of our view) which demonstrates the controls. We are using DevExpress controls here. The controls bind to the viewmodel properties and uses the ValidatesOnNotifyDataErrors attribute set to True. Now the control subscribes to the error raised in the changed DataErrorsChangedEventArgs.

<dxlc:LayoutControl Grid.Row="1">
<dxlc:LayoutGroup>
<dxlc:LayoutItem Label="Database:">
<dxe:TextEdit ToolTip="Enter the database name."
Text="{Binding DSName, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"
HorizontalAlignment="Left"
Width="200">
</dxe:TextEdit>
</dxlc:LayoutItem>
<dxlc:LayoutItem Label="Description:">
<dxe:TextEdit x:Name="Description" ToolTip="Enter the description"
Text="{Binding Description, Mode=TwoWay,ValidatesOnNotifyDataErrors=True}"
TextWrapping="Wrap"
VerticalContentAlignment="Top"
Height="100"/>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
<Separator/>
<dxlc:LayoutGroup>
<dxlc:LayoutItem Label="IP Address:">
<dxe:TextEdit x:Name="IPAddress" ToolTip="Enter the IP address of the database server."
Text="{Binding IPAddress, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"
HorizontalAlignment="Left"
Width="200"/>
</dxlc:LayoutItem>
<dxlc:LayoutItem Label="Username:">
<dxe:TextEdit x:Name="Username" ToolTip="Enter the database username."
Text="{Binding Username, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}"
HorizontalAlignment="Left"
Width="200"/>
</dxlc:LayoutItem>
<dxlc:LayoutItem Label="Password:">
<dxe:PasswordBoxEdit x:Name="Password" ToolTip="Enter the password for the database."
Text="{Binding Password, Mode=TwoWay,ValidatesOnNotifyDataErrors=True}"
HorizontalAlignment="Left"
Width="200"/>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
</dxlc:LayoutControl>
view raw view.xaml hosted with ❤ by GitHub
Lastly, the viewmodel. During the construction of the viewmodel, the Save delegate is registered. When the user clicks the save button, the validation is fired. What a joy it was to see the validation fields light up.

public class MariaDBDialogViewModel : ValidatableBindableBase, IRegionManagerAware
{
public MariaDBDialogViewModel()
{
SaveCommand = new DelegateCommand(OnSaveExecute, CanExecuteSave);
}
#region Methods
//TODO:
private bool CanExecuteSave()
{
return true;
}
/// <summary>
/// Save Data for Maria DB
/// </summary>
private void OnSaveExecute()
{
try
{
ClearAllErrors();
_mariaDBModel = new MariaDBModel
{
DBType = "MariaDB",
DSName = DSName,
Description = Description,
IPAddress = IPAddress,
Username = Username,
Password = Password
};
var validator = new MariaDBValidator();
ValidationResult result = validator.Validate(_mariaDBModel);
foreach (var error in result.Errors)
{
SetError(error.PropertyName, error.ErrorMessage);
}
if (!result.IsValid)
return;
_dataController.AddNewDataSet(_mariaDBModel.DSName, _mariaDBModel);
}
finally
{
Logging.For<MariaDBDialog>().Trace("DataSet was saved successfully.");
}
}
#endregion
#region Properties
private MariaDBModel _mariaDBModel;
public DelegateCommand SaveCommand { get; private set; }
IDataController _dataController = new DataController();
private string _dsName;
public string DSName
{
get { return _dsName; }
set { SetProperty(ref _dsName, value); }
}
private string _description;
public string Description
{
get { return _description; }
set { SetProperty(ref _description, value); }
}
private string _dbType;
public string DBType
{
get { return _dbType; }
set { SetProperty(ref _dbType, value); }
}
private string _ipAddress;
public string IPAddress
{
get { return _ipAddress; }
set { SetProperty(ref _ipAddress, value); }
}
private string _username;
public string Username
{
get { return _username; }
set { SetProperty(ref _username, value); }
}
// Unsafe storage
private string _password;
public string Password
{
get { return _password; }
set { SetProperty(ref _password, value); }
}
#endregion
}
}
view raw viewmodel.cs hosted with ❤ by GitHub

It took a little while to get it constructed just right, but I really like this implementation.

3 comments:

  1. Kindly email me a sample of the validation part. I have struglling with validation for a long time with prism.

    Thank

    ReplyDelete
  2. Hey, I still don't get it the way you have implemented Validation. I'm a die hard of WPF and MVVM. Looks like you are repeating yourself in the properties region of the viewModel. I think you should seperate them into two :
    1. The entity
    2. The Model/Domain/DTO
    I know the term Model/Domain/DTO is confusing most of the time. But i prefer the term Domain, and your viewmodel should interract with the domain.

    Thats is my view.

    ReplyDelete
  3. Hello, I'm new in WPF and MVVM, I'm trying to use this code but I would like to show red border when I have an error in a property in TextBox. I don't know how to do it.

    ReplyDelete