How to disable Button on validation error - c#

I have a DataGrid like so:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn" Width="*" Header="Latitude">
<DataGridTextColumn.Binding>
<Binding Path="Y">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn x:Name="XColumn" Width="*" Header="Longitude">
<DataGridTextColumn.Binding>
<Binding Path="X">
<Binding.ValidationRules>
<validation:DoubleValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
I have two columns that have the same validation rule (checking to see if the value in the cell is a double):
public class DoubleValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null)
{
double proposedValue;
if (!double.TryParse(value.ToString(), out proposedValue))
{
return new ValidationResult(false, "'" + value.ToString() + "' is not a whole double.");
}
}
return new ValidationResult(true, null);
}
}
This works fine, and a red border is displayed around the cells if the user entered value is not a double. Now I would like to disable a button if there is a validation error with any of the cells.
Following some other posts on this topic, I achieved this using MultiDataTriggers:
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=XColumn}" Value="False" />
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=YColumn}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
This isn't working though. The button never disables even when there is a validation error. What am I doing wrong?
Edit: Here's my model and related code in the view model:
public class CustomVertex
{
public double X { get; set; }
public double Y { get; set; }
public CustomVertex()
{ }
}
public class CustomPolygonViewModel : ViewModelBase
{
public ObservableCollection<CustomVertex> Vertices { get; set; }
public CustomPolygonViewModel()
{
Vertices = new ObservableCollection<CustomVertex>();
}
}
My DataContext is set up correctly and I verified that the model's x and y are being updated on changing the value. The validation rule is being hit properly.

