Daniel Hilgarth
Daniel Hilgarth

Reputation: 174319

GroupBox in ContentControl - support for IDataErrorInfo implemented by the content bound to the ContentControl

I have a ViewModel that represents multiple options and implements IDataErrorInfo. This ViewModel is only valid if at least one of these options is selected. It is bound to a ContentControl. A DataTemplate is used to visualize the ViewModel as a GroupBox containing an ItemsControl. Another DataTemplate visualizes each option as a CheckBox.

What do I have to do, to make the ContentControl work together with IDataErrorInfo and check the validity when a check box is checked or unchecked?


Some code:

Binding:

<ContentControl Content="{Binding GeneralInvoiceTypes, ValidatesOnDataErrors=True}"
                Margin="0,0,5,0" />

Data templates:

<DataTemplate DataType="{x:Type ViewModels:MultipleOptionsViewModel}">
  <GroupBox Header="{Binding Title}">
    <ItemsControl ItemsSource="{Binding Options}" />
  </GroupBox>
</DataTemplate>
<DataTemplate DataType="{x:Type ViewModels:OptionViewModel}">
  <CheckBox IsChecked="{Binding IsChecked}"
            Content="{Binding Name}"
            Margin="6,3,3,0" />
</DataTemplate>

Style:

<Style TargetType="{x:Type ContentControl}">
  <Style.Triggers>
    <Trigger Property="Validation.HasError"
             Value="true">
      <Setter Property="ToolTip"
              Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
    </Trigger>
  </Style.Triggers>
  <Setter Property="Validation.ErrorTemplate">
    <Setter.Value>
      <ControlTemplate>
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="90*" />
            <ColumnDefinition Width="20" />
          </Grid.ColumnDefinitions>
          <Border BorderBrush="Red"
                  BorderThickness="1"
                  CornerRadius="2.75"
                  Grid.Column="0">
            <AdornedElementPlaceholder Grid.Column="0" />
          </Border>
          <TextBlock Foreground="Red"
                     Grid.Column="1"
                     Margin="0"
                     FontSize="12"
                     VerticalAlignment="Center"
                     HorizontalAlignment="Left"
                     x:Name="txtError">
            *
          </TextBlock>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Upvotes: 7

Views: 1074

Answers (3)

Fredrik Hedblad
Fredrik Hedblad

Reputation: 84657

What do I have to do, to make the ContentControl work together with IDataErrorInfo and check the validity when a check box is checked or unchecked?

Adding a bit to Rachels answer.

This problem would be easier to solve with Asynchronous data Validation but unfortunately, that isn't available before WPF 4.5 is released.

Content is binding to GeneralInvoiceTypes in MainViewModel. Since we can't do Asynchronous data Validation, then PropertyChanged has to be raised for GeneralInvoiceTypes for the validation to occur. This would work but I would take the approach that Rachel suggested and introduce another property called IsValid in MultipleOptionsViewModel

The binding to IsValid can be done from Tag (or an attached property) to GeneralInvoiceTypes.IsValid. We would also have to be notified in MultipleOptionsViewModel when IsChecked is changed in any of the Options. This can be done by using a command binding in the CheckBoxes for example.

So some changes along the following lines would be required.

I also uploaded a sample project with this implemented here: https://www.dropbox.com/s/fn8e4n4s68wj3vk/ContentControlValidationTest.zip?dl=0

ContentControl

<ContentControl Content="{Binding Path=GeneralInvoiceTypes}"
                Tag="{Binding Path=GeneralInvoiceTypes.IsValid,
                              ValidatesOnDataErrors=True}" />

OptionViewModel DataTemplate

<DataTemplate DataType="{x:Type ViewModels:OptionViewModel}">
    <CheckBox IsChecked="{Binding IsChecked}"
                Content="{Binding Name}"
                Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContentControl}},
                                Path=DataContext.IsValidCheckCommand}"
                Margin="6,3,3,0" />
</DataTemplate>

MultipleOptionsViewModel

private ICommand m_isValidCheckCommand;
public ICommand IsValidCheckCommand
{
    get
    {
        return m_isValidCheckCommand ??
            (m_isValidCheckCommand = new RelayCommand(param => IsValidCheck()));
    }
}

private void IsValidCheck()
{
    IsValid = CheckIsValid();
}

private bool CheckIsValid()
{
    foreach (OptionViewModel option in Options)
    {
        if (option.IsChecked == true)
        {
            return true;
        }
    }
    return false;
}

private bool m_isValid;
public bool IsValid
{
    get { return m_isValid; }
    set
    {
        m_isValid = value;
        OnPropertyChanged("IsValid");
    }
}

public string this[string columnName]
{
    get
    {
        if (columnName == "IsValid")
        {
            if (IsValid == false)
            {
                return "At least 1 Option must be selected";
            }
        }
        return string.Empty;
    }
}

Upvotes: 3

Daniel Hilgarth
Daniel Hilgarth

Reputation: 174319

Based on the answers given, I solved it like this:

  1. Change the DataTemplate of the MultipleOptionsViewModel to bind the Options property with ValidatesOnDataErrors=True:

    <DataTemplate DataType="{x:Type ViewModels:MultipleOptionsViewModel}">
        <GroupBox Header="{Binding Title}">
            <ItemsControl ItemsSource="{Binding Options,
                                        ValidatesOnDataErrors=True}"/>
        </GroupBox>
    </DataTemplate>
    
  2. Change the error style to target ItemsControl instead of ContentControl.

  3. Make sure that MultipleOptionsViewModel raises a PropertyChanged for "Options" when one of the child options is checked or unchecked
  4. Make sure that MultipleOptionsViewModel reacts to the column "Options" in its implementation if IDataErrorInfo.Item, i.e. in its indexer.

This solution has the benefit, that it uses the default behavior of IDataErrorInfo, i.e. the consumer of this ViewModel doesn't need handle it any special.

I know that this solution is not 100% equivalent to that I asked about in the question - the error template is now displayed inside the group box instead of around it, but that's something I can live with.

Upvotes: 0

Rachel
Rachel

Reputation: 132558

Does the class containing theGeneralInvoiceTypes property implement IDataErrorInfo?

Setting ValidatesOnDataErrors=True will show validation errors for the DataContext that contains the bound property, so in this case it is validing ParentViewModel.GetValidationError("GeneralInvoiceTypes"), and not GeneralInvoiceTypes.GetValidationError()

In this case, you can either add IDataErrorInfo to your ParentViewModel and validate the GeneralInvoiceTypes property by returning it's Validation Error like this:

public string GetValidationError(string propertyName)
{
    if (propertyName == "GeneralInvoiceTypes")
        return GeneralInvoiceTypes.GetValidationError();

    return null;
}

or you can create an IsValid property on GeneralInvoiceTypes which returns GetValidationError() == null and base your validation Trigger off of {Binding IsValid} rather then Validation.HasError

Upvotes: 1

Related Questions