Added basic validation project

This commit is contained in:
Anastasios Zampelis 2020-12-30 16:23:14 +02:00
parent 6053dedaf8
commit 6f429cf5e9
17 changed files with 591 additions and 1 deletions

View File

@ -17,6 +17,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BasicNavigation", "BasicNav
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StyletBasicNavigation", "..\StyletBasicNavigation\StyletBasicNavigation.csproj", "{F3F7DC59-238B-42D0-BC91-BDACECB4C2C0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StyletBasicValidation", "StyletBasicValidation", "{D09B63C8-7726-4864-99BC-9B4EC78574E0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StyletFirstValidation", "..\StyletFirstValidation\StyletFirstValidation.csproj", "{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -43,6 +47,10 @@ Global
{F3F7DC59-238B-42D0-BC91-BDACECB4C2C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3F7DC59-238B-42D0-BC91-BDACECB4C2C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3F7DC59-238B-42D0-BC91-BDACECB4C2C0}.Release|Any CPU.Build.0 = Release|Any CPU
{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -53,6 +61,7 @@ Global
{D778EC71-2747-4605-9DEE-FD2E399CB772} = {B0182D30-4833-48A2-9236-7390C9DE4323}
{F33DF940-E1FF-477B-81EC-28719EEF6FAE} = {B0182D30-4833-48A2-9236-7390C9DE4323}
{F3F7DC59-238B-42D0-BC91-BDACECB4C2C0} = {093E93E7-1E20-48DD-90EC-186C134ED377}
{489F6CD9-E492-4AC8-ACE7-B9B9513C1ED1} = {D09B63C8-7726-4864-99BC-9B4EC78574E0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {81976B23-619E-40C9-AD7F-F6C825119E38}

View File

@ -1,6 +1,12 @@
# StyletWpfExamples
- MyFirstStyletProject
Basic MVVM Navigation example heavily inspired by by https://github.com/MeshackMusundi/StaffStuff-Stylet.
<br>
- StyletBasicNavigation
Basic MVVM Navigation including Modal.
![basic_navigation](./Showcase/basic_navigation.png)
<br>
- StyletBasicNavigation
Basic MVVM Validation.
![basic_validation](./Showcase/basic_validation.png)
<br>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -37,7 +37,7 @@
<ListBox ItemsSource="{Binding ViewsCollection}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=.}" Background="{Binding Converter={StaticResource StringToBrushConverter}}" Foreground="White" />
<TextBlock Text="{Binding Path=.}" Background="{Binding Converter={StaticResource StringToBrushConverter}}" Foreground="White" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

View File

@ -0,0 +1,13 @@
<Application x:Class="StyletFirstValidation.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:StyletFirstValidation">
<Application.Resources>
<s:ApplicationLoader>
<s:ApplicationLoader.Bootstrapper>
<local:Bootstrapper/>
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
</Application.Resources>
</Application>

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace StyletFirstValidation
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@ -0,0 +1,22 @@
using FluentValidation;
using Stylet;
using StyletFirstValidation.ViewModels;
using StyletIoC;
namespace StyletFirstValidation
{
public class Bootstrapper : Bootstrapper<ShellViewModel>
{
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
builder.Bind(typeof(IModelValidator<>)).To(typeof(FluentModelValidator<>));
//builder.Bind<IValidator<UserViewModel>>().To<UserViewModelValidator>();
builder.Bind(typeof(IValidator<>)).ToAllImplementations();
}
protected override void Configure()
{
// Perform any other configuration before the application starts
}
}
}

View File

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace StyletFirstValidation.Converters
{
[TypeConverter(typeof(StringableConverter))]
public struct Stringable<T> : IEquatable<Stringable<T>>
{
private readonly string _stringValue;
/// <summary>
/// String representation of the value
/// </summary>
public string StringValue
{
get { return this._stringValue; }
}
private readonly T _value;
/// <summary>
/// Actual value, or default(T) if IsValid is false
/// </summary>
public T Value
{
get { return this._value; }
}
private readonly bool _isValid;
/// <summary>
/// True if Value ias a proper value (i.e. we were constructed from a T, or we were constructed from a string which could be converted to a T)
/// </summary>
public bool IsValid
{
get { return this._isValid; }
}
/// <summary>
/// Create a new instance, representing the given value
/// </summary>
/// <param name="value">Value to represent</param>
public Stringable(T value) : this(value, value.ToString(), true) { }
private Stringable(T value, string stringValue, bool isValid)
{
this._value = value;
this._stringValue = stringValue;
this._isValid = isValid;
}
/// <summary>
/// Create a new instance from the given string. If the string can be converted to a T, then IsValue is true and Value contains the converted value.
/// If not, IsValid is false and Value is default(T)
/// </summary>
/// <param name="stringValue"></param>
/// <returns></returns>
public static Stringable<T> FromString(string stringValue)
{
T dest = default(T);
bool isValid = false;
// The TypeConverter for String can't convert it into anything else, so don't bother getting that
var fromConverter = TypeDescriptor.GetConverter(typeof(T));
if (fromConverter.CanConvertFrom(typeof(string)) && fromConverter.IsValid(stringValue))
{
dest = (T)fromConverter.ConvertFrom(stringValue);
isValid = true;
}
return new Stringable<T>(dest, stringValue, isValid);
}
public static implicit operator T(Stringable<T> stringable)
{
return stringable.Value;
}
public static implicit operator Stringable<T>(T value)
{
return new Stringable<T>(value);
}
public override string ToString()
{
return this.StringValue;
}
public override bool Equals(object obj)
{
if (!(obj is Stringable<T>))
return false;
return base.Equals((Stringable<T>)obj);
}
public bool Equals(Stringable<T> other)
{
return EqualityComparer<T>.Default.Equals(this.Value, other.Value) && this.StringValue == other.StringValue;
}
public static bool operator ==(Stringable<T> o1, Stringable<T> o2)
{
return o1.Equals(o2);
}
public static bool operator !=(Stringable<T> o1, Stringable<T> o2)
{
return !(o1 == o2);
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 27 + this.Value.GetHashCode();
hash = hash * 27 + this.StringValue.GetHashCode();
return hash;
}
}
}
/// <summary>
/// TypeConverter for Stringable{T}
/// </summary>
/// <remarks>
/// This is used by WPF. This means that if a Stringable{T} property is bound to a string control (e.g. TextBox), then this TypeConverter
/// is used to convert from that string back to a Stringable{T}
/// </remarks>
public class StringableConverter : TypeConverter
{
private readonly Type valueType;
private readonly Func<string, object> generator;
public StringableConverter(Type type)
{
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Stringable<>) || type.GetGenericArguments().Length != 1)
throw new ArgumentException("Incompatible type", "type");
this.valueType = type;
// Generate a Func<string, object> which gives us a Stringable<T>, given a string
// WPF instantiates us once, then uses us lots, so the overhead of doing this here is worth it
var fromMethod = type.GetMethod("FromString", BindingFlags.Static | BindingFlags.Public);
var param = Expression.Parameter(typeof(string));
this.generator = Expression.Lambda<Func<string, object>>(Expression.TypeAs(Expression.Call(fromMethod, param), typeof(object)), param).Compile();
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
return this.generator(value as string);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || destinationType == this.valueType || base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (value == null)
return String.Empty;
// Common case = just call the overloaded ToString - no need for reflection
if (destinationType == typeof(string))
return value.ToString();
var valueType = value.GetType();
if (destinationType.IsAssignableFrom(this.valueType) && typeof(Stringable<>).IsAssignableFrom(valueType))
{
var valueProperty = valueType.GetProperty("Value", BindingFlags.Public | BindingFlags.Instance);
return valueProperty.GetValue(value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}

View File

@ -0,0 +1,44 @@
using FluentValidation;
using Stylet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace StyletFirstValidation
{
public class FluentModelValidator<T> : IModelValidator<T>
{
private readonly IValidator<T> validator;
private T subject;
public FluentModelValidator(IValidator<T> validator)
{
this.validator = validator;
}
public void Initialize(object subject)
{
this.subject = (T)subject;
}
public async Task<IEnumerable<string>> ValidatePropertyAsync(string propertyName)
{
// If someone's calling us synchronously, and ValidationAsync does not complete synchronously,
// we'll deadlock unless we continue on another thread.
return (await this.validator.ValidateAsync(this.subject, o=>o.IncludeProperties(propertyName), CancellationToken.None).ConfigureAwait(false))
.Errors.Select(x => x.ErrorMessage);
}
public async Task<Dictionary<string, IEnumerable<string>>> ValidateAllPropertiesAsync()
{
// If someone's calling us synchronously, and ValidationAsync does not complete synchronously,
// we'll deadlock unless we continue on another thread.
return (await this.validator.ValidateAsync(this.subject).ConfigureAwait(false))
.Errors.GroupBy(x => x.PropertyName)
.ToDictionary(x => x.Key, x => x.Select(failure => failure.ErrorMessage));
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>StyletFirstValidation</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.3.0" />
<PackageReference Include="Stylet" Version="1.3.5.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
using FluentValidation;
using StyletFirstValidation.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StyletFirstValidation.Validators
{
public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
public UserViewModelValidator()
{
ValidatorOptions.Global.LanguageManager.Enabled = false;
RuleFor(x => x.UserName).NotEmpty().Length(1, 20);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Password).NotEmpty().Matches("[0-9]").WithMessage("{PropertyName} must contain a number");
RuleFor(x => x.PasswordConfirmation).Equal(s => s.Password).WithMessage("{PropertyName} should match Password");
RuleFor(x => x.Age).Must(x => x.IsValid).WithMessage("{PropertyName} must be a valid number");
When(x => x.Age.IsValid, () =>
{
RuleFor(x => x.Age.Value).GreaterThan(0).WithName("Age");
});
}
}
}

View File

@ -0,0 +1,14 @@
using Stylet;
namespace StyletFirstValidation.ViewModels
{
public class ShellViewModel : Conductor<IScreen>
{
public ShellViewModel(UserViewModel userViewModel)
{
this.DisplayName = "Stylet.Samples.ModelValidation";
this.ActiveItem = userViewModel;
}
}
}

View File

@ -0,0 +1,120 @@
using Stylet;
using StyletFirstValidation.Converters;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StyletFirstValidation.ViewModels
{
public class UserViewModel : Screen
{
private IWindowManager windowManager;
public ObservableCollection<string> ErrorsCollection { get; set; } = new ObservableCollection<string>();
#region Properties
private string _userName;
public string UserName
{
get => _userName;
set
{
SetAndNotify(ref _userName, value);
ValidateProperty();
this.NotifyOfPropertyChange(() => this.CanSubmit);
}
}
private string _email;
public string Email
{
get => _email;
set
{
SetAndNotify(ref _email, value);
ValidateProperty();
this.NotifyOfPropertyChange(() => this.CanSubmit);
}
}
private string _password;
public string Password
{
get => _password;
set
{
SetAndNotify(ref _password, value);
ValidateProperty();
this.NotifyOfPropertyChange(() => this.CanSubmit);
}
}
private string _passwordConfirmation;
public string PasswordConfirmation
{
get => _passwordConfirmation;
set
{
SetAndNotify(ref _passwordConfirmation, value);
ValidateProperty();
this.NotifyOfPropertyChange(() => this.CanSubmit);
}
}
private Stringable<int> _age;
public Stringable<int> Age
{
get => _age;
set
{
SetAndNotify(ref _age, value);
ValidateProperty();
this.NotifyOfPropertyChange(() => this.CanSubmit);
}
}
#endregion
public UserViewModel(IWindowManager windowManager, IModelValidator<UserViewModel> validator) : base(validator)
{
this.windowManager = windowManager;
// Force initial validation
this.Validate();
// Whenever password changes, we need to re-validate PasswordConfirmation
this.Bind(x => x.Password, (o, e) => this.ValidateProperty(() => this.PasswordConfirmation));
}
public void Submit()
{
if (this.Validate())
this.windowManager.ShowMessageBox("Successfully submitted", "success");
}
public void SubmitAlways()
{
ErrorsCollection.Clear();
if (this.Validate())
{
this.windowManager.ShowMessageBox("Successfully submitted", "success");
}
else
{
var errors = this.Validator.ValidateAllPropertiesAsync().Result;
foreach (var error in errors.Values.ToList().SelectMany(d=>d))
{
ErrorsCollection.Add(error.ToString());
}
}
}
public bool CanSubmit
{
get { return !this.HasErrors; }
}
}
}

View File

@ -0,0 +1,14 @@
<Window x:Class="StyletFirstValidation.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:StyletFirstValidation.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:ShellViewModel}"
Title="Stylet Project" Height="450" Width="800">
<Grid>
<ContentControl s:View.Model="{Binding ActiveItem}"/>
</Grid>
</Window>

View File

@ -0,0 +1,15 @@
using System.Windows;
namespace StyletFirstValidation.Views
{
/// <summary>
/// Interaction logic for ShellView.xaml
/// </summary>
public partial class ShellView : Window
{
public ShellView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,62 @@
<UserControl x:Class="StyletFirstValidation.Views.UserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:StyletFirstValidation.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="CommonStyle" TargetType="{x:Type Control}">
<Setter Property="Margin" Value="10,5,0,5"/>
<Setter Property="FontSize" Value="12" />
</Style>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource CommonStyle}"/>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource CommonStyle}"/>
<Style TargetType="{x:Type TextBlock}" BasedOn="{StaticResource CommonStyle}"/>
</UserControl.Resources>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="0" Grid.Column="2" Grid.RowSpan="5">
<ListBox ItemsSource="{Binding ErrorsCollection}" Background="Red">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}" Foreground="White"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Label Grid.Row="0" Grid.Column="0" Target="{Binding ElementName=txtUserName}">UserName:</Label>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="txtUserName" Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="1" Grid.Column="0" Target="{Binding ElementName=txtEmail}">Age:</Label>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="txtAge" Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="2" Grid.Column="0" Target="{Binding ElementName=txtEmail}">Email:</Label>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtEmail" Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="3" Grid.Column="0" Target="{Binding ElementName=txtPassword}">Password:</Label>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="txtPassword" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Row="4" Grid.Column="0" Target="{Binding ElementName=txtPasswordConfirmation}">Confirm Password:</Label>
<TextBox Grid.Row="4" Grid.Column="1" x:Name="txtPasswordConfirmation" Text="{Binding PasswordConfirmation, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Row="5" Grid.Column="0" Command="{s:Action SubmitAlways}" HorizontalAlignment="Left">Submit Always</Button>
<Button Grid.Row="5" Grid.Column="1" Command="{s:Action Submit}" HorizontalAlignment="Left">Submit</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace StyletFirstValidation.Views
{
/// <summary>
/// Interaction logic for UserView.xaml
/// </summary>
public partial class UserView : UserControl
{
public UserView()
{
InitializeComponent();
}
}
}