简体   繁体   中英

Binding nested validation rules on nested User Controls

This is my first question here on SO...I have been a ready for a long time and never needed to ask for help because I usually find what I need, but I am having a hard time with this one...

I am working on a tools suite in WPF. I created a few User Controls as follow:

  1. LabelTextBox (Label on the left and TextBox on the right)
  2. LabelTextBoxToggle ( LabelTextBox on the left and Checkbox on the right)
  3. LabelTextBoxBrowseFile ( LabelTextBox on the left and Browse File Button on the right)

I use Dependency Properties to bind all the properties I need and they all work fine. The problem I ran into recently is getting ValidationRules to work correctly on the base TextBox I use in LabelTextBox when those rules are applied to the LabelTextBoxToggle and LabelTextBoxBrowseFile UserControls, since I have to bind 2 levels down in order to update controls in LabelTextBox . I can get the Validation Rule to run, but I can't get the TextBox control to update its background color accordingly when errors are found, like I do when LabelTextBox isn't nested within another User Control.

So, here's my code below:

Style used for TextBox:

<!-- TextBox Default Style, Supports Validation Rules -->
<Style TargetType="{x:Type TextBox}">
    <Setter Property="Background" Value="{StaticResource TextBoxBGDefault}" />
    <Style.Triggers>
        <Trigger Property="IsKeyboardFocused" Value="True">
            <Setter Property="Background" Value="{StaticResource TextBoxBGHasFocus}" />
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="Background" Value="{StaticResource TextBoxBGHasFocus}" />
        </Trigger>
        <DataTrigger Binding="{Binding Path=(Validation.HasError)}" Value="true">
            <Setter Property="Background" Value="{StaticResource TextBoxBGHasError}" />
            <Setter Property="BorderBrush" Value="Firebrick" />
            <Setter Property="BorderThickness" Value="1.5" />
            <Setter Property="ToolTipService.InitialShowDelay" Value="2" />
            <Setter Property="ToolTip" Value="{Binding Path=(Validation.Errors)[0].ErrorContent}" />
        </DataTrigger>

    </Style.Triggers>
</Style>

LabelTextBox.xaml:

<Grid x:Name="LayoutRoot" DataContext="{Binding ElementName=ControlRoot, Mode=OneWay, ValidatesOnDataErrors=True}">
    <Grid.RowDefinitions>
        <RowDefinition Height="24" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Label
        x:Name="NameLabel"
        Width="{Binding Path=LabelWidth, Converter={StaticResource WidthToAutoConverter}}"
        Margin="0"
        HorizontalAlignment="{Binding Path=HorizontalContentAlignment}"
        HorizontalContentAlignment="{Binding Path=LabelHAlign, Converter={StaticResource valueToStringConverter}}"
        VerticalContentAlignment="Center"
        Content="{Binding Path=LabelContent}"
        Padding="10,2,5,2" />
    <TextBox
        x:Name="ValueTextBox"
        Grid.Column="1"
        KeyDown="TextBox_KeyDown_Enter"
        Padding="5,0"
        Text="{Binding TextBoxContent, Mode=TwoWay}"
        TextChanged="TextBox_TextChanged" VerticalContentAlignment="Center" Height="22" VerticalAlignment="Center" />
    <TextBlock
        x:Name="ErrorMsgTextBlock"
        Grid.Row="1"
        Grid.Column="1"
        Margin="0"
        HorizontalAlignment="Left"
        VerticalAlignment="Top"
        Style="{DynamicResource ValidationErrorLabel}"
        Text="{Binding Path=(Validation.Errors)[0].ErrorContent, ElementName=ControlRoot}"
        Visibility="{Binding Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilityConverter}, ElementName=ControlRoot, Mode=OneWay}" TextWrapping="Wrap" />
</Grid>

LabelTextBoxBaseClass:

#region TextBox Dependency Properties
public string TextBoxContent
{
    get { return (string)GetValue( TextBoxContentProperty ); }
    set { SetValue( TextBoxContentProperty, value ); }
}
public static readonly DependencyProperty TextBoxContentProperty =
    DependencyProperty.Register( "TextBoxContent"
    , typeof( string )
    , typeof( LabelTextBoxBaseClass ), new PropertyMetadata( "" )
);

LabelTextBoxToggle.xaml:

<!-- This is the nested UserControl -->
<local:LabelTextBox
    x:Name="LTBControl"
    Margin="0"
    VerticalContentAlignment="Center"
    IsEnabled="{Binding Path=IsChecked, ElementName=ToggleCheckBox}"
    LabelContent="{Binding Path=LabelContent}"
    LabelHAlign="{Binding Path=LabelHAlign}"
    LabelWidth="{Binding Path=LabelWidth}"
    RaiseEnterKeyDownEvent="{Binding RaiseEnterKeyDownEvent, Mode=TwoWay}"
    RaiseTextChangedEvent="{Binding RaiseTextChangedEvent, Mode=TwoWay}"
    TextBoxContent="{Binding Path=TextBoxContent, Mode=TwoWay}" />
