I noticed that it is possible to bind variables of the type DateTime to a textbox in WPF. If I enter a wrong value it will not validate and show a red border.
How can I implement my own class, that I can bind to a textbox without having to bind to a property of the class? The Textbox should show a string and the class will validate the input.
Is this possible?
My current solution is this:
In the Model:
public string DefaultLanguageValue
{
get
{
return _defaultLanguageValue;
}
set
{
if (value != this._defaultLanguageValue)
{
ValidateLanguage(value);
this._defaultLanguageValue = value;
NotifyPropertyChanged();
}
}
}
private void ValidateLanguage(string value)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value, rx))
{
throw new ArgumentException();
}
}
In the XAML:
<TextBox Text="{Binding TreeViewModel.Model.DefaultLanguageValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}" BorderThickness="0" MinWidth="100"/>
It would be nice to have a Class that I can just bind like a String, Int or DateTime for examlpe. Any Ideas?
You could bind to the Tag property of the TextBox itself and validate using a ValidationRule:
public class DateValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (!DateTime.TryParse(value as string, out DateTime _))
return new ValidationResult(false, "Invalid date...");
return ValidationResult.ValidResult;
}
}
XAML:
<TextBox>
<TextBox.Text>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DateValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
This doesn't require you to bind to a view model.
I finally tried the solution suggested by mm8.
The only issue I have now is that if one enters an invalid value into the textbox, it will not update the textbox when I programmatically change the value of the source after clicking a button.
I tried Validation after update, but this allows the user to save invalid values.
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chkDefaultLanguage" IsChecked="{Binding TreeViewModel.TreeModel.DefaultLanguage, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="DefaultLanguage: " />
<TextBox BorderThickness="0" MinWidth="100">
<TextBox.Text>
<Binding Path="TreeViewModel.TreeModel.DefaultLanguageValue" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationrules:LanguageCodeValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
class LanguageCodeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value.ToString(), rx))
{
return new ValidationResult(false, "Invalid Language Codee.");
}
return ValidationResult.ValidResult;
}
}
Related
I would like to bind the text from ComboBox CB1 to the Validation Rule (CommunicationMode) in ComboBox CB2.
My source code looks like this, but I get the error: "Binding" can only be set for a "DependencyProperty" of a "DependencyObject".
Is there a way to solve this?
public string CommunicationMode { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return new ValidationResult(true, null);
}
<ComboBox Name="CB1">
<ComboBox.Text>
<Binding Path="CB1" UpdateSourceTrigger="PropertyChanged"/>
</ComboBox.Text>
</ComboBox>
<ComboBox Name="CB2">
<ComboBox.Text>
<Binding Path="CB2" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<valid:ComboboxValidationRule CommunicationMode="{Binding ElementName=CB1, Path=Name}" ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>
You could create a Wrapper class with a dependency property:
public class ComboboxValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
//your validation logic...
return new ValidationResult(true, null);
}
public Wrapper Wrapper { get; set; }
}
public class Wrapper : DependencyObject
{
public static readonly DependencyProperty CommunicationModeProperty =
DependencyProperty.Register(nameof(CommunicationMode), typeof(string), typeof(Wrapper));
public string CommunicationMode
{
get { return (string)GetValue(CommunicationModeProperty); }
set { SetValue(CommunicationModeProperty, value); }
}
}
XAML:
<ComboBox Name="CB2">
<ComboBox.Text>
<Binding Path="CB2" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<valid:ComboboxValidationRule ValidatesOnTargetUpdated="True">
<valid:ComboboxValidationRule.Wrapper>
<valid:Wrapper CommunicationMode="{Binding Source={x:Reference CB1}, Path=Name}" />
</valid:ComboboxValidationRule.Wrapper>
</valid:ComboboxValidationRule>
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>
Situation :
When you click on new product a popup screen show ups :
As you can see the button "Opslagen" is disabled, that is good because "Productnaam" is mandatory.
Now If I start typing the button "Opslagen" is enabled, so far thats ok.
But when I remove tekst a message in red shows that this field is mandatory, but the button won't disable anymore :
When I type something again the red text dissapears again. But the button behavior is not working as expected.
The XAML :
<TextBox Width="200"
Height="30"
HorizontalAlignment="Left"
VerticalContentAlignment="Center">
<TextBox.Text>
<Binding Path="ProductName" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validators:EmptyValidationRule ValidatesOnTargetUpdated="True" ValidationStep="RawProposedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
The EmptyValidationRule class :
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
{
return new ValidationResult(true, null);
}
if (!string.IsNullOrEmpty(value.ToString()) && !string.IsNullOrWhiteSpace(value.ToString()))
{
return new ValidationResult(true, null);
}
return new ValidationResult(false, "Dit veld is verplicht.");
}
And finally the IsSaveButtonDisabled property in the ViewModel :
public bool IsSaveButtonEnabled
{
get
{
if (!string.IsNullOrEmpty(_productName) && !string.IsNullOrWhiteSpace(_productName))
{
return true;
}
else
{
return false;
}
}
}
I really have no idea. It must be the combination of the ValidationRule and the check if the property ProductName is empty.
Edit, button code :
<Button Content="Opslagen"
IsEnabled="{Binding IsSaveButtonEnabled}"
Background="Bisque"
Width="120"
Height="30"
Command="{Binding SaveCommand}" />
ProductName property :
private string _productName;
public string ProductName
{
get => _productName;
set
{
_productName = value;
RaisePropertyChanged(nameof(ProductName));
RaisePropertyChanged(nameof(IsSaveButtonEnabled));
}
}
The Save command is like this :
SaveCommand = new RelayCommand(SaveProduct);
And SaveProduct is just a method that saves the product. This stuff is working.
Set the ValidationStep property to UpdatedValue to run the ValidationRule after the source property has been set:
<validators:EmptyValidationRule ValidatesOnTargetUpdated="True"
ValidationStep="UpdatedValue" />
Then the PropertyChanged event for the IsSaveButtonEnabled property should be invoked and the Button should be disabled even if the validation rule fails.
In my WPF project i started use ValidationRule with text fields and found a problem. I created simple project to test ValidationRule. Everything works fine, but if my input value is not in valid range, my property store last valid value. Thats why i have a question: how can I check current value, that not valid for ValidationRule? May be, i doing something wrong?
Main Window
</Window ... >
<Window.Resources>
<ControlTemplate x:Key="errorTemplate">
<Border BorderBrush="OrangeRed" BorderThickness="2">
<Grid>
<AdornedElementPlaceholder/>
<TextBlock Text="{Binding [0].ErrorContent}" Foreground="OrangeRed"
VerticalAlignment="Center" HorizontalAlignment="Right"
Margin="0,0,4,0"/>
</Grid>
</Border>
</ControlTemplate>
</Window.Resources>
<StackPanel>
<TextBox Validation.ErrorTemplate ="{StaticResource errorTemplate}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Width="200" Margin="0 20">
<TextBox.Text>
<Binding Path="ForText"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:EmptyFieldRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Content="Check"
Command="{Binding ForCommand}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="40"/>
</StackPanel>
</Window>
View model for Window, that i set in DataContext:
public class MainWindowVM : VM
{
private string _text = "some text";
public string ForText
{
get => _text;
set { _text = value; OnPropertyChanged(nameof(ForText)); }
}
public ICommand ForCommand { get; set; }
public MainWindowVM() { ForCommand = new RelayCommand(() => MessageBox.Show(ForText)); }
}
Validation rule:
public class EmptyFieldRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string information = value as string;
if (string.IsNullOrWhiteSpace(information))
return new ValidationResult(false, "!!!");
return ValidationResult.ValidResult;
}
}
Binding in wpf evaluate all the ValidationRule before updating the value. You have to make few changes in the EmptyFieldRule class to achieve this. This is a kind of trick to resolve your issue but there are other better ways to implement the same.
Change ValidationStep property to ValidationStep.UpdatedValue and use Reflection to get updated value in the ValidationResult method.
public class EmptyFieldRule : ValidationRule
{
public EmptyFieldRule()
{
this.ValidationStep = ValidationStep.UpdatedValue;
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var expression = value as BindingExpression;
var information = expression?.DataItem?.GetType()?.GetProperty(expression.ResolvedSourcePropertyName)?.GetValue(expression.DataItem) as string;
if (string.IsNullOrWhiteSpace(information))
return new ValidationResult(false, "!!!");
return ValidationResult.ValidResult;
}
}
Here's the XAML code representing a TextBox used as input for the IdCard
<TextBox.Text>
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
The validation :
public class AlphaNumValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (string.IsNullOrWhiteSpace((value ?? "").ToString()))
return new ValidationResult(false, Resources.Strings.MessagesResource.RequiredField);
else if (value.ToString().MatchRegex(RegexResource.ALPHANUMERIC))
return new ValidationResult(true, null);
else
return new ValidationResult(false, Resources.Strings.MessagesResource.InvalidFormat);
}
}
The ViewModel
public override bool IsValid
{
get { return !string.IsNullOrWhiteSpace(IdCardNumber); }
}
private string idCardNumber;
public string IdCardNumber
{
get { return idCardNumber; }
set { Set(() => IdCardNumber, ref idCardNumber, value);
RaisePropertyChanged("IsValid");
}
}
What I want to have is to update IsValid everytime the IdCard input is updated , I tried different ValidationStep but none do as I wish.
At first when loading the input for the first time IsValid is false , when typing a correct value it becomes true after deleting input and adding wrong non-supported values IsValid stays the same since it keeps the last correct value.
Any way to solve this ?
There is an attached event Validation.Error that is fired when binding error occurs.
So basically you could attach to this event and set value of Validation.HasErrors property to your viewmodel's IsValid property.
I see a conflict however. You defined your validation logic in the View, but you want to access it in your ViewModel, that's why you are having troubles.
I recommend you to move entire validation logic to your viewmodel by implementing INotifyDataErrorInfo. then you will have all validation rules and validation errors at your disposal in viewmodel.
You can try to change the UpdateSourceTrigger property with LostFocus:
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="LostFocus">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
Edit :
To bind the validation result you can use HasError property :
<TextBox Name="TextBox">
<TextBox.Text>
<Binding Mode="TwoWay"
Path="Text"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:AlphaNumValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="{Binding (Validation.HasError), ElementName=TextBox}"/>
I have a simple example application, 2 text boxes a validation rule and a button.
I want my button to trigger validation and if it's not valid, not to continue with its execution.
<TextBox>
<TextBox.Text>
<Binding Path="FirstName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter first name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox>
<TextBox.Text>
<Binding Path="LastName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter last name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Command="{Binding OkCommand}" Content="Ok" />
The validation rule:
public class MyValidationRule : ValidationRule
{
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string valueToCheck = value as string;
if (string.IsNullOrWhiteSpace(valueToCheck))
{
return new ValidationResult(false, ErrorMessage);
}
else
{
return new ValidationResult(true, null);
}
}
}
My OkCommand is empty method:
OkCommand= new RelayCommand(OkRequested);
private void OkRequested()
{
// Do stuff
}
However, no matter if the text boxes are empty OkCommand gets executed properly. What am I doing wrong here? I want text boxes to be styled if they're not valid (I've excluded styles from example), but that does not happen.
You should add into OkCommand something like this. And same for other textBoxes.
if ( Validation.GetHasError( textBox1 ) )
{
MessageBox.Show( Validation.GetErrors( textBox1 )[0].ErrorContent.ToString() );
return;
}