views:

32

answers:

1

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.

+1  A: 

Validation.HasError is only being set on the TextBlock for the individual cell, because that is where the binding is applied. This is one of the children of the ListViewItem, but not the ListViewItem itself. It is not being set on the Window either, which is why your message box always displays "valid!".

One approach you could use to highlight the entire row when one cell fails validation is to set the ValidationAdornerSite for the cell to be its row. This will cause the ErrorTemplate for the ListViewItem to be applied, which by default will give it a red border. Try adding a style like this:

<Style TargetType="TextBlock">
    <Setter
        Property="Validation.ValidationAdornerSite"
        Value="{Binding RelativeSource={RelativeSource AncestorType=ListViewItem}}"/>
</Style>
Quartermeister
I've been playing around with this for a while, and I've not been able to get this to work. It seems as if the TextBlock isn't validating at all (although, I do see the validation rule being invoked). I've been using the VisualTreeHelper to examine the ListViewItem's children and manually call `Validation.GetHasError` on each `TextBlock` found (of course navigating down through the `GridViewRowPresenter`), and it always returns false, regardless of whether or not the validation rule has failed. Any ideas?
Nathan Ernst
@Nathan: If I run exactly the code you have posted in your question, Validation.GetHasError returns true on the TextBlock for the empty name string. I'm running 3.5 SP1 too, so I don't know what the difference could be.
Quartermeister
@Quartermeister, curious...I'll have to dig a bit deeper with the reduced code. Could be that some firm-wide styles I have to use in my actual app are affecting the triggers (which is where I've been testing the various methods recently). I'll let you know what I find.
Nathan Ernst
Was just playing around with my small app, and I could not get the background of the `ListViewItem` to change. I got the standard red border, but no style triggers on `(Validation.ValidationAdornerSiteFor).(Validation.HasError)` with relative source 'self' triggered any style changes at all.
Nathan Ernst
@Nathan: I think the reason that binding to ValidationAdornerSiteFor doesn't work is that multiple TextBlocks have the same ListViewItem as their ValidationAdornerSite. ValidationAdornerSiteFor is only returning one (maybe the last one), and that one TextBlock doesn't have any errors.
Quartermeister
@Quartermeister, right you are. If I remove the second column, my trigger works correctly. I think I'm going to have to write a custom attached property for ListViewItem that can check all of its children for errors in order to apply the styling I want. Thanks for the help!
Nathan Ernst