My actual scenario is this: I have a ListView
and a custom UserControl
setup in a master-detail. It is possible, through a menu item, to add multiple items that are initially invalid.
What I'd like to do is, eventually, prevent submission if any items in the list are invalid. Short term, I'm trying to give a visual clue to the item that is invalid. My thought is, introduce a style to the ListView
targeting ListViewItem
trigger on the ListViewItem
's attached Validation.HasError
property to trigger the entire row's background to become red.
To perform this, I have, of course, added the style, and introduced a simple validation rule that I use the in the DisplayMemberBinding
of the GridViewColumn
. I've verified with the debugger that the rule is being invoke, and that the rule functions as expected, but I do not see the style changes.
I've included all relevant portions below in a reproduction. I'd appreciate any help here. I should note, that the button always generates a message box with "valid!" as the text, as well, despite the debugger showing the failed rule being hit.
I'm also using .Net 3.5 SP1.
Person.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ListViewItemValidation
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
static Person[] _Data;
public static Person[] Data
{
get
{
if (_Data == null)
{
_Data =new[]{
new Person() { Name="John", Age=30},
new Person() { Name="Mary", Age=40},
new Person() { Name="", Age=20},
new Person() { Name="Tim", Age=-1},
};
}
return _Data;
}
}
}
}
RequiredStringValidator.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;
namespace ListViewItemValidation
{
public class RequiredStringValidator : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (string.IsNullOrEmpty(value as string))
return new ValidationResult(false, "String cannot be empty.");
return ValidationResult.ValidResult;
}
}
}
Window1.xaml:
<Window
x:Class="ListViewItemValidation.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ListViewItemValidation"
Title="Window1" Height="300" Width="300">
<DockPanel>
<Button Content="Validate"
DockPanel.Dock="Bottom"
Click="ValidateClicked"
/>
<ListView
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Static l:Person.Data}"
>
<ListView.Resources>
<Style TargetType="ListViewItem">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Name">
<GridViewColumn.DisplayMemberBinding>
<Binding Path="Name">
<Binding.ValidationRules>
<l:RequiredStringValidator
ValidatesOnTargetUpdated="True"
ValidationStep="RawProposedValue"
/>
</Binding.ValidationRules>
</Binding>
</GridViewColumn.DisplayMemberBinding>
</GridViewColumn>
<GridViewColumn
Header="Age"
DisplayMemberBinding="{Binding Path=Age}"
/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Window>
Window1.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ListViewItemValidation
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void ValidateClicked(object sender, RoutedEventArgs e)
{
if (Validation.GetHasError(this))
MessageBox.Show("invalid!");
else
MessageBox.Show("valid!");
}
}
}
Update: Final Solution I added the following class to provide an attached property for ListViewItem to check if any children contain bound properties with validation rules that have failed:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace ListViewItemValidation
{
public class ListViewItemExtensions
{
#region ChildrenHaveError Property
public bool ChildrenHaveError
{
get { return (bool)this.ListViewItem.GetValue(ChildrenHaveErrorProperty); }
set { this.ListViewItem.SetValue(ChildrenHaveErrorProperty, value); }
}
public static bool GetChildrenHaveError(ListViewItem obj)
{
return EnsureInstance(obj).ChildrenHaveError;
}
public static void SetChildrenHaveError(ListViewItem obj, bool value)
{
EnsureInstance(obj).ChildrenHaveError = value;
}
public static readonly DependencyProperty ChildrenHaveErrorProperty =
DependencyProperty.RegisterAttached(
"ChildrenHaveError",
typeof(bool),
typeof(ListViewItemExtensions),
new PropertyMetadata(
new PropertyChangedCallback((o, a) => { EnsureInstance((ListViewItem)o); })
)
);
#endregion
#region ValidatesChildren Property
public bool ValidatesChildren
{
get { return (bool)this.ListViewItem.GetValue(ValidatesChildrenProperty); }
set { this.ListViewItem.SetValue(ValidatesChildrenProperty, value); }
}
public static bool GetValidatesChildren(ListViewItem obj)
{
return EnsureInstance(obj).ValidatesChildren;
}
public static void SetValidatesChildren(ListViewItem obj, bool value)
{
EnsureInstance(obj).ValidatesChildren = value;
}
public static readonly DependencyProperty ValidatesChildrenProperty =
DependencyProperty.RegisterAttached(
"ValidatesChildren",
typeof(bool),
typeof(ListViewItemExtensions),
new PropertyMetadata(
new PropertyChangedCallback((o, a) => { EnsureInstance((ListViewItem)o); })
)
);
#endregion
#region Instance Property
public static ListViewItemExtensions GetInstance(ListViewItem obj)
{
return (ListViewItemExtensions)obj.GetValue(InstanceProperty);
}
public static void SetInstance(ListViewItem obj, ListViewItemExtensions value)
{
obj.SetValue(InstanceProperty, value);
}
public static readonly DependencyProperty InstanceProperty =
DependencyProperty.RegisterAttached("Instance", typeof(ListViewItemExtensions), typeof(ListViewItemExtensions));
#endregion
#region ListViewItem Property
public ListViewItem ListViewItem { get; private set; }
#endregion
static ListViewItemExtensions EnsureInstance(ListViewItem item)
{
var i = GetInstance(item);
if (i == null)
{
i = new ListViewItemExtensions(item);
SetInstance(item, i);
}
return i;
}
ListViewItemExtensions(ListViewItem item)
{
if (item == null)
throw new ArgumentNullException("item");
this.ListViewItem = item;
item.Loaded += (o, a) =>
{
this.FindBindingExpressions(item);
this.ChildrenHaveError = ComputeHasError(item);
};
}
static bool ComputeHasError(DependencyObject obj)
{
var e = obj.GetLocalValueEnumerator();
while (e.MoveNext())
{
var entry = e.Current;
if (!BindingOperations.IsDataBound(obj, entry.Property))
continue;
var binding = BindingOperations.GetBinding(obj, entry.Property);
foreach (var rule in binding.ValidationRules)
{
ValidationResult result = rule.Validate(obj.GetValue(entry.Property), null);
if (!result.IsValid)
{
BindingExpression expression = BindingOperations.GetBindingExpression(obj, entry.Property);
Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null));
return true;
}
}
}
for (int i = 0, count = VisualTreeHelper.GetChildrenCount(obj); i < count; ++i)
if (ComputeHasError(VisualTreeHelper.GetChild(obj, i)))
return true;
return false;
}
void OnDataTransfer(object sender, DataTransferEventArgs args)
{
this.ChildrenHaveError = ComputeHasError(this.ListViewItem);
}
void FindBindingExpressions(DependencyObject obj)
{
var e = obj.GetLocalValueEnumerator();
while (e.MoveNext())
{
var entry = e.Current;
if (!BindingOperations.IsDataBound(obj, entry.Property))
continue;
Binding binding = BindingOperations.GetBinding(obj, entry.Property);
if (binding.ValidationRules.Count > 0)
{
Binding.AddSourceUpdatedHandler(obj, new EventHandler<DataTransferEventArgs>(this.OnDataTransfer));
Binding.AddTargetUpdatedHandler(obj, new EventHandler<DataTransferEventArgs>(this.OnDataTransfer));
}
}
for (int i = 0, count = VisualTreeHelper.GetChildrenCount(obj); i < count; ++i)
{
var child = VisualTreeHelper.GetChild(obj, i);
this.FindBindingExpressions(child);
}
}
}
}
Then, I modified the ListViewItem
style to be:
<Style TargetType="ListViewItem">
<Style.Setters>
<Setter Property="l:ListViewItemExtensions.ValidatesChildren" Value="True" />
</Style.Setters>
<Style.Triggers>
<Trigger Property="l:ListViewItemExtensions.ChildrenHaveError" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
Many thanks to @Quartermeister for helping me out with this one.