You have to let your view model implement INotifyDataErrorInfo MSDN. Example. Example from MSDN (Silverlight).
Since .Net 4.5 this is the recommended way to introduce validation to your view models and will help you to solve your propblem.
When implementing this interface you will have to provide a HasErrors property that you can bind to. INotifyDataErrorInfo replaces the obsolete IDataErrorInfo.
Binding to the Validation.HasError directly, as you did in your triggers, will not work since Validation.HasError is a read-only attached property and therefore doesn't support binding. To prove this I found this statement on MSDN:
... read-only dependency properties aren't appropriate for many of the scenarios for which dependency properties normally offer a solution (namely: data binding, directly stylable to a value, validation, animation, inheritance).
How INotifyDataErrorInfo works
When the ValidatesOnNotifyDataErrors property of Binding is set to true, the binding engine will search for an INotifyDataErrorInfo implementation on the binding source to subscribe to the ErrorsChanged event.
If the ErrorsChanged event is raised and HasErrors evaluates to true, the binding will invoke the GetErrors() method for the actual property to retrieve the particular error message and apply the customizable validation error template to visualize the error. By default a red border is drawn around the validated element.
How to implement INotifyDataErrorInfo
The CustomVertex class is actually the ViewModel for the DataGrid columns since you are binding to it's properties. So it has to implement the INotifyDataErrorInfo. It could look like this:
public class CustomVertex : INotifyPropertyChanged, INotifyDataErrorInfo
{
public CustomVertex()
{
this.errors = new Dictionary<string, List<string>>();
this.validationRules = new Dictionary<string, List<ValidationRule>>();
this.validationRules.Add(nameof(this.X), new List<ValidationRule>() {new DoubleValidationRule()});
this.validationRules.Add(nameof(this.Y), new List<ValidationRule>() {new DoubleValidationRule()});
}
public bool ValidateProperty(object value, [CallerMemberName] string propertyName = null)
{
lock (this.syncLock)
{
if (!this.validationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
{
return;
}
// Clear previous errors from tested property
if (this.errors.ContainsKey(propertyName))
{
this.errors.Remove(propertyName);
OnErrorsChanged(propertyName);
}
propertyValidationRules.ForEach(
(validationRule) =>
{
ValidationResult result = validationRule.Validate(value, CultuteInfo.CurrentCulture);
if (!result.IsValid)
{
AddError(propertyName, result.ErrorContent, false);
}
}
}
}
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!this.errors.ContainsKey(propertyName))
{
this.errors[propertyName] = new List<string>();
}
if (!this.errors[propertyName].Contains(error))
{
if (isWarning)
{
this.errors[propertyName].Add(error);
}
else
{
this.errors[propertyName].Insert(0, error);
}
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (this.errors.ContainsKey(propertyName) &&
this.errors[propertyName].Contains(error))
{
this.errors[propertyName].Remove(error);
if (this.errors[propertyName].Count == 0)
{
this.errors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!this.errors.ContainsKey(propertyName)) return null;
return this.errors[propertyName];
}
public bool HasErrors
{
get { return errors.Count > 0; }
}
#endregion
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double x;
public double X
{
get => x;
set
{
if (ValidateProperty(value))
{
this.x = value;
OnPropertyChanged();
}
}
}
private double y;
public double Y
{
get => this.y;
set
{
if (ValidateProperty(value))
{
this.y = value;
OnPropertyChanged();
}
}
}
private Dictionary<String, List<String>> errors;
// The ValidationRules for each property
private Dictionary<String, List<ValidationRule>> validationRules;
private object syncLock = new object();
}
The View:
<DataGrid CanUserSortColumns="False" CanUserAddRows="True" ItemsSource="{Binding Vertices}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="YColumn"
Width="*"
Header="Latitude"
Binding="{Binding Y, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
<DataGridTextColumn x:Name="XColumn"
Width="*"
Header="Longitude"
Binding="{Binding X, ValidatesOnNotifyDataErrors=True}"
Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />
</DataGrid.Columns>
</DataGrid>
The following is the validation error template, in case you like to customize the visual representation (optional). It is set on the validated element (in this case the DataGridTextColumn) via the attached property Validation.ErrorTemplate (see above):
<ControlTemplate x:Key=ValidationErrorTemplate>
<StackPanel>
<!-- Placeholder for the DataGridTextColumn itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
The Button that will be disabled when the validation fails (since I don't know where this button is located in the visual tree I will assume that it shares the DataContext of a DataGrid column, the CustomVertex data model):
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=HasErrors}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
There are many examples on the web. I updated the links to provide some content to start with.
I recommend moving the implementation of INotifyDataErrorInfo into a base class together with INotifyPropertyChanged and let all your view models inherit it. This makes the validation logic reusable and keeps your view model classes clean.
You can change the implementation details of INotifyDataErrorInfo to meet requirements.
Remarks: The code is not tested. The snippets should work, but are intended to provide an example how the INotifyDataErrorInfo interface could be implemented.

Related

INotifyDataErrorInfo ErrorsChanged doesn't work for UI element bound to a ListCollectionView

This XAML element is bound to a ListCollectionView in my View Model:
<Style x:Key="ErrorStyle" TargetType="{x:Type Control}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
<Setter Property="Background" Value="Salmon"/>
</Trigger>
</Style.Triggers>
</Style>
...
<controls:AutoCompleteBox Grid.Column="1" Grid.Row="0" Margin="5" Height="20" Width="270" HorizontalAlignment="Left" VerticalAlignment="Center"
Name="typeName"
Style="{StaticResource ErrorStyle}"
Text="{Binding Path=AirframeCollectionView/TypeName, UpdateSourceTrigger=LostFocus, Mode=TwoWay,
ValidatesOnNotifyDataErrors=True,
NotifyOnValidationError=True,
ValidatesOnExceptions=True}"
ItemsSource="{Binding Path=TypeNames}"
IsTextCompletionEnabled="True"
FilterMode="Contains"
MinimumPrefixLength="3">
</controls:AutoCompleteBox>
The ListCollectionView is defined thus:
public ListCollectionView AirframeCollectionView
{
get
{
return this.airframeCollectionView;
}
set
{
this.airframeCollectionView = value;
this.RaisePropertyChanged("AirframeCollectionView");
}
}
and initialised:
this.currentAirframes = new ObservableCollection<Airframe>(this.UnitOfWork.Airframes.GetAirframesForRegistration(this.SearchRegistration));
this.AirframeCollectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(this.currentAirframes);
When validating AirframeCollectionView/TypeName I'm using the INotifyDataErrorInfo interface hence:
private readonly Dictionary<string, ICollection<string>> validationErrors = new Dictionary<string, ICollection<string>>();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get { return this.validationErrors.Count > 0; }
}
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName) || !this.validationErrors.ContainsKey(propertyName))
{
return null;
}
return this.validationErrors[propertyName];
}
private void RaiseErrorsChanged(string propertyName)
{
if (this.ErrorsChanged != null)
{
this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
To raise an error I've been doing this:
this.validationErrors["AirframeCollectionView/TypeName"] = validationErrors;
this.RaiseErrorsChanged("AirframeCollectionView/TypeName");
This doesn't trigger the error response in the UI however. I changed the property name from "AirframeCollectionView/TypeName" to "TypeName" but that doesn't work either. In the debugger I've confirmed that validationErrors gets loaded with errors and that ErrorsChanged is fired with the supplied property name.
Note that this was all working when I implemented INotifyDataErrorInfo in the Model rather than the ViewModel but for various reasons I want the implementation to be in ViewModel.
Question
What property name format do I have to use when setting up the DataErrorsChangedEventArgs and triggering ErrorsChanged? Or is there some other structural problem I have here?
I conclude that you can't get INotifyDataErrorInfo to talk to the UI when using a ListCollectionView property in your view model and firing ErrorsChanged from the view model. To get this working therefore I have:
In the model (POCO) - Implemented INotifyDataError. Included a public RaiseErrorsChanged method which allows a property name and error list to be passed in. This adds the errors to an error dictionary and then fires ErrorsChanged.
In the view model - Subscribed to the PropertyChanged event for each Airframe object in the ListCollectionView. In the PropertyChanged event handler I carry out validation and then call the airframe's RaiseErrorsChanged method with any error details.
This keeps bespoke validation out of the model and all is well.

ListBox binding to ObservableCollection<string>

In the following class, the itemssource of a listbox should bind to the Interfaces property.
public class BaseClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private const string TYPE_TITLE = "Type";
private string _Type;
public string Type
{
get { return _Type; }
set { _Type = value; this.NotifyPropertyChanged(PropertyChanged, TYPE_TITLE); }
}
public ObservableCollection<string> Interfaces { get; set; }
public BaseClass()
{
Interfaces = new ObservableCollection<string>();
}
public void Reset()
{
_Type = null;
Interfaces.Clear();
}
}
In that list box the selected item should be able to edit as the inline edit scenario,
<DataTemplate x:Key="BaseClass_Interfaces_InlineEdit_Template">
<TextBox Text="{Binding Mode=TwoWay, Path=., NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}" TextChanged="TextBox_TextChanged"/>
</DataTemplate>
<DataTemplate x:Key="BaseClass_Interfaces_InlineView_Template">
<TextBlock Text="{Binding Path=., UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}" x:Key="BaseClass_Iterfaces_ItemStyle_Template">
<Setter Property="ContentTemplate" Value="{StaticResource BaseClass_Interfaces_InlineView_Template}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="ContentTemplate" Value="{StaticResource BaseClass_Interfaces_InlineEdit_Template}" />
</Trigger>
</Style.Triggers>
</Style>
The ListBox has a container as a parent hierarchy which its DataContext property bind to an instance of BaseClass hence the ListBox could bind to the Interfaces property.
<ListBox Grid.Row="2" Grid.ColumnSpan="2" Margin="3" ItemsSource="{Binding Interfaces, Mode=TwoWay}" SelectionMode="Single"
ItemContainerStyle="{StaticResource ResourceKey=BaseClass_Iterfaces_ItemStyle_Template}" />
The list box before select any item
Editing the selected item
Another item select after edit and the changes doesn't affected
There are two problems :
The TextBox should have "Path=." otherwise the "Two-way binding requires Path or XPath." exception message received.
With consider the above problem, the ObservableCollection items never updated after text changed!!!!!!
I found the answer!
My wondering was about the text box which the text property changed but the changes does not propagated to the source, based on the link the binding mechanism works on the properties of sources in the other words the change of the properties monitors not the object itself.
The solution is a wrapper class around the string, i wrote this wrapper before for another primitive type (bool).
public class Wrapper<T>
{
public T Item { get; set; }
public Wrapper(T value = default(T))
{
Item = value;
}
public static implicit operator Wrapper<T>(T item)
{
return new Wrapper<T>(item);
}
public static implicit operator T(Wrapper<T> item)
{
if (null != item)
return item.Item;
return default(T);
}
}
So the editing data template change as follow
<DataTemplate x:Key="BaseClass_Interfaces_InlineEdit_Template">
<TextBox Text="{Binding Mode=TwoWay, Path=Item, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
And every thing work as charm!!!

ObservableCollection is not update after DataGrid Cell Change

I am new in wpf and mvvm databinding. Now i am trying to make crud process with data gird. I have a problem to updateing process.I want to get update value after data grid cell updated by mvvm process.
Model
public class EmployeeType : INotifyPropertyChanged
{
string _EmpType;
public string EmpType
{
get
{
return _EmpType;
}
set
{
if(_EmpType !=value)
{
_EmpType = value;
RaisePropertyChange("EmpType");
}
}
}
string _EmpTypeDesc;
public string EmpTypeDesc
{
get
{
return _EmpTypeDesc;
}
set
{
if(_EmpTypeDesc!=value)
{
_EmpTypeDesc = value;
RaisePropertyChange("EmpTypeDesc");
}
}
}
bool _OTRounding;
public bool OTRounding
{
get
{
return _OTRounding;
}
set
{
if(_OTRounding!=value)
{
_OTRounding = value;
RaisePropertyChange("OTRounding");
}
}
}
decimal _EarlyOTTimeBuffer;
public decimal EarlyOTTimeBuffer
{
get
{
return _EarlyOTTimeBuffer;
}
set
{
if(_EarlyOTTimeBuffer!=value)
{
_EarlyOTTimeBuffer = value;
RaisePropertyChange("EarlyOTTimeBuffer");
}
}
}
string _EarlyOTRounding;
public string EarlyOTRounding
{
get
{
return _EarlyOTRounding;
}
set
{
if(_EarlyOTRounding!=value)
{
_EarlyOTRounding = value;
RaisePropertyChange("EarlyOTRounding");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChange(string prop)
{
if(PropertyChanged !=null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
View Model
class EmployeeTypeViewModel:ViewModelBase
{
private ObservableCollection<EmployeeType> _EmployeeTypeList = new ObservableCollection<EmployeeType>();
private ObservableCollection<TimeFormat> _ThreeTimeFormat = new ObservableCollection<TimeFormat>();
public ObservableCollection<TimeFormat> ThreeTimeFormat
{
get
{
return _ThreeTimeFormat;
}
set
{
_ThreeTimeFormat = value;
RaisePropertyChanged("ThreeTimeFormat");
}
}
public ObservableCollection<EmployeeType> EmployeeTypeList
{
get
{
return _EmployeeTypeList;
}
set
{
_EmployeeTypeList = value;
RaisePropertyChanged("EmployeeTypeList");
}
}
public EmployeeType _SelectedEarlyOTRounding;
public EmployeeType SelectedEarlyOTRounding
{
get
{
return _SelectedEarlyOTRounding;
}
set
{
if (_SelectedEarlyOTRounding != value)
{
_SelectedEarlyOTRounding = value;
RaisePropertyChanged("SelectedEarlyOTRounding");
}
}
}
public EmployeeTypeViewModel()
{
_EmployeeTypeList = DataAccess.EmployeeTypeDataAccessor.GetAllEmployeeTypes();
ThreeTimeFormat = TMSHelper.GetThreeTimeFormat();
}
}
View
<UserControl.Resources>
<ViewModels:EmployeeTypeViewModel x:Key="ViewModel"/>
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource ViewModel}}">
<DataGrid Margin="10,10,9.6,10.2" x:Name="empgrid" ItemsSource="{Binding EmployeeTypeList,Mode=TwoWay}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="EmpType" Binding="{Binding EmpType,Mode=TwoWay}"/>
<DataGridCheckBoxColumn Header="OTRounding" Binding="{Binding OTRounding}"/>
<DataGridTextColumn Header="Description" Binding="{Binding EmpTypeDesc}"/>
<DataGridTextColumn Header="Early OT Time Buffer" Binding="{Binding EarlyOTTimeBuffer}"/>
<DataGridTemplateColumn Header="Early OT Time Rounding">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedValuePath="Value" DisplayMemberPath="Key" ItemsSource="{Binding Path=DataContext.ThreeTimeFormat,ElementName=empgrid}" SelectedValue="{Binding EarlyOTRounding,Mode=TwoWay}" SelectedItem="{Binding SelectedEarlyOTRounding}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
I just realized things is if I changed value in datagrid cell,It's will auto update the EmployeeTypeList in viewmodel.Because i added mode=twoway in itemsource of grid.right? But when i debug, Set of EmployeeTypeList never happen.Why? if my doing process is wrong,please let me known how to do that? If you don't understand,please let me known.Thanks.
You probably just don't understand to binding completly at this point and it is okay.
Mode=TwoWay means that binded property will be changed on the UI when value is changed in the underlying object and also when user change the value on the UI.
In your case you should have to replace collection on the UI to notice the change. So, far you are changing content of the ObservableCollection and because of that you are not getting any notification on collection level. You should have get notification for EmpType when you change it on the UI.
Is it clear?
One of the BindingMode values. The default is Default, which returns
the default binding mode value of the target dependency property.
However, the default value varies for each dependency property. In
general, user-editable control properties, such as those of text boxes
and check boxes, default to two-way bindings, whereas most other
properties default to one-way bindings.
(source: MSDN )
so generally where it is possible to edit usually does not need to inform
now, for your combobox to work correctly try this.
I usually use it and works perfectly
<DataGridComboBoxColumn Header="Early OT Time Rounding"
DisplayMemberPath="Key"
SelectedValuePath="Value"
SelectedValueBinding="{Binding EarlyOTRounding, UpdateSourceTrigger=PropertyChanged}"
>
<DataGridComboBoxColumn.ElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.ThreeTimeFormat, UpdateSourceTrigger=PropertyChanged}"/>
<Setter Property="Width" Value="280" />
</Style>
</DataGridComboBoxColumn.ElementStyle>
<DataGridComboBoxColumn.EditingElementStyle>
<Style TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=DataContext.ThreeTimeFormat, UpdateSourceTrigger=PropertyChanged}"/>
</Style>
</DataGridComboBoxColumn.EditingElementStyle>
</DataGridComboBoxColumn>
attention to this
DataGridComboBoxColumn must have a SelectedValueBinding property set to being an item in your EmployeeTypeList so that the selected value is changed in the list
in your case:
SelectedValueBinding="{Binding EarlyOTRounding, UpdateSourceTrigger=PropertyChanged}"

WPF- validation error event doesn't fire

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");
}
}

Setting a treeview items text colour in realtime based on a ViewModel property

In my WPF application I am using the MVVM pattern. My view has a treeview which I bind an observableCollection of objects as defined below. What I want to do is to change the colour of a tree item name when the bound object sets it’s dirty property to true. I can get it to set the colour when I first populate the tree but then it doesn’t reflect the changes when the property changes between false and true.
public class HierarchicalItem
{
private readonly ObservableCollection<HierarchicalItem> _children = new ObservableCollection<HierarchicalItem>();
public ViewModelBase ViewModel { get; set; }
public string Name
{
get { return ViewModel.ViewModelName; }
}
public ICollection<HierarchicalItem> Children
{
get { return _children; }
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (_isSelected)
EventSystem.Publish(new SelectedViewModelMessage { SelectedViewModel = ViewModel });
}
}
public bool IsDirty
{
get { return ViewModel.IsDirty; }
}
}
This is the treeview xaml:
<TreeView Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Path=Views}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:HierarchicalItem}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDirty}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Here is the collection that gets bound to the tree:
private readonly ObservableCollection<HierarchicalItem> _views = new ObservableCollection<HierarchicalItem>();
public ObservableCollection<HierarchicalItem> Views
{
get { return _views; }
}
The ViewModels that are referenced in the HierarchicalItem collection all derive from a base class that exposes the “IsDirty” property. This is definantly changing state so I’m not sure if I’ve made a coding mistake or if what I want to achieve can’t be done this way. The classes all use the “INotifyPropertyChanged” interface. Here is the “IsDirty” property in from the ViewModel base class:
public class ViewModelBase : ValidatableModel
{
#region Properties
private bool _isDirty;
public bool IsDirty
{
get { return _isDirty; }
protected set
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
.
.
.
Etc
It's because your HierarchicalItem (the one you are having issues with) does not use a full INPC approach for its IsDirty property. The viewmodel does, but that is not enough, as the DataTemplate will be using the IsDirty property of the HierarchicalItem, so that needs to be full INPC property too
Changed that to this and it should be ok.
private bool _isDirty;
public bool IsDirty
{
get { return _isDirty; }
protected set
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
Though for your use case you will need to figure out some way to fire that. Or another thing you could try would be to change the binding in HierarchicalItem DataTemplate to this
<DataTrigger Binding="{Binding ViewModel.IsDirty}" Value="True">

Categories

Resources