文本框验证:红色边框并不总是出现在无效结果上

发布于 2024-12-07 16:05:25 字数 427 浏览 0 评论 0原文

我有一个绑定到需要值的属性的文本框,即:

 [Required(ErrorMessage = "required value")]
 public string SomeText
 {
     //get set...
 }

在我的 XAML 中,我对文本框进行了以下设置:

 UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, ValidatesOnExceptions=true

正如预期的那样,当文本框中没有值时,会出现红色边框,但是当我选择不同的选项卡,然后返回到无效结果的页面,红色边框不再出现。仅当我输入有效结果然后将其删除时,它才会重新出现。

我该如何调试这个?如何找出导致红色边框出现的事件?

I have a textbox that is bound to a property that requires a value, ie:

 [Required(ErrorMessage = "required value")]
 public string SomeText
 {
     //get set...
 }

And in my XAML, I have the following setup for my textbox:

 UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, ValidatesOnExceptions=true

As expected, the red border appears when there is no value in the textbox, however when I select a different tab and then go back to the page with the invalid results, the red border no longer appears. It only reappears if I enter a valid result and then erase it.

How can I debug this? How can I find out what event causes the red border to appear?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

一城柳絮吹成雪 2024-12-14 16:05:25

在 WPF 中,当选项卡上的项目从可视化树中卸载时,它们被标记为无效的事实就会丢失。基本上,当发生验证错误时,UI 会响应验证堆栈中的事件并将该项目标记为无效。当项目返回可视化树时,不会重新评估此标记,除非也重新评估绑定(如果用户单击选项卡项目,则通常不会重新评估)。

在某处定义这样的函数(我将其与其他一些东西一起放在静态 ValidationHelper 类中):

public static void ReMarkInvalid( DependencyObject obj )
{
    if( Validation.GetHasError( obj ) ) {
        List<ValidationError> errors = new List<ValidationError>( Validation.GetErrors( obj ) );
        foreach( ValidationError error in errors ) {
            Validation.ClearInvalid((BindingExpressionBase)error.BindingInError);
            Validation.MarkInvalid((BindingExpressionBase)error.BindingInError, error);
        }
    }

    for( int i = 0; i < VisualTreeHelper.GetChildrenCount( obj ); i++ ) {
        ReMarkInvalid( VisualTreeHelper.GetChild( obj, i ) );
    }
}

我认为您可以在 TabControl 的 Selected 事件中调用此函数,它应该具有所需的效果。例如:

private void TabControl_Selected(...) 
{
    ReMarkInvalid( tabControl );
}

如果这不起作用,您可能需要以较低的调度程序优先级执行此操作,以确保可视化树首先完成加载。这看起来像是将 ReMarkInvalid... 替换为:

Dispatcher.BeginInvoke( new Action( delegate()
{
    ReMarkInvalid( tabControl );
} ), DispatcherPriority.Render );

In WPF when items on the tab get unloaded from the visual tree the fact that they were marked invalid is lost. Basically, when a validation error happens the UI responds to an event in the validation stack and marks the item invalid. This marking doesn't get re-evaluated when the item comes back into the visual tree unless the binding is also re-evaluated (which it usually isn't if the user clicks on a tab item).

Define a function like this somewhere (I put it in a static ValidationHelper class along with some other things):

public static void ReMarkInvalid( DependencyObject obj )
{
    if( Validation.GetHasError( obj ) ) {
        List<ValidationError> errors = new List<ValidationError>( Validation.GetErrors( obj ) );
        foreach( ValidationError error in errors ) {
            Validation.ClearInvalid((BindingExpressionBase)error.BindingInError);
            Validation.MarkInvalid((BindingExpressionBase)error.BindingInError, error);
        }
    }

    for( int i = 0; i < VisualTreeHelper.GetChildrenCount( obj ); i++ ) {
        ReMarkInvalid( VisualTreeHelper.GetChild( obj, i ) );
    }
}

I think you can call this function in the TabControl's Selected event and it should have the desired effect. E.g.:

private void TabControl_Selected(...) 
{
    ReMarkInvalid( tabControl );
}

If that doesn't work you may need to do this at a lower Dispatcher priority to make sure the visual tree has finished loading first. Which would look like replacing ReMarkInvalid... with:

Dispatcher.BeginInvoke( new Action( delegate()
{
    ReMarkInvalid( tabControl );
} ), DispatcherPriority.Render );
鱼窥荷 2024-12-14 16:05:25

您可以简单地将选项卡的内容放入 AdornerDecorator 标记中:

<TabControl>
    <TabItem>

        <AdornerDecorator>

            <ContentControl Content="{Binding TabItemViewModel}" />
            <Grid>
                <!-- Other Stuff -->
            </Grid>

        </AdornerDecorator>

    </TabItem>
