Pages

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.

Sunday, May 14, 2017

rain barrel 2.0

This year I was ready to begin my rain barrels 2.0. My goal is to have full pressure spray from my rain barrels, powered by solar. Well .... let's start by cleaning them up. 


I pulled them from under the deck (where they are stored for the winter) and cleaned them out really good.



I got them for $45 from my local water treatment plan several years ago. They work great, except the pressure is not enough to get water through them. I ended up using water pails to move water to my garden. Not much fun or efficient.



I didn't use them last year and of course, no rain most of the summer. Not jinxing this summer, bring them out.


I went off to the nearest big box store and purchased about 3 of these.


Couple of coats and a few days later .... all done.


Next, I purchased a highly recommended water pump from Amazon. It is the cutest, little portable guy. It is electric .... so this year, I'll plug it in. Next year though ...... solar power!



We plugged in the hose and turned on the pump. And TA-DA, pull pressure!


Nice solid pressure directly from the rain barrels.


Now ... I have the coolest rain barrels that provide full pressure to a garden hose. Look out garden, full organic rain water - all for you.