I'm trying to achieve validation on button click for a textbox using binding. Basically when I click Submit my textbox is not turning red and giving me the "Required" error, it is when I add text to it.
I'm new at validation and been looking at this for almost a week on and off in frustration. I think my answer may have something to-do with propertychangedevent? but I'm not sure and resorting to asking the professionals.
All and any help with this will be must appreciated.
Here is my Model class:
public class sForms : INotifyPropertyChanged, IDataErrorInfo
{
private string name;
public string NAME { get { return name; } set { if (name != value) name = value.Trim(); OnPropertyChanged("NAME"); } }
public string this[string columnName]
{
get
{
return ValidationError(columnName);
}
}
public string Error { get { return null; } }
private string ValidationError(string columnName)
{
string error = null;
switch (columnName)
{
case "NAME":
error = IsNameValid();
break;
}
return
error;
}
static readonly string[] ValidatedProperties = { "NAME" };
public bool IsValid
{
get
{
foreach (string property in ValidatedProperties)
{
if (ValidationError(property) != null)
{
return
false;
}
}
return
true;
}
}
public string IsNameValid()
{
if (string.IsNullOrWhiteSpace(NAME) || string.IsNullOrEmpty(NAME))
return "Required";
else
return
null;
}
#region Property Changed
private void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Here is my XAML for my button + Text Box;
<TextBox Controls:TextBoxHelper.UseFloatingWatermark="True"
Controls:TextBoxHelper.Watermark="Name *"
Grid.Column="1" Grid.Row="1"
Margin="0 0 2 0"
Text="{Binding Path=NAME, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
>
<Button Content="Submit"
Style="{DynamicResource SquareButtonStyle}"
VerticalAlignment="Bottom" HorizontalAlignment="Right"
Margin="0 0 10 0"
Click="Submit_Click"
/>
Here is my code behind;
public v_subsForm()
{
InitializeComponent();
this.DataContext = subs;
}
sForms subs = new sForms();
#region PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
private void Submit_Click(object sender, RoutedEventArgs e)
{
if (subs.IsValid)
MessageBox.Show("True");
else
MessageBox.Show("False");
}
First, your code works as it should, assuming you included all MahApps.Metro resources that are required. Also, you don't need to implement INotifyPropertyChanged in your code-behind (that's your MainWindow I guess).
I'm trying to achieve validation on button click for a textbox using binding.
That is not how IDataErrorInfo works. IDataErrorInfo defines an API that the binding can query for errors on the object that it's bound to. So, when your NAME property is changed, the binding will query the indexer on your sForms object: subs["NAME"]. If it gets an error, error template is applied. This is usually paired with a submit button whose Command property is bound to a command whose CanExecute checks for errors and if there are errors the button is disabled (so you can not submit if there are errors, button is disabled).
If you want to do your validation on button click, you don't need to implement IDataErrorInfo. System.Windows.Controls.Validation class has attached properties that drive presentation of errors: HasError, Errors, ErrorTemplate. But you can't just set Validation.HasError to true (there is no accessible setter) like you can set Validation.ErrorTemplate. To set Validation.HasError in code-behind, you can use Validation.MarkInvalid method, but this is not how these things are usually done. Here is a quick example, for this to work you need to set Name property on your TextBox to MyTextBox:
private void Submit_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(MyTextBox.Text)) return;
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(MyTextBox, TextBox.TextProperty);
BindingExpressionBase bindingExpressionBase =
BindingOperations.GetBindingExpressionBase(MyTextBox, TextBox.TextProperty);
ValidationError validationError =
new ValidationError(new ExceptionValidationRule(), bindingExpression);
validationError.ErrorContent = "My error message.";
Validation.MarkInvalid(bindingExpressionBase, validationError);
}
So if MyTextBox.Text is empty, it will be considered invalid.
Related
I'm having a bit of a problem with WPF property binding. First the code.
C#
public partial class WPFTextBox: UserControl
{
private bool _bold;
public bool Bold
{
get { return _bold; }
set
{
_bold = value;
OnPropertyChanged("Bold");
}
}
private bool _selectionChanged;
public WPFTextBox()
{
InitializeComponent();
DataContext = this;
Bold = true; // <--- This works, the checkbox will be checked
_selectionChanged = false;
}
private void txtDetails_SelectionChanged(object sender, RoutedEventArgs e)
{
var selection = txtDetails.Selection;
_selectionChanged = true;
Bold = selection.FontWeight() == FontWeights.Bold;
// ^-- This doesn't work It will trigger everything, but the checkbox won't
// change value. FontWeight() is an extension I wrote
_selectionChanged = false;
}
private void OnPropertyChanged(string name)
{
if(_selectionChanged)
return; // If the change was brought from the user moving the
// cursor in the textbox, don't change the textbox.
TextRange range = txtDetails.Selection;
switch(name)
{
case "Bold":
// change selection to bold, like I mentioned I does work
break;
default:
break;
}
}
}
XAML
<RichTextBox Name="txtDetails" SelectionChanged="txtDetails_SelectionChanged"/>
<CheckBox Name="chkBold" Content="Bold" IsChecked="{Binding Path=Bold}"/>
I'm creating a textbox with format options. The binding works in the constructor, but not in the selection changed event. I've tried adding a lot of options to the binding such as Mode=TwoWay and different property changed triggers.
The reason I'm using the _selectionChanged bool is because if I don't check for that, if I have a word was different formatting such as hello and I click on it, it will change the formatting for all of the word to either bold or not. I think maybe it's because I'm handling it in selection changed event, but then I'm not sure where else I could change property value.
See the example from here, you can just grab the INPC part.
set
{
_bold = value;
OnPropertyChanged("Bold");
NotifyPropertyChanged();
}
You need to inherit INotifyPropertyChanged interface
and implement PropertyChangedEventHandler
public class WPFTextBox: UserControl,System.ComponentModel.INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And call OnPropertyChanged in the setter of your property
1.You can use UpdateSourceTrigger=PropertyChanged event also.
2.Yes binding work on controls from the Extended WPF Toolkit.
IsChecked="{xcd:Path=Bold}"
I've a hard time understanding why ICommand.CanExecutes always contains the previous value instead of the new value if a nested property is used instead of a normal property.
The problem is described below and I seriously can't figure out a way to fix this besides using some form of "Facade" pattern where I create properties in the viewmodel and hook them to their corresponding property in the model.
Or use the damn CommandManager.RequerySuggested event. The reason this is not optimal is because the view presents over 30 commands, just counting the menu, and if all CanExecute updates every time something changes, it will take a few seconds for all menuitems / buttons to update. Even using the example down below with only a single command and button together with the command manager it takes around 500ms for the button to enable/disable itself.
The only reason I can think of is that the CommandParameter binding is not updated before the CanExecute is fired and then I guess there is nothing you can do about it.
Thanks in advance :!
For example
Let's say we've this basic viewmodel
public class BasicViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set {
this.name = value;
RaisePropertyChanged("Name");
Command.RaiseCanExecuteChanged();
}
}
private Project project;
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
if (value != null) value.PropertyChanged += ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
}
}
private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) {
Command.RaiseCanExecuteChanged();
}
public DelegateCommand<string> Command { get; set; }
public BasicViewModel()
{
this.Project = new Example.Project();
Command = new DelegateCommand<string>(this.Execute, this.CanExecute);
}
private bool CanExecute(string arg) {
return !string.IsNullOrWhiteSpace(arg);
}
private void Execute(string obj) { }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null) {
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
and this model
public class Project : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
RaisePropertyChanged("Text");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Now in my view I've this textbox and button.
<Button Content="Button" CommandParameter="{Binding Path=Project.Text}" Command="{Binding Path=Command}" />
<TextBox Text="{Binding Path=Project.Text, UpdateSourceTrigger=PropertyChanged}" />
It works, every time I type something in the textbox the CanExecute is invoked, BUT the parameter is always set to the previous value. Let say I write 'H' in the textbox, CanExecute is fired with parameter set to NULL. Next I write 'E', now the textbox contains "HE" and the CanExecute fires again. This time with the parameter set to 'H' only.
For some strange reason the parameter is always set to the previous value and when I check the Project.Text it's set to "HE" but parameter is still set to only 'H'.
If I now change the command parameter to
CommandParameter="{Binding Path=Name}"
and the Textbox.Text to
Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"
everything works perfectly. The CanExecute parameter always contain the latest value and not the previous value.
The facade pattern you're talking about it standard WPF practice. The main problem with the way that you're doing it is that when events are raised, their subscribed event handlers execute in the order that they are subscribed. The line of code where you have:
if (value != null) value.PropertyChanged += ChildPropertyChanged;
This subscribes to the "PropertyChanged" Event of your "Project" class. Your UIElements are also subscribed to this same "PropertyChanged" event through your binding in the XAML. In short, your "PropertyChanged" event now has 2 subscribers.
The thing about events is that they fire in a sequence and what's happening in your code, is that when the event fires from your "Project.Text" it executes your "ChildPropertyChanged" event, firing your "CanExecuteChanged" event, which finally runs your "CanExecute" function(which is when you're seeing the incorrect parameter).
THEN, after that, your UIElements get their EventHandlers executed by that same event. And their values get updated.
It's the order of your subscriptions causing the problem. Try this and tell me if it fixes your problem:
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
if (project != null) project.PropertyChanged += ChildPropertyChanged;
}
}
This is how I would have done this, and it works as expected. The only difference here is I'm using RelayCommand instead of DelegateCommand - they fundamentally have the same implementation so they should be interchangeable.
When the user enters the text and then clicks the button the execute method of the RelayCommand gets the expected text - simple.
XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Grid.Column="0"
Grid.Row="0"
Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="0"
Grid.Row="1"
Content="Test"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Command="{Binding Path=TextCommand, Mode=OneWay}" />
</Grid>
ViewModel:
public sealed class ExampleViewModel : BaseViewModel
{
private string _text;
public ExampleViewModel()
{
TextCommand = new RelayCommand(TextExecute, CanTextExecute);
}
public string Text
{
get
{
return _text;
}
set
{
_text = value;
OnPropertyChanged("Text");
}
}
public ICommand TextCommand { get; private set; }
private void TextExecute()
{
// Do something with _text value...
}
private bool CanTextExecute()
{
return true;
}
}
I found this great attached property from swythan on the prism codeplex discussion forum that did the trick very well. Of course it does not answer why the command parameter is set to the previous value but it fixes the problem in a nice way.
The code is slightly modified from the source, enabling the possibility to use it on controls in a TabItem by calling HookCommandParameterChanged when the OnLoaded event is invoked.
public static class CommandParameterBehavior
{
public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
typeof(bool),
typeof(CommandParameterBehavior),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));
public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
{
return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
}
public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
{
target.SetValue(IsCommandRequeriedOnChangeProperty, value);
}
private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is ICommandSource))
return;
if (!(d is FrameworkElement || d is FrameworkContentElement))
return;
if ((bool)e.NewValue)
HookCommandParameterChanged(d);
else
UnhookCommandParameterChanged(d);
UpdateCommandState(d);
}
private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
{
return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
}
private static void HookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);
// N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
// so we need to hook the Unloaded event and call RemoveValueChanged there.
HookUnloaded(source);
}
private static void UnhookCommandParameterChanged(object source)
{
var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);
UnhookUnloaded(source);
}
private static void HookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded += OnUnloaded;
fe.Loaded -= OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded += OnUnloaded;
fce.Loaded -= OnLoaded;
}
}
private static void UnhookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded -= OnUnloaded;
fe.Loaded += OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded -= OnUnloaded;
fce.Loaded += OnLoaded;
}
}
static void OnLoaded(object sender, RoutedEventArgs e)
{
HookCommandParameterChanged(sender);
}
static void OnUnloaded(object sender, RoutedEventArgs e)
{
UnhookCommandParameterChanged(sender);
}
static void OnCommandParameterChanged(object sender, EventArgs ea)
{
UpdateCommandState(sender);
}
private static void UpdateCommandState(object target)
{
var commandSource = target as ICommandSource;
if (commandSource == null)
return;
var rc = commandSource.Command as RoutedCommand;
if (rc != null)
CommandManager.InvalidateRequerySuggested();
var dc = commandSource.Command as IDelegateCommand;
if (dc != null)
dc.RaiseCanExecuteChanged();
}
}
Source: https://compositewpf.codeplex.com/discussions/47338
I am having a binding issue I wasn't able to figure out for the past two days. I have thoroughly went through most of the relevant threads on SO, and I still wasn't able to pinpoint where my error lies.
The issue I'm having is with one of the textboxes in my program. The purpose of it is to show the file the user has selected from the file browser. I have bound the text property of it to a string called parameterFileSelected but the textbox never updates even though debugging seems to be showing that the iNotifyPropertyChanged is called and executed properly.
Please help me take a look at my code below if there are any mistakes in my code.
The textbox is part of an xaml called GenerateReports and this view is tied to the GenerateReportsViewModel as follows:
Code for setting datacontext to GenerateReportsViewModel
<Grid >
<Grid.DataContext>
<vm:GenerateReportsViewModel/>
</Grid.DataContext>
<Grid.ColumnDefinitions>
....
Code for TextBox. I have tried removing the Twoway mode, changing it to Oneway and removing the mode but there is no difference.
<TextBox Grid.Column="2" Grid.Row="1" Margin="5" Text="{Binding parameterFileSelected, Mode=Twoway, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
To get the file browser and then to pass the selected file result to the GenerateReportsViewModel, this is the function in the codebehind file. The genviewmodel is initialized in the beginning of the codebehind file as GenerateReportsViewModel genViewModel = new GenerateReportsViewModel();
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
DataContext = genViewModel;
genViewModel.updateParameterFileSelected(openFileDialog.FileName.ToString());
}
}
This is the code that's called in GenerateReportsViewModel to update the parameterFileSelected string the textbox is bound to.
class GenerateReportsViewModel : ViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Here is the ViewModelBase the viewmodel is attached to.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void SetValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (property != null)
{
if (property.Equals(value)) return;
}
OnPropertyChanged(propertyName);
property = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
EDIT
Working Solution after Applying Kevin's Suggestions
For simplicity sake, the Datacontext was set in the XAML.
<Grid>
<Grid.DataContext>
<vm:GenerateReportsViewModel x:Name="generateReportsViewModel"/>
</Grid.DataContext>
Then, I call the string the textbox was bound to, in the viewmodel directly from code behind.
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
generateReportsViewModel.parameterFileSelected = openFileDialog.FileName.ToString();
}
}
The ViewModel now uses Kevin's ViewModelBase:
public class GenerateReportsViewModel : ViewModelBase
{
public string parameterFileSelected
{
get { return this.GetValue<string>(); }
set { this.SetValue(value); }
}
}
Thank you Kevin for your solution. Now my 2-day-long problem is solved.
I found out that my previous ViewModelBase was calling iNotifyPropertyChanged but somehow when the View was updated, the value was null instead.
I'm trying to understand why using the ref keyword in your viewModel. I learned a nice way to create the BaseViewModel from the Classon and Baxter book which you can find below. The view-model implements the INotifyPropertyChanged like you did. What you did with [CallerMemberName] is great, it's really magical the way we can reference to our properties thanks to it.
The view model uses a the dictionary to store its properties. It uses a pretty neat trick of looking through the dictionnary keys to see if we contain the string name of the property.Otherwise, we will return a default T value.
public class CommonBaseViewModel: INotifyPropertyChanged
{
private Dictionary<string, object> Values { get; set; }
protected CommonBaseViewModel()
{
this.Values = new Dictionary<string, object>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected T GetValue<T>([CallerMemberName] string name=null)
{
if (this.Values.ContainsKey(name))
{
return (T)this.Values[name];
}
else
{
return default(T);
}
}
protected void SetValue(object value, [CallerMemberName] string name = null)
{
this.Values[name] = value;
//notify my property
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected void OnPropertyChanged([CallerMemberName] string name=null)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if(this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
}
As for your GenerateReportViewModel, with the common view model that I provided you, your class then becomes :
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Oh before I forgot, I don't know if it was your intention, but your GenerateReportViewModel is private. This has some impact on your code. Don't forget that by defaut, classes are private!
As for your code behind, even though it could be consider bad practice, I recommend that you have a private field (OpenFileDialog _openFileDialog)that you construct while initializing your page. Because doing it each time your clicking your button is going to consume more data that you need your application to.
//EDIT
I have review my code,and it seemed that the property was not programmed correctly.
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
More about my comment about constructing the page and binding the view model. While creating your page, you have to create the view-model for that page and then bind it to the data context.
I don't know what you do in your code, but I could provide with this sample such as
public GenerateReportView()
{
InitializeComponent();
//Some operations
var generateReportViewModel = new GenerateReportViewModel();
this.DataContext = generateReportViewModel;
}
Overview:
I have a Silverlight 4 app where I am seeing problematic behavior from the ValidationSummary control when used with a ListBox and am hoping someone can help me out.
At a high level, I have a ListBox where each row is defined (via the ItemTemplate) with a textbox named "txtInput". The textbox is bound (TwoWay) to an object property marked with a DataAnnotation.Range attribute. The ListBox is bound to a collection of these objects. In the same parent control, I also have a ValidationSummary control.
Scenario:
Imagine a case where there are two or more objects in the item collection. The user would see the ListBox with multiple rows, each containing a textbox. If the user types invalid data into the first textbox, a ValidationException is thrown as expected, and the ValidationSummary control shows the error, as expected. The textbox also gets the validation error styling (red border).
Then, if the user enters invalid data into the second row's text box (without fixing the data in the first textbox), the second textbox also throws a ValidationException and gets the validation error styling (red border), as expected, HOWEVER, the ValidationSummary control shows only one instance of the error message.
Then, if the user fixes either one (but not both) of the invalid text entries, the fixed textbox has the validation styling (red border) removed, and the ValidationSummary box goes away (meaning it thinks all validation errors have been resolved and has .HasErrors set to false). The second (still invalid) textbox still has the validation error styling (red border) applied.
My expectation is that the still remaining invalid textbox would cause the ValidationSummary control to continue to be displayed. My assumption is that the ValidationSummary control is just keeping track of failures by property name and once there is a successful attempt to set a property of that name, it clears the error marker (ie: it doesn't account for the case where multiple instances of the same name occur).
Wanted Result:
Ultimately, what I'm trying to do is prevent the user from clicking the "Save" button on the screen when there is invalid data. I am currently doing this by binding the IsEnabled property of the button to the HasErrors property of the ValidationSummary, but this doesn't work if the ValidationSummary shows the above behavior.
Can anyone tell me either a way to get the ValidationSummary control to respect multiple failures of the same (repeated) textbox, or provide a viable alternative way to disable the Save button when these failures exist? (note: in my actual app, each row has multiple input controls, so any solution would need to consider that)
XAML snippet:
<sdk:ValidationSummary x:Name="valSummary" />
<ListBox ItemsSource="{Binding DomainObjectCollection, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=true, NotifyOnValidationError=true}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="txtInput" Text="{Binding DecimalValue, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=true, NotifyOnValidationError=true}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="btnSave" Content="Save" Command="{Binding SaveButtonCommand}" IsEnabled="{Binding HasErrors, ElementName=valSummary, Converter={StaticResource NotBoolConverter}}" />
Domain Object classes:
[System.Runtime.Serialization.CollectionDataContractAttribute()]
public partial class DomainObjectCollection : System.Collections.ObjectModel.ObservableCollection<DomainObject>
{
}
[System.Runtime.Serialization.DataContractAttribute()]
public partial class DomainObject : System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.IDataErrorInfo, System.ComponentModel.INotifyDataErrorInfo
{
private int DomainObjectId_BackingField;
private decimal DecimalValue_BackingField;
private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>> _errors;
[System.Runtime.Serialization.DataMemberAttribute()]
public virtual int DomainObjectId
{
get { return this.DomainObjectId_BackingField; }
set
{
if (!DomainObjectId_BackingField.Equals(value))
{
this.DomainObjectId_BackingField = value;
this.RaisePropertyChanged("DomainObjectId");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
[System.ComponentModel.DataAnnotations.RangeAttribute(typeof(decimal), "0", "100", ErrorMessage = "Value must be from 0 to 100.")]
public virtual decimal DecimalValue
{
get { return this.DecimalValue_BackingField; }
set
{
if (!DecimalValue_BackingField.Equals(value))
{
this.DecimalValue_BackingField = value;
this.RaisePropertyChanged("DecimalValue");
}
}
}
string System.ComponentModel.IDataErrorInfo.Error
{
get { return string.Empty; }
}
string System.ComponentModel.IDataErrorInfo.this[string propertyName]
{
get
{
var results = Validate(propertyName);
return results.Count == 0 ? null : string.Join(System.Environment.NewLine, results.Select(x => x.ErrorMessage));
}
}
private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>> Errors
{
get
{
if (_errors == null)
_errors = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>>();
return _errors;
}
}
bool System.ComponentModel.INotifyDataErrorInfo.HasErrors
{
get { return Errors.Count > 0; }
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
public event System.EventHandler<System.ComponentModel.DataErrorsChangedEventArgs> ErrorsChanged;
protected internal void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
private void Raise(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new System.ComponentModel.DataErrorsChangedEventArgs(propertyName));
}
System.Collections.IEnumerable System.ComponentModel.INotifyDataErrorInfo.GetErrors(string propertyName)
{
System.Collections.Generic.List<object> propertyErrors;
if (Errors.TryGetValue(propertyName, out propertyErrors))
return propertyErrors;
return new System.Collections.Generic.List<object>();
}
public void AddError(string propertyName, object error)
{
System.Collections.Generic.List<object> propertyErrors;
if (!Errors.TryGetValue(propertyName, out propertyErrors))
{
propertyErrors = new System.Collections.Generic.List<object>();
Errors.Add(propertyName, propertyErrors);
}
if (propertyErrors.Contains(error))
return;
propertyErrors.Add(error);
Raise(propertyName);
}
public void RemoveError(string propertyName)
{
Errors.Remove(propertyName);
Raise(propertyName);
}
public virtual System.Collections.Generic.List<System.ComponentModel.DataAnnotations.ValidationResult> Validate(string propertyName)
{
var results = new System.Collections.Generic.List<System.ComponentModel.DataAnnotations.ValidationResult>();
var propertyInfo = GetType().GetProperty(propertyName);
if (propertyInfo == null)
return results;
RemoveError(propertyName);
var context = new System.ComponentModel.DataAnnotations.ValidationContext(this, null, null)
{
MemberName = propertyName
};
if (!System.ComponentModel.DataAnnotations.Validator.TryValidateProperty(propertyInfo.GetValue(this, null), context, results))
{
foreach (var validationResult in results)
AddError(propertyName, validationResult.ErrorMessage);
}
return results;
}
}
I ran into this issue awhile back and figured out it happens because the ValidationSummary control uses the name of the control to see if it already has the error in its collection of errors. I was working on a solution and came up with the following behavior. We ended up going a different route due to some issues I ran into because a lot of UI is generated on the fly.
You can take a look and give it a try to see if it might fix your issue:
public class ValidationSummaryCountFixBehavior : Behavior<ValidationSummary>
{
private Dictionary<string, ValidationSummaryItem> _validationErrors = new Dictionary<string, ValidationSummaryItem>();
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
var target = AssociatedObject.Target as FrameworkElement ?? VisualTreeHelper.GetParent(AssociatedObject) as FrameworkElement;
if (target != null)
{
target.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(target_BindingValidationError);
}
AssociatedObject.Loaded -= new RoutedEventHandler(AssociatedObject_Loaded);
}
void target_BindingValidationError(object sender, ValidationErrorEventArgs e)
{
FrameworkElement inputControl = e.OriginalSource as FrameworkElement;
if (((e != null) && (e.Error != null)) && ((e.Error.ErrorContent != null) && (inputControl != null)))
{
string message = e.Error.ErrorContent.ToString();
string goodkey = inputControl.GetHashCode().ToString(CultureInfo.InvariantCulture);
goodkey = goodkey + message;
if (e.Action == ValidationErrorEventAction.Added && ValidationSummary.GetShowErrorsInSummary(inputControl))
{
string messageHeader = null;
ValidationSummaryItem item = new ValidationSummaryItem(message, messageHeader, ValidationSummaryItemType.PropertyError, new ValidationSummaryItemSource(messageHeader, inputControl as Control), null);
_validationErrors[goodkey] = item;
}
else
{
_validationErrors.Remove(goodkey);
}
}
UpdateDisplayedErrors();
}
private void UpdateDisplayedErrors()
{
AssociatedObject.Errors.Clear();
foreach (ValidationSummaryItem item in _validationErrors.Values)
{
AssociatedObject.Errors.Add(item);
}
}
}
say I have this control:
public partial class bloc999 : UserControl
{
bloc999Data mainBlock = new bloc999Data();
public bloc999()
{
InitializeComponent();
mainBlock.txtContents = "100";
base.DataContext = mainBlock;
}
}
in the xaml:
<TextBox Margin="74,116,106,0" Name="txtContents"
Text="{Binding Path=txtContents, UpdateSourceTrigger=PropertyChanged,Mode = TwoWay}" />
<TextBox Margin="74,145,106,132" Name="txtContents2"
Text="{Binding Path=txtContents2, UpdateSourceTrigger=PropertyChanged,Mode = TwoWay}" />
Then I have this class:
public class bloc999Data : INotifyPropertyChanged
{
string _txtContents;
string _txtContents2;
public event PropertyChangedEventHandler PropertyChanged;
void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(
this, new PropertyChangedEventArgs(propName));
}
public string txtContents2
{
get
{
return this._txtContents2;
}
set
{
if (int.Parse(value) > int.Parse(this._txtContents))
{
this._txtContents2 = "000";
}
else
this._txtContents2 = value;
NotifyPropertyChanged("txtContents2");
}
}
public string txtContents
{
get
{
return this._txtContents;
}
set
{
this._txtContents = value;
NotifyPropertyChanged("txtContents");
}
}
}
Ok now say I have A button on the form and I do this in the code:
mainBlock.txtContents2 = "7777777";
It puts 000 in the textbox, but If i just type in manually, in the textbox (txtContents2), the setter code is called but for some reason the textboxes value does not change, the instance value does change. help?
I believe it's just because the value is changing within the context of the data binding operation, so WPF just ignores it because it knows the value is changing and thinks the event is superfluous. What it doesn't know is that you've gone and changed the value from the value WPF has to something else again.
If you do the notification in a separate message then WPF will process it outside the context of the current data binding operation and will thus pick up the change:
if (int.Parse(value) > int.Parse(this._txtContents))
{
this._txtContents2 = "000";
// notify WPF of our change to the property in a separate message
Dispatcher.BeginInvoke((ThreadStart)delegate
{
NotifyPropertyChanged("txtContents2");
});
}
else
{
this._txtContents2 = value;
NotifyPropertyChanged("txtContents2");
}
This assumes your view model has access to the Dispatcher. An example of how to do so is shown in my blog post on a base ViewModel class.
I was having similar problem earlier here
In your usercontrol, update Binding and set UpdateSourceTrigger to Explicit
<TextBox Margin="74,145,106,132" x:Name="txtContents2" TextChanged="txtContents2_TextChanged"
Text="{Binding Path=txtContents2, UpdateSourceTrigger=Explicit,Mode = TwoWay}" />
then in the TextChanged event handler update the binding manually by validating the input.
move validation logic from property txtContent2's setter in bloc999Data in this event handler
private void txtContents2_TextChanged(object sender, TextChangedEventArgs e)
{
if (int.Parse(txtContents2.Text) > int.Parse(mainBlock.txtContents))
{
mainBlock.txtContents2 = "000";
txtContents2.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
}
else
{
mainBlock.txtContents2 = txtContents2.Text;
txtContents2.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
and it works.
Hope it helps!!