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.
Related
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;
}
}
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;
}
}
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;
}
i think i have read already all related articles but non of them help..
im trying to enable/disable a save button of datagrid by the error state- but with no success.
this is my code:
contractor:
AddHandler(Validation.ErrorEvent, new RoutedEventHandler(OnErrorEvent));
XAML:
<Page
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:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:local="clr-namespace:Metsuka_APP" x:Class="Metsuka_APP.MichlolimManagment"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="500"
Title="MichlolimManagment"
x:Name="Michlolim_Managment" Validation.Error="Michlolim_Managment_Error">
<Page.Resources>
<DataGrid x:Name="AGAFIMDataGrid" VerticalAlignment="Center" RowEditEnding="rowEditEnding" Margin="10" FlowDirection="RightToLeft" Height="340"
AutoGenerateColumns="False" EnableRowVirtualization="True"
ItemsSource="{Binding Source={StaticResource aGAFIMViewSource}}" Grid.Row="1"
RowDetailsVisibilityMode="VisibleWhenSelected"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
HorizontalGridLinesBrush="Silver"
VerticalGridLinesBrush="Silver">
<DataGrid.Resources>
<Style x:Key="errorStyle" TargetType="{x:Type TextBox}">
<Setter Property="Padding" Value="-2"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn x:Name="agaf_nameColumn" Header="name" Width="*">
<DataGridTextColumn.Binding>
<Binding Path="agaf_name" NotifyOnValidationError="True" >
<Binding.ValidationRules>
<local:MichlolimValidationRule ValidationStep="UpdatedValue"/>
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.RowValidationErrorTemplate>
<ControlTemplate>
<Grid Margin="0,-2,0,-2"
ToolTip="{Binding RelativeSource={RelativeSource
FindAncestor, AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}">
<Ellipse StrokeThickness="0" Fill="Red"
Width="{TemplateBinding FontSize}"
Height="{TemplateBinding FontSize}" />
<TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" />
</Grid>
</ControlTemplate>
</DataGrid.RowValidationErrorTemplate>
</DataGrid>
code behind:
private int errorCount;
private void OnErrorEvent(object sender, RoutedEventArgs e)
{
var validationEventArgs = e as ValidationErrorEventArgs;
if (validationEventArgs == null)
throw new Exception("Unexpected event args");
switch (validationEventArgs.Action)
{
case ValidationErrorEventAction.Added:
{
errorCount++; break;
}
case ValidationErrorEventAction.Removed:
{
errorCount--; break;
}
default:
{
throw new Exception("Unknown action");
}
}
btnSavePop.IsEnabled = errorCount == 0;
}
but the "OnErrorEvent" never fires- any idea why?
You need to set NotifyOnValidationError="True" on your bindings - otherwise, the event won't be raised. I would recommend using the IDataErrorInfo or INotifyDataErrorInfo interfaces instead for an MVVM error handling approach.
Try creating a class like the following:
public class AgafDescriptor : INotifyPropertyChanged, IDataErrorInfo
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
RaisePropertyChanged(x => x.Name);
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged<T>(Expression<Func<AgafDescriptor, T>> propertyExpression)
{
PropertyChangedEventHandler localPropertyChanged = this.PropertyChanged as PropertyChangedEventHandler;
if ((localPropertyChanged != null) && (propertyExpression != null))
{
MemberExpression body = propertyExpression.Body as MemberExpression;
if (body != null)
{
localPropertyChanged(this, new PropertyChangedEventArgs(body.Member.Name));
}
}
}
#endregion
#region IDataErrorInfo Members
// Does nothing in WPF.
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
string returnVal = null;
if (string.Equals("Name", columnName, StringComparison.Ordinal))
{
if (string.IsNullOrWhiteSpace(Name))
{
returnVal = "A name must be supplied.";
}
}
return returnVal;
}
}
#endregion
}
This will provide an error whenever there is a change to the Name property. Please note that if you wish to trigger new validation checks without modifying the property you just need to call:
RaisePropertyChanged(x => x.Name);
You will then need to change your binding to something like:
<DataGridTextColumn x:Name="agaf_nameColumn" Header="name" Width="*" Binding="{Binding Name, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, NotifyOnValidationError=True}"/>
Note you will have to load your data from the DB and create a descriptor for each item you want to display in the DataGrid.
The reason behind why you aren't seeing the event being fired:
You aren't raising property change events (INotifyPropertyChanged or through a DependencyProperty) therefore the UI won't receive updates and the event won't be fired because it hasn't received an update to then perform the validation. By binding direct to your DB then you aren't raising the property change events. You can see that the Name property I suggested in my answer does raise the property changed event
The attribute
Validation.Error="Michlolim_Managment_Error"
in your XAML already sets a handler for the error event on the window level, however, to a method which isn't defined in your code behind fragment. Your AddHandler call looks ok, but, depending on where it is in the constructor, it might get overridden by the XAML event handler definition.
Probably not related, but you might want to change
(Validation.Errors)[0].ErrorContent
to
(Validation.Errors)/ErrorContent
in your ToolTip bindings, since the former causes binding errors if there are no errors. Unfortunately, it is still contained in the documentation samples...
From my sample of your code I think you are missing specifying UpdateSourceTrigger="PropertyChanged" or UpdateSourceTrigger="LostFocus" on the DataGridTextColumn in the DataGrid instead of using the default behavior of the DataGrids binding.
That is assuming my assumptions are correct see bottom.
Your code causes OnErrorEvent to fire if i change:
<DataGridTextColumn.Binding>
<Binding Path="agaf_name" NotifyOnValidationError="True" >
...
To include UpdateSourceTrigger for PropertyChanged or LostFocus like so:
<DataGridTextColumn.Binding>
<Binding Path="agaf_name" NotifyOnValidationError="True" UpdateSourceTrigger="PropertyChanged" >
....
Assumptions - For your ValidationRule to test i made it always return false (see below). And for the test item source i am binding to a string value in 'agaf_name' property.
public class MichlolimValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
return new ValidationResult(false, "bad");
}
}
Here’s a (somewhat) pared-down example of some code I have that’s attempting to make use of a BindingGroup:
XAML:
<Window x:Class="BindingGroupQuandary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BindingGroupQuandary"
Title="Binding Group Test" Width="400" Height="400" MinWidth="400" MinHeight="400">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<GroupBox Header="Items">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Name="dataList">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="First Property" DisplayMemberBinding="{Binding FirstProperty}" />
<GridViewColumn Header="Second Property" DisplayMemberBinding="{Binding SecondProperty}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
<Button Grid.Row="1" Content="Add Item" Click="add_Click" HorizontalAlignment="Left" Margin="0,5,0,5" />
</Grid>
</GroupBox>
<GroupBox Grid.Row="1" Name="editControls" Header="Edit Item">
<GroupBox.BindingGroup>
<BindingGroup Name="editItemBindings"/>
</GroupBox.BindingGroup>
<StackPanel>
<Label Content="First Property (Required)"/>
<TextBox>
<TextBox.Text>
<Binding Path="FirstProperty">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Content="Second Property (Required)"/>
<TextBox>
<TextBox.Text>
<Binding Path="SecondProperty">
<Binding.ValidationRules>
<local:NotEmptyValidationRule ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Content="Commit" Click="commit_Click" HorizontalAlignment="Left" Margin="0,10,0,0"/>
</StackPanel>
</GroupBox>
</Grid>
</Window>
Code-behind:
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
namespace BindingGroupQuandary
{
class SomeData
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
class NotEmptyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string textValue = (value as string);
ValidationResult result;
if (string.IsNullOrWhiteSpace(textValue))
{
result = new ValidationResult(false, "The field must contain a text value and cannot be empty.");
}
else
{
result = new ValidationResult(true, null);
}
return result;
}
}
public partial class MainWindow : Window
{
private ObservableCollection<SomeData> _items = new ObservableCollection<SomeData>();
public MainWindow()
{
InitializeComponent();
dataList.ItemsSource = _items;
}
private void add_Click(object sender, RoutedEventArgs e)
{
editControls.DataContext = (new SomeData() { FirstProperty = "This property has an initial value." });
editControls.BindingGroup.BeginEdit();
}
private void commit_Click(object sender, RoutedEventArgs e)
{
SomeData current = editControls.DataContext as SomeData;
if (current != null)
{
if (editControls.BindingGroup.CommitEdit())
{
_items.Add(current);
editControls.DataContext = null;
}
}
}
}
}
The effect I’m looking for is that:
I click “Add Item” to bind an object to the editing controls in the lower half of the window (which normally aren’t visible until the data context is supplied in the actual application).
I intentionally don’t set ValidatesOnTargetUpdated on either property binding because I don’t necessarily want to show any error adorners yet; the field text already hints that the fields are required.
When I do click Commit, I want the BindingGroup to re-check all of the validation rules to catch any fields that are still empty and show the error adorners at that point.
Unfortunately, using the sample code above, if I click “Add” and then click “Commit” immediately without filling in the “Second Property” field or changing anything, the binding group doesn’t call my validation rules at all; CommitEdit returns “true” and the object is passed through to the underlying collection. Calling ValidateWithoutUpdate also just returns “true.” My tests with the application seem to suggest that CommitEdit and ValidateWithoutUpdate actually only call validation rules where:
The rule is currently false/invalid.
The rule is currently valid but the value has changed.
The methods don’t seem to reevaluate valid rules for which the value hasn’t changed. I suppose this behavior makes sense (and perhaps this is just a general binding behavior?), but it seems to contradict the documentation which states, for CommitEdit:
Runs all the ValidationRule objects and updates the binding sources if all validation rules succeed.
I imagine the validation rules mentioned in that statement could refer only to the top-level rules in the BindingGroup itself and not the validation rules in the individual bindings. At any rate, my actual question is: is it possible to get the BindingGroup to re-check all of the validation rules in this example; if not, could I do that directly by using the BindingExpressions collection? Or, should I just give up and rely on ValidatesOnTargetUpdated (perhaps paired with a less glaring adorner template) to set the initial rule states correctly when the data context is established?