0

I have to show a list where each item will be validated. I am subscribed to Validation.ErrorEvent on a top level to monitor for children.

When I remove item with validation error from list this event is not rised.

In example below I have 3 TextBox on screen, each is bound to int property. Entering wrong value will fire event (Title is changed to "+"), fixing value afterwards will fire event once (Title is changed to "-").

However removing TextBox while having error will not rise event (to clean up) and Title stay "+":

How can I fix that? Ideally I want that this event is automatically rised before removing happens.

Please note: in real project there is complex hierarchy of view models, solutions like "set Title in delete method" would require monitoring for sub-views and propagating that info through all hierarchy, which I'd like to avoid. I'd prefer view-only solution.

MCVE:

public partial class MainWindow : Window
{
    public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };

    public MainWindow()
    {
        InitializeComponent();
        AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
            Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));
        DataContext = this;
    }

    void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);
}

public class VM
{
    public int Test { get; set; }
}

xaml:

<StackPanel>
    <ItemsControl ItemsSource="{Binding Items}" Height="200">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBox Text="{Binding Test, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
    <Button Content="Remove first" Click="Button_Click" />
</StackPanel>
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • You're removing the vm, nothing will be fired... The collection changed as well, nothing is fired telling anyone that the collection has changed. – Trevor Oct 24 '19 at 13:44
  • @Çöđěxěŕ, [CollectionChanged](https://learn.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1.collectionchanged) is fired. Or what do you mean by nothing? – Sinatr Oct 24 '19 at 13:54
  • 1
    The errors are being stored in an attached property on the text boxes: Try `var errors = Validation.GetErrors(e.OriginalSource as DependencyObject);` in your ErrorEvent handler. All you're doing is monitoring changes in those lists, and at the time the TextBoxes are unloaded, their errors are intact. I suppose you could keep a Dictionary of errors `Dictionary>` and update it on ErrorEvent and Items.CollectionChanged. You're using the UI as a data structure, which is never a good idea. The MVVM way to do validation is IDataErrorInfo. – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 13:56
  • 1
    Just because a feature exists in WPF doesn't mean it's good MVVM practice to use it in any particular way. – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 13:57
  • What I was trying to say is that validation only fires when the bindings update. Have you tried setting `ValidatesOnTargetUpdated="True"`, in your binding, this is part of `ValidationRule`... – Trevor Oct 24 '19 at 14:01
  • @EdPlunkett, currently VM is not aware about convertion error and I totally like it. But on higher level I care about such errors in child views to prevent navigation away. What you suggest is to: 1) inform VM about such errors 2) pass them all the way up (e.g. by using events). Even if it's a good practice, I'd like to avoid that. Binding to another element property is ok, why binding to another element red border (view-to-view) wouldn't be so? In my case I just want to monitor for all red borders. – Sinatr Oct 24 '19 at 14:32
  • If you prefer to keep all of your validation in the view, you could handle the Unloaded event on the textboxes and do something there. Call `Validation.GetErrors(sender as DependencyObject)` to get the errors on the outgoing textbox. What you do then is up to you, but it won't be pretty. I'm not sure what you mean by "binding to another element red border (view-to-view)". – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 14:39
  • Actually one possible solution: [polling](https://stackoverflow.com/q/58431927/1997232). I could periodically poll visual tree for validation errors when `Title` is `"+"`. Hmm.. – Sinatr Oct 24 '19 at 14:40
  • There's a boolean `Validation.HasError` attached property you could bind something to, but it's not going to be reset to false when a textbox with errors is unloaded. Periodic polling does not sound like a good idea. You have events that are raised when anything changes. Scanning the visual tree for Validation.HasError is bad enough without polling. Do it on TextBox.Unloaded, if you're going to do it at all. – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 14:41
  • If you're actually using a ListBox or ListView instead of an ItemsControl, the Loaded and Unloaded events are a problem due to virtualization. I would recommend that you use conventional validation. – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 15:20
  • 1
    @EdPlunkett sure thing, `Validation.ClearInvalid` this is what technically does it, it removes all validation objects associated with that element. – Trevor Oct 24 '19 at 17:59
  • 1
    @EdPlunkett no you are not! I'm trying to wrap this answer up... and since my last few comments, I found a better way to remove them instead of taking the first one I find, I actually go after the ViewModel because the `CollectionChanged` event uses the VM... – Trevor Oct 24 '19 at 18:00
  • 1
    @Çöđěxěŕ Figure of speech! I even thought of prompting intellisense for `Validation.Clear...` at some point but got distracted by something. – 15ee8f99-57ff-4f92-890c-b56153 Oct 24 '19 at 18:01

1 Answers1

1

After a little time, I have a working solution that you could start with. As already mentioned by Ed, You're using the UI as a data structure, which is never a good idea. The MVVM way to do validation is IDataErrorInfo, this is true and really you should be implementing the IDataErrorInfo interface to handle these errors.

On another note, here's what I did to get it working. I am handling the CollectionChanged event for the ObservableCollection of your VM's. When the collection changes, you need to find the element that was actually being removed, if found, we can try and clear it's ValidationError object for that element itself.

Here is the class -

public partial class MainWindow : Window
    {
        public ObservableCollection<VM> Items { get; } = new ObservableCollection<VM> { new VM(), new VM(), new VM() };                             

        public MainWindow()
        {
            InitializeComponent();

            Items.CollectionChanged += Items_CollectionChanged;

            AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
            Title = ((ValidationErrorEventArgs)e).Action == ValidationErrorEventAction.Added ? "+" : "-"));

            DataContext = this;        
        }

        private void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) {
                foreach (TextBox tb in FindVisualChildren<TextBox>(this))
                {
                    if(tb.DataContext == e.OldItems[0])
                    {
                        Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty));
                        break;
                    }                    
                }
            }            
        }

        private void Button_Click(object sender, RoutedEventArgs e) => Items.RemoveAt(0);

        public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
        {
            if (depObj != null)
            {
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
                {
                    DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                    if (child != null && child is T)
                    {
                        yield return (T)child;
                    }

                    foreach (T childOfChild in FindVisualChildren<T>(child))
                    {
                        yield return childOfChild;
                    }
                }
            }
        }

    }
    public class VM
    { 
        public int Test { get; set; }

    }

The bread and butter to make this work is Validation.ClearInvalid(tb.GetBindingExpression(TextBox.TextProperty)); which actually removes all ValidationError objects from the BindingExpressionBase object which in this case is TextBox.TextProperty.

Note: There has been no error checking here, you may want to do that.

Trevor
  • 7,777
  • 6
  • 31
  • 50
  • As already mentioned I want to avoid the top-level view-model dealing with children, that would require to know their implementation details. Not all bindings are on `TextBox.Text`, not all collections are browsable, etc. – Sinatr Oct 25 '19 at 07:10
  • Your solution give me another idea worth to check: is it possible to get notified when something in visual tree is removed or when bindings are removed ? Ideally it should be a routed event, so I can subscribe on top-level and only run validation error check for those. Maybe add such event to a custom binding class? Hmm.. – Sinatr Oct 25 '19 at 07:29
  • 1
    @Sinatr You're building a mountain of increasingly exotic kludges to avoid a simple, straightforward solution. This is called the Rube Goldberg Hail Mary Sunk Cost Fallacy Anti-Pattern. – 15ee8f99-57ff-4f92-890c-b56153 Oct 25 '19 at 13:56