<CheckBox
    x:Name="ToggleCheckBox"
    Grid.Column="1"
    Margin="5,0"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    HorizontalContentAlignment="Center"
    VerticalContentAlignment="Center"
    Click="ToggleCheckBox_Click"
    IsChecked="{Binding CheckBoxChecked, Mode=TwoWay}" />

MaterialBuilder.xaml:

<UserControl.Resources>
    <BindingGroup x:Key="SRBindingGroup" Name="PropertiesBindingGroup">
        <BindingGroup.ValidationRules>
            <local:AddMaterialRule ValidationStep="ConvertedProposedValue" />
        </BindingGroup.ValidationRules>
    </BindingGroup>
    <srvalidators:StringNullOrEmptyValidationRule x:Key="stringNullOrEmptyValidationRule" ErrorMessage="Custom Dir cannot be null!" />
    <srconverters:ListToStringConverter x:Key="ListToStringConverter" />
    <srconverters:ListToStringConverter x:Key="listToStringConverter" />
    <sys:String x:Key="newLine">\n</sys:String>
</UserControl.Resources>

<StackPanel x:Name="spSetup">

    <!-- This contains a nested UserControl (LabelTextBox), and I can't get its TextBox background to change color, I just get the red border around the whole control on Validation Errors. -->
    <srcontrols:LabelTextBoxBrowseFile
        x:Name="ltbMaterialBlueprint"
        Height="Auto"
        Margin="0,5"
        LabelContent="Material Blueprint:"
        LabelWidth="120"
        LostFocus="ltbMaterialBlueprint_UpdateUI"
        OnButtonClick="ltbMaterialBlueprint_UpdateUI"
        OnTextBoxEnterKeyDown="ltbMaterialBlueprint_UpdateUI"
        TextBoxContent="{Binding MaterialBlueprintFilePath, Mode=TwoWay}">
        <srcontrols:LabelTextBoxBrowseFile.TextBoxContent>
            <Binding
                Mode="TwoWay"
                Path="CustomDirName"
                UpdateSourceTrigger="PropertyChanged"
                ValidatesOnDataErrors="True">
                <Binding.ValidationRules>
                    <srvalidators:StringNullOrEmptyValidationRule ErrorMessage="Custom Dir cannot be empty!" />
                </Binding.ValidationRules>
            </Binding>
        </srcontrols:LabelTextBoxBrowseFile.TextBoxContent>
    </srcontrols:LabelTextBoxBrowseFile>

    <!-- Here I use the base LabelTextBox control by itself and everything works as intended. The TextBox's background color changes to red on Validation Errors. -->
    <srcontrols:LabelTextBox
        x:Name="ltbMaterialName"
        Margin="0,5,10,5"
        LabelContent="Name:"
        LabelWidth="60"
        OnTextBoxTextChange="ltbMaterialName_Validate"
        RaiseEnterKeyDownEvent="True"
        RaiseTextChangedEvent="True">
        <!--  Set-up the TextBox Content to use the ValidationRule by passing this GroupBox's BindingGroup resource as a parameter  -->
        <srcontrols:LabelTextBox.TextBoxContent>
            <Binding
                Mode="TwoWay"
                Path="MaterialName"
                UpdateSourceTrigger="Explicit"
                ValidatesOnDataErrors="True">
                <Binding.ValidationRules>
                    <local:AddMaterialRule
                    BGroup="{StaticResource SRBindingGroup}"
                    CheckForDuplicates="True"
                    CheckForEmptyName="True"
                    IsMaterialName="True"
                    ValidationStep="ConvertedProposedValue" />
                </Binding.ValidationRules>
            </Binding>
        </srcontrols:LabelTextBox.TextBoxContent>
    </srcontrols:LabelTextBox>
</StackPanel>

I know it's probably a DataContext issue, but unlike the other controls and dependency properties, I cannot figure out how to make the base UserControl ui elements update their look when Validation Errors are found. Here's some images of what I mean:

Working TextBox (LabelTextBox control used here):

Working TextBox Example

Broken TextBox (LabelTextBoxToggle control used here, with nested LabelTextBox):

Broken TextBox (nested in UserControl)

Any help or suggestion is very welcomed of course! Thanks for your time!

Your problem is similar to mine. I've also created custom control containing text block (as label) and text box (as input). The goal is to have universal control for data input with simple label. The problem was validation. I've also managed easily to bind and validate data, but displaying errors with template on specified textbox that was inside my control... that was the issue and if I understand correctly you have the same problem. So my solution is:

<UserControl x:Class="CapMachina.Common.Controls.FormField_UC" x:Name="FormFieldCtrl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:CapMachina.Common.Controls"
         xmlns:Converters="clr-namespace:CapMachina.Common.Converters"
         xmlns:metro="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">

