wpfxaml

Disable Button if ItemsControl has validation errors


I have a MVVM (Prism) application. UserControl has these elements:

<Grid>
    <ItemsControl x:Name="MyItemsControl"
                  ItemsSource="{Binding Path=OrderCollection}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBox x:Name="MyTextBox">
                    <TextBox.Text>
                        <Binding Path="ID" Mode="TwoWay">
                            <Binding.ValidationRules>
                                <vr:OrderIDValidationRule/>
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    <Button x:Name="MyButton">
    </Button>
</Grid>

OrderIDValidationRule is just a simple validation rule that checks if ID is within valid range. Code from the ViewModel:

public ObservableCollection<Order> OrderCollection;

public class Order : BindableBase
{
    private int? _id;
    public int? ID
    {
        get => _id;
        set => SetProperty(ref _id, value);
    }
}

Is there a way to disable MyButton, if MyTextBox (with any Order) has some bad value. For instance, if user enters "abc". In that case, validation error is displayed, but binding source is not updated - not sure if INotifyDataErrorInfo can help here


Solution

  • Example based on my answer https://stackoverflow.com/a/74293719/13349759

    using System;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Data;
    
    namespace CommonCore
    {
        public static class BindingExpressionHelper
        {
            private static readonly Func<BindingExpressionBase, DependencyObject, DependencyProperty, object> GetValueOfBindingExpression;
    
            static BindingExpressionHelper()
            {
                Type beType = typeof(BindingExpressionBase);
                var beMethod = beType
                    .GetMethod("GetValue", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(DependencyObject), typeof(DependencyProperty) })
                    ?? throw new Exception("GetValue method not found.");
                var beFunc = (Func<BindingExpressionBase, DependencyObject, DependencyProperty, object>)
                    beMethod.CreateDelegate(typeof(Func<BindingExpressionBase, DependencyObject, DependencyProperty, object>));
                GetValueOfBindingExpression = beFunc;
            }
    
            /// <summary>Returns the source value of this binding expression.</summary>
            /// <param name="bindingExpression">The binding expression whose value to get.</param>
            /// <returns>The value of the binding expression received.</returns>
            public static object GetSourceValue(this BindingExpressionBase bindingExpression)
            {
                DependencyObject target = bindingExpression.Target;
                DependencyProperty targetProperty = bindingExpression.TargetProperty;
                var value = GetValueOfBindingExpression(bindingExpression, target, targetProperty);
    
                return value;
            }
        }
    
    }
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using static System.Windows.Media.VisualTreeHelper;
    
    
    namespace CommonCore.Helpers
    {
        public static partial class VisualTreeHelper
        {
            public static IEnumerable<DependencyObject> GetChildren(this DependencyObject parent)
            {
                ArgumentNullException.ThrowIfNull(parent);
                Queue<DependencyObject> queue = new Queue<DependencyObject>(16);
                queue.Enqueue(parent);
                while (queue.Count != 0)
                {
                    DependencyObject current = queue.Dequeue();
                    yield return current;
    
                    int count = GetChildrenCount(current);
                    for (int i = 0; i < count; i++)
                    {
                        queue.Enqueue(GetChild(current, i));
                    }
                }
            }
            public static IEnumerable<T> GetChildren<T>(this DependencyObject parent)
               where T : DependencyObject
                => parent.GetChildren().OfType<T>();
    
            public static T GetFirstChild<T>(this DependencyObject parent)
               where T : DependencyObject
                => (T)parent.GetChildren().FirstOrDefault(child => child is T);
    
        }
    }
    
    using System.Globalization;
    using System.Windows.Controls;
    using System.Windows.Data;
    using CommonCore;
    
    namespace Core2024.SO.AlexeyTitov.question79081208
    {
        public class OrderIDValidationRule : ValidationRule
        {
    
            public override ValidationResult Validate(object value, CultureInfo cultureInfo)
            {
    
                int id;
                switch (value)
                {
                    case int _id: id = _id; break;
                    case string text: id = int.Parse(text, cultureInfo); break;
                    case BindingExpressionBase bindingExpression:
                        id = (int)(bindingExpression.GetSourceValue() ?? 0);
                        break;
                    default:
                        id = int.MinValue;
                        break;
                }
    
                if (id < 0 || id > 100)
                    return new ValidationResult(false, "Значение вне диапазона [0..100]");
                return ValidationResult.ValidResult;
            }
        }
    
    }
    
    using Simplified;
    using System.Collections.ObjectModel;
    
    namespace Core2024.SO.AlexeyTitov.question79081208
    {
        public class Order : BaseInpc
        {
            private int? _id;
            public int? ID
            {
                get => _id;
                set => Set(ref _id, value);
            }
        }
    
        public class OrdersViewMode : BaseInpc
        {
            public ObservableCollection<Order> OrderCollection { get; }
                = new ObservableCollection<Order>
                (
                    Enumerable.Range(0, 20).Select(_ => new Order() { ID = Random.Shared.Next(-20, 120) })
                );
        }
    
    }
    
    <Window x:Class="Core2024.SO.AlexeyTitov.question79081208.TestErrorHandlerWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Core2024.SO.AlexeyTitov.question79081208"
            mc:Ignorable="d"
            Title="TestErrorHandlerWindow" Height="450" Width="800">
        <Window.DataContext>
            <local:OrdersViewMode/>
        </Window.DataContext>
            <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <ItemsControl x:Name="MyItemsControl"
                          ItemsSource="{Binding Path=OrderCollection}"
                          Validation.Error="OnOrderError">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBox x:Name="MyTextBox">
                                <TextBox.Text>
                                    <Binding Path="ID" Mode="TwoWay" NotifyOnValidationError="True">
                                        <Binding.ValidationRules>
                                        <!--<local:OrderIDValidationRule ValidationStep="ConvertedProposedValue"/>-->
                                        <local:OrderIDValidationRule ValidationStep="ConvertedProposedValue"
                                                                     ValidatesOnTargetUpdated="True"/>
                                    </Binding.ValidationRules>
                                    </Binding>
                                </TextBox.Text>
                            </TextBox>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
    
                <Button x:Name="MyButton" Grid.Row="1"
                        Margin="5" Padding="15 5"
                        Content="Click Me"
                        HorizontalAlignment="Center" VerticalAlignment="Center">
                </Button>
            <x:Code>
                <![CDATA[
            private void OnOrderError(object sender, ValidationErrorEventArgs e)
            {
                bool hasError = ((DependencyObject)sender).GetChildren().Any(Validation.GetHasError);
                MyButton.IsEnabled = !hasError;
            }
                ]]>
            </x:Code>
            </Grid>
    </Window>
    

    P.S. If you are "not afraid" of the Russian language, you can look at another implementation here: How to get validation errors from all child elements