</TabControl>

更新:

AdornerDecorator 不会在不可见的控件(在未选择的选项卡中)上呈现边框。一旦边框已经渲染,它只会保留跨选项卡的边框。

然而,达纳·卡特赖特上面放置的代码工作正常。您只需在 MarkInvalid 之前放置一个 ClearInvalid 即可,正如lost_bits1110所指出的:

Validation.ClearInvalid((BindingExpressionBase)error.BindingInError);
Validation.MarkInvalid((BindingExpressionBase)error.BindingInError, error);

You can simply put the contents of your tab inside AdornerDecorator tags:

<TabControl>
    <TabItem>

        <AdornerDecorator>

            <ContentControl Content="{Binding TabItemViewModel}" />
            <Grid>
                <!-- Other Stuff -->
            </Grid>

        </AdornerDecorator>

    </TabItem>
</TabControl>

Update:

AdornerDecorator doesn't render borders on controls that are invisible (in unselected tabs). It just preserves borders across tabs once the borders have already been rendered.

However, the code put above by Dana Cartwright works fine. You just have to put a ClearInvalid before MarkInvalid as pointed out by lost_bits1110:

Validation.ClearInvalid((BindingExpressionBase)error.BindingInError);
Validation.MarkInvalid((BindingExpressionBase)error.BindingInError, error);
爱本泡沫多脆弱 2024-12-14 16:05:25

我遇到了一个特别麻烦的情况,@dana-cartwright 的出色答案没有效果。

我发现如果完全重置错误绑定,我能够获得所需的行为:

private static void ResetBindingsWithError(DependencyObject obj)
{
    if (Validation.GetHasError(obj))
    {
        // Loops over each of the dependency object's bindings that have errors
        foreach (BindingExpression bindingExpression in
                 Validation.GetErrors(obj)
                           .Select(validationError => validationError.BindingInError)
                           .OfType<BindingExpression>()
                           .ToArray())
        {
            // Reset the binding
            Binding binding = bindingExpression.ParentBinding;
            DependencyProperty targetProperty = bindingExpression.TargetProperty;

            if (bindingExpression.TargetProperty == null)
            {
                // Clearing one binding, may affect others that result in a TargetProperty of null
                continue;
            }

            BindingOperations.ClearBinding(obj, targetProperty);
            BindingOperations.SetBinding(obj, targetProperty, binding);
        }
    }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        ResetBindingsWithError(VisualTreeHelper.GetChild(obj, i));
    }
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (sender is not TabControl tabControl)
    {
        throw new InvalidOperationException("Event should only fire against TabControl");
    }

    // `sender` will always be the TabControl the event was registered against, but
    // `e.OriginalSource` will be the Selector that produced the event. Because this is a
    // bubbling event, we only want to handle the event if the sender is our top-level
    // TabControl.
    if ((tabControl == e.OriginalSource) &&
        (tabControl.SelectedContent is DependencyObject tabControlContent))
    {
        Dispatcher.BeginInvoke(() => ResetBindingsWithError(tabControlContent));
    }
}

I have a particularly troublesome situation where @dana-cartwright's excellent answer is not having an effect.

I've found that I'm able to get the desired behavior if I reset the binding in error completely:

private static void ResetBindingsWithError(DependencyObject obj)
{
    if (Validation.GetHasError(obj))
    {
        // Loops over each of the dependency object's bindings that have errors
        foreach (BindingExpression bindingExpression in
                 Validation.GetErrors(obj)
                           .Select(validationError => validationError.BindingInError)
                           .OfType<BindingExpression>()
                           .ToArray())
        {
            // Reset the binding
            Binding binding = bindingExpression.ParentBinding;
            DependencyProperty targetProperty = bindingExpression.TargetProperty;

            if (bindingExpression.TargetProperty == null)
            {
                // Clearing one binding, may affect others that result in a TargetProperty of null
                continue;
            }

            BindingOperations.ClearBinding(obj, targetProperty);
            BindingOperations.SetBinding(obj, targetProperty, binding);
        }
    }

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        ResetBindingsWithError(VisualTreeHelper.GetChild(obj, i));
    }
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (sender is not TabControl tabControl)
    {
        throw new InvalidOperationException("Event should only fire against TabControl");
    }

    // `sender` will always be the TabControl the event was registered against, but
    // `e.OriginalSource` will be the Selector that produced the event. Because this is a
    // bubbling event, we only want to handle the event if the sender is our top-level
    // TabControl.
    if ((tabControl == e.OriginalSource) &&
        (tabControl.SelectedContent is DependencyObject tabControlContent))
    {
        Dispatcher.BeginInvoke(() => ResetBindingsWithError(tabControlContent));
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文