开发者

ValidationRules on a ListView's bound items

开发者 https://www.devze.com 2023-01-08 06:34 出处:网络
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.

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 Syst开发者_Go百科em.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.


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>
0

精彩评论

暂无评论...
验证码 换一张
取 消