<UserControl.Resources>
    <Converters:ConditionalValueConverter x:Key="conditionalValueConverter" />
    <Converters:NullableObjectToVisibilityConverter x:Key="nullableObjectToVisibilityConverter" />
  </UserControl.Resources>
  <StackPanel>
    <TextBlock FontWeight="Bold" Text="{Binding Header, ElementName=FormFieldCtrl}" Margin="1" />
    <TextBox x:Name="MainTxtBx" metro:TextBoxHelper.Watermark="{Binding WaterMarkText, ElementName=FormFieldCtrl}" TextWrapping="Wrap"
             Text="{Binding Text, ElementName=FormFieldCtrl, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}" 
             Margin="1" IsReadOnly="{Binding IsReadOnly, ElementName=FormFieldCtrl}" TextChanged="MainTxtBx_TextChanged" Loaded="MainTxtBx_Loaded">
      <TextBox.Style>
        <MultiBinding Converter="{StaticResource conditionalValueConverter}">
          <Binding Path="IsReadOnly" ElementName="FormFieldCtrl" />
          <Binding Path="ReadOnlyStyle" ElementName="FormFieldCtrl" />
          <Binding Path="DefaultStyle" ElementName="FormFieldCtrl" />
        </MultiBinding>
      </TextBox.Style>
    </TextBox>
  </StackPanel>
</UserControl>

And code behind:

    using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace CapMachina.Common.Controls
{
  public partial class FormField_UC : UserControl
  {
public string Header
{
  get { return (string)GetValue(HeaderProperty); }
  set { SetValue(HeaderProperty, value); }
}

public static readonly DependencyProperty HeaderProperty =
    DependencyProperty.Register("Header", typeof(string), typeof(FormField_UC));

public string Text
{
  get { return (string)GetValue(TextProperty); }
  set { SetValue(TextProperty, value); }
}

public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register("Text", typeof(string), typeof(FormField_UC));

public string WaterMarkText
{
  get { return (string)GetValue(WaterMarkTextProperty); }
  set { SetValue(WaterMarkTextProperty, value); }
}

public static readonly DependencyProperty WaterMarkTextProperty =
    DependencyProperty.Register("WaterMarkText", typeof(string), typeof(FormField_UC));

public bool IsReadOnly
{
  get { return (bool)GetValue(IsReadOnlyProperty); }
  set { SetValue(IsReadOnlyProperty, value); }
}

public static readonly DependencyProperty IsReadOnlyProperty =
    DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(FormField_UC), new PropertyMetadata(true));

public Style ReadOnlyStyle { get; set; }
public Style DefaultStyle { get; set; }

public FormField_UC()
{
  ReadOnlyStyle = Application.Current.FindResource("ReadOnlyTextBox") as Style;
  DefaultStyle = Application.Current.FindResource("DefaultTextBox") as Style;
  InitializeComponent();
}

private void MainTxtBx_TextChanged(object sender, TextChangedEventArgs e)
{
  if (string.IsNullOrEmpty(MainTxtBx.Text) && IsReadOnly)
    Visibility = Visibility.Collapsed;
  else
    Visibility = Visibility.Visible;
}

private void MainTxtBx_Loaded(object sender, RoutedEventArgs e)
{
  BindingExpression mainTxtBxBinding = BindingOperations.GetBindingExpression(MainTxtBx, TextBox.TextProperty);
  BindingExpression textBinding = BindingOperations.GetBindingExpression(this, TextProperty);

  if (textBinding != null && mainTxtBxBinding != null && textBinding.ParentBinding != null && textBinding.ParentBinding.ValidationRules.Count > 0 && mainTxtBxBinding.ParentBinding.ValidationRules.Count < 1)
  {
    foreach (ValidationRule vRule in textBinding.ParentBinding.ValidationRules)
      mainTxtBxBinding.ParentBinding.ValidationRules.Add(vRule);
  }
    }
  }
}

Usage:

<Controls:FormField_UC Header="First name" IsReadOnly="False" HorizontalAlignment="Left" VerticalAlignment="Top">
  <Controls:FormField_UC.Text>
    <Binding Path="Person.FirstName" Mode="TwoWay">
      <Binding.ValidationRules>
        <VDRules:NamesValidationRule InventoryPattern="{StaticResource NamesRegex}">
          <VDRules:NamesValidationRule.Attributes>
            <Validation:ValidationAttributes IsRequired="True" />
          </VDRules:NamesValidationRule.Attributes>
        </VDRules:NamesValidationRule>
      </Binding.ValidationRules>
    </Binding>
  </Controls:FormField_UC.Text>
</Controls:FormField_UC>

What i did was copy validation rules to nested text box after all bindings were created. You cannot modify binding after use, but you can add validation rules to it :)

It is very important to set certain properties inside custom control like:

<UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, ValidatesOnExceptions=True>

cos you cannot set them afterwards. So setting them in usage line is not needed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM