There are many different types of controls in the WPF window. More specifically: a questionnaire of 25 questions, in each of which there are 10-12 options reflected by RadioButtons or Checkboxes, plus 2-3 additional TextBox controls for each question.

Due to the huge amount of bool properties, they are not bound to separate bool, but to bool [] arrays. Textboxes, of course, are tied to ordinary string properties. For example (removed styles and coordinates from markup as irrelevant):

<RadioButton IsChecked="{Binding RelationTypeMap[0], ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"> <RadioButton.Content> <TextBlock Text="ΠΌΡƒΠΆ, ΠΆΠ΅Π½Π°" /> </RadioButton.Content> </RadioButton> // ... ΠΈ Ρ‚Π°ΠΊ Π΄Π°Π»Π΅Π΅ .... <RadioButton IsChecked="{Binding RelationTypeMap[10], Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"> <RadioButton.Content> <TextBlock Text="Π½Π΅ родствСнник" /> </RadioButton.Content> </RadioButton> // ... Π° Π²ΠΎΡ‚ TextBox <TextBox Text="{Binding RelationOther, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/> 

IMPORTANT: Under the terms of the task, all controls should be open. I can not prohibit entering additional fields depending on the state of the checkboxes.

It was necessary to create a system of validation, which will paint with red erroneous logical relations, of which the heap is possible. For example, if the tick "not relative" is ticked, then paint the RelationOther field with red if it is not empty.

There are also logical relationships between different issues. For example, if question 2 (two RadioButtons are β€œgender”) is β€œmale” (not female), then any filling in the following 4 questions is red (questions for women only).

I decided to use a validation system based on IDataErrorInfo, as a native for WPF. Markup that refers to a validation that "ignites" the validation event is visible above. And here is the ViewModel fragment:

 public class FormLViewModel : QuestionnairesBaseViewModel, IQuestionnaireViewModel, IDataErrorInfo { // ... #region Валидация public string this[string columnName] { get { string error = String.Empty; switch (columnName) { case "RelationOther": if (!string.IsNullOrEmpty(RelationOther) && RelationTypeMap.Any(x => x == true) && !RelationTypeMap[10]) error = "Ошибка! Π£ΠΊΠ°Π·Π°Π½ΠΈΠ΅ описания Π½Π΅ трСбуСтся"; break; case "MarriageFaceNum": if ((MarriageFaceNum ?? 0) > 0 && MarriageStateMap.Any(x => x == true) && !(MarriageStateMap[0] || MarriageStateMap[1])) error = "Ошибка! Π£ΠΊΠ°Π·Π°Π½ΠΈΠ΅ Π½ΠΎΠΌΠ΅Ρ€Π° супруга Π½Π΅ трСбуСтся"; break; case "ChildsNum": if ((ChildsNum ?? 0) > 0 && (SexMap[0] || FullOld < 15)) error = "Ошибка! ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π΄Π΅Ρ‚Π΅ΠΉ ΡƒΠΊΠ°Π·Ρ‹Π²Π°ΡŽΡ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΆΠ΅Π½Ρ‰ΠΈΠ½Ρ‹ ΠΎΡ‚ 15 Π»Π΅Ρ‚ ΠΈ ΡΡ‚Π°Ρ€ΡˆΠ΅"; break; case "ChildBirthDate": if ((ChildBirthDate ?? default) != default && (SexMap[0] || FullOld < 15) && (ChildsNum ?? 0) > 0) error = "Ошибка! Π”Π°Ρ‚Ρƒ роТдСния ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ Ρ€Π΅Π±Π΅Π½ΠΊΠ° ΡƒΠΊΠ°Π·Ρ‹Π²Π°ΡŽΡ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΆΠ΅Π½Ρ‰ΠΈΠ½Ρ‹ ΠΎΡ‚ 15 Π»Π΅Ρ‚ ΠΈ ΡΡ‚Π°Ρ€ΡˆΠ΅, ΠΈΠΌΠ΅ΡŽΡ‰ΠΈΠ΅ Π΄Π΅Ρ‚Π΅ΠΉ"; break; case "Nationality": if (NoNationality && !string.IsNullOrEmpty(Nationality)) error = "Ошибка! НС трСбуСтся ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π½Π°Ρ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ ΠΏΡ€ΠΈ ΠΎΡ‚ΠΊΠ°Π·Π΅ ΠΎΡ‚ ΠΎΡ‚Π²Π΅Ρ‚Π°"; break; case "MoneySourcesOther": if (!string.IsNullOrEmpty(MoneySourcesOther) && MoneySourceMap.Any(x => x == true) && !MoneySourceMap[11]) error = "Ошибка! НС трСбуСтся Π²Π²ΠΎΠ΄ΠΈΡ‚ΡŒ ΠΈΠ½ΠΎΠΉ источник Π΄ΠΎΡ…ΠΎΠ΄Π°"; break; case "JobPlacementRegion": if (JobPlacementMap[0] && !string.IsNullOrEmpty(JobPlacementRegion)) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ располоТСниС Π’Π°ΡˆΠ΅ΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"Π”Π°\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "JobPlacementTown": if (JobPlacementMap[0] && !string.IsNullOrEmpty(JobPlacementTown)) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ располоТСниС Π’Π°ΡˆΠ΅ΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"Π”Π°\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "JobPlacementForeign": if (JobPlacementMap[0] && !string.IsNullOrEmpty(JobPlacementForeign)) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ располоТСниС Π’Π°ΡˆΠ΅ΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"Π”Π°\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "JobPlacementDistrict": if (JobPlacementMap[0] && !string.IsNullOrEmpty(JobPlacementDistrict)) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ располоТСниС Π’Π°ΡˆΠ΅ΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"Π”Π°\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "JobSearchReasonOther": if (!string.IsNullOrEmpty(JobSearchReasonOther) && JobSearchReasonMap.Any(x => x == true) && !JobSearchReasonMap[9]) error = "Ошибка! Π£ΠΊΠ°Π·Π°Π½ΠΈΠ΅ ΠΈΠ½ΠΎΠΉ ΠΏΡ€ΠΈΡ‡ΠΈΠ½Ρ‹ Π½Π΅ трСбуСтся"; break; case "ResidenceFromYear": if (ResidenceFromBirth && (ResidenceFromYear ?? 0) > 0) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π³ΠΎΠ΄ прибытия, Ссли Π’Ρ‹ ΠΆΠΈΠ²Π΅Ρ‚Π΅ здСсь с роТдСния"; break; case "ResidenceFromMonth": if (ResidenceFromBirth && (ResidenceFromMonth ?? 0) > 0) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π³ΠΎΠ΄ ΠΈ мСсяц прибытия, Ссли Π’Ρ‹ ΠΆΠΈΠ²Π΅Ρ‚Π΅ здСсь с роТдСния"; break; case "PreviousResidence": if (ResidenceFromBirth && !string.IsNullOrEmpty(PreviousResidence)) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ ΠΏΡ€Π΅ΠΆΠ½Π΅Π΅ мСсто ΠΆΠΈΡ‚Π΅Π»ΡŒΡΡ‚Π²Π°, Ссли Π’Ρ‹ ΠΆΠΈΠ²Π΅Ρ‚Π΅ здСсь с роТдСния"; break; case "ForeignResidenceCountry": if (!string.IsNullOrEmpty(ForeignResidenceCountry) && ForeignResidenceMap.Any(x => x == true) && !ForeignResidenceMap[0]) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ страну проТивания, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"НСт\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "ReturnYear": if ((ReturnYear ?? 0) > 0 && ForeignResidenceMap.Any(x => x == true) && !ForeignResidenceMap[0]) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π³ΠΎΠ΄ возвращСния, Ссли ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ»ΠΈ \"НСт\" Π² вопросС Π²Ρ‹ΡˆΠ΅"; break; case "RegistrationPlaceOther": if (!string.IsNullOrEmpty(RegistrationPlaceOther) && RegistrationPlaceMap.Any(x => x == true) && !RegistrationPlaceMap[2]) error = "Ошибка! НС Π½ΡƒΠΆΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π½Π°ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π½ΠΈΠ΅"; break; } if (!string.IsNullOrEmpty(error)) _withErrors = true; return error; } } public string Error => string.Empty; // ... } 

PROBLEM: Any changes in controls such as TextBox, SpinEdit (DevExpress), etc., in short, that are not mapped to bool [] arrays, lead to a call to the indexer, in which I can analyze the current situation and swear on the field if necessary . However, any changes in the state of checkboxes (arrays bool []) do not trigger the event to fire, we do not get into the indexer, and nothing works.

As a result: if you first tick, and then fill in TextBoxes, validation occurs, but if you first fill in TextBoxes, and then create invalidations with ticks, no errors.

I have already been written here an example of how to solve a problem through a surrogate field, where each tick merges its value. I reproduced the example - it works. But in the example, all bool's fields are single. I tried to make them arrays, like mine, and the example stopped working.

Unfortunately, it is necessary to correct the validation quickly, crutches are allowed. Expanding all arrays into single fields is not an option. There will be more than 200. Too much for the properties of the ViewModel. Help me find a quick fix, please! You can even on crutches!

    2 answers 2

    Detailed response at the request of the participant.

    I had a binding to bool [] in XAML. When switching to ObservableCollection, the binding did not change at all. That is, I did not touch View at all!

    Here is the binding in XAML was and remains:

     <RadioButton IsChecked="{Binding RelationTypeMap[0]}"> <RadioButton.Content> <TextBlock Text="ΠΌΡƒΠΆ, ΠΆΠ΅Π½Π°" /> </RadioButton.Content> </RadioButton> // ... ΠΈ Ρ‚Π°ΠΊ Π΄Π°Π»Π΅Π΅ .... <RadioButton IsChecked="{Binding RelationTypeMap[10]}"> <RadioButton.Content> <TextBlock Text="Π½Π΅ родствСнник" /> </RadioButton.Content> </RadioButton> 

    Only earlier RelationTypeMap was bool [], and now it has become:

      ObservableCollection<bool> _relationTypeMap; public ObservableCollection<bool> RelationTypeMap { get => _relationTypeMap; set => Set("RelationTypeMap", ref _relationTypeMap, value); } 

    In the designer announced the event subscription:

      RelationTypeMap.CollectionChanged += RelationTypeMap_CollectionChanged; 

    In the event handler:

      private void RelationTypeMap_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnReplacePropertiesNotification(e, "RelationOther"); 

    In the inherited abstract class (the abstract class is optional; you can directly insert the code of this method into the handler, but it was more convenient for me):

      protected void OnReplacePropertiesNotification(NotifyCollectionChangedEventArgs e, params string[] properties) { if (e.Action == NotifyCollectionChangedAction.Replace) PropertiesNotification(properties); } protected void PropertiesNotification(params string[] properties) { foreach (string property in properties) RaisePropertyChanged(property); } 

    And the validation itself:

      #region Валидация public string this[string columnName] { get { string error = String.Empty; switch (columnName) { case "RelationOther": if (!string.IsNullOrEmpty(RelationOther) && RelationTypeMap.Any(x => x == true) && !RelationTypeMap[10]) error = "Ошибка! Π£ΠΊΠ°Π·Π°Π½ΠΈΠ΅ описания Π½Π΅ трСбуСтся"; break; // ΠΈ Ρ‚Π°ΠΊ Π΄Π°Π»Π΅Π΅, Π΄Ρ€ΡƒΠ³ΠΈΠ΅ поля... } return error; } } public string Error => string.Empty; 

    The goal was to validate the RelationOther field not only by changing the controls associated with this field, but also by changing the RadionButtons associated with RelationTypeMap, which are logically related to RelationOther. And this goal is fully achieved.

    Although, of course, I had to repeat the event subscription and handler for the remaining 24 collections of bool-s, to which other RadioButton groups are tied.

    • Well done. Just do not have to create for this another answer. It was enough to supplement his previous answer. - Bulson
    • @Bulson Ok, not yet accustomed. Deleted the previous answer, so that the eyes are not callous. - Andrey Kutasevich

    I do not know why you did not manage to remake an example for your version. There it was necessary to create a separate class for the radio button, plus recall the use of delegates in C #.

    work example

    Create a class that will correspond to one radio button

     public class RadioButtonViewModel : INotifyPropertyChanged { private readonly string _template; private readonly Func<string, bool> _checkEqualsTemplate; private readonly Action<string> _assigningValue; //ctor public RadioButtonViewModel(string template, Func<string, bool> checkEqualsTemplate, Action<string> assigningValue) { if (String.IsNullOrEmpty(template)) throw new ArgumentNullException(nameof(template)); _template = template; _checkEqualsTemplate = checkEqualsTemplate ?? throw new ArgumentNullException(nameof(checkEqualsTemplate)); _assigningValue = assigningValue ?? throw new ArgumentNullException(nameof(assigningValue)); } public bool RadioButtonValue { get => _checkEqualsTemplate(_template); set => _assigningValue(_template); } public void RaisePropertyChanged() { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RadioButtonValue))); } public event PropertyChangedEventHandler PropertyChanged; } 

    The view model will be such a surrogate property remains in place.

     public class MainViewModel : INotifyPropertyChanged, IDataErrorInfo { //ctor public MainViewModel() { SetRadioButtons(); AnimalType = "NotAnimal"; } private void SetRadioButtons() { RadioButtons = new List<RadioButtonViewModel> { new RadioButtonViewModel("NotAnimal", OnCheckRadioButton, OnAssignToRadioButton), new RadioButtonViewModel("Cow", OnCheckRadioButton, OnAssignToRadioButton), new RadioButtonViewModel("Dog", OnCheckRadioButton, OnAssignToRadioButton), new RadioButtonViewModel("Cat", OnCheckRadioButton, OnAssignToRadioButton), }; } //ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΡΡŽΡ‰ΠΈΠΉ private bool OnCheckRadioButton(string arg) { return AnimalType.Equals(arg); } //ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΡ€ΠΈΡΠ²Π°ΠΈΠ²Π°ΡŽΡ‰ΠΈΠΉ private void OnAssignToRadioButton(string obj) { AnimalType = obj; } //коллСкция для привязки ΠΊ Ρ€Π°Π΄ΠΈΠΎΠΊΠ½ΠΎΠΏΠΊΠ°ΠΌ public List<RadioButtonViewModel> RadioButtons { get; set; } //суррогатноС ΠΏΠΎΠ»Π΅ :) private string _AnimalType; public string AnimalType { get => _AnimalType; set { _AnimalType = value; //Π²ΠΎΠ·Π±ΡƒΠΆΠ΄Π°Π΅ΠΌ Ρƒ ΠΊΠ°ΠΆΠ΄ΠΎΠΉ событиС PropertyChanged foreach (var rb in RadioButtons) { rb.RaisePropertyChanged(); } //Π½Π΅ Π·Π°Π±Ρ‹Π²Π°Π΅ΠΌ ΠΎ Π·Π½Π°Ρ‡ΠΈΠΌΠΎΠΌ для нас свойствС OnPropertyChanged("Name"); } } private string _Name; public string Name { get => _Name; set { _Name = value; OnPropertyChanged(); } } //IDEI public string Error => String.Empty; public string this[string columnName] { get { //Ссли Π²Ρ‹Π±Ρ€Π°Π½Π° пСрвая Ρ€Π°Π΄ΠΈΠΎΠΊΠ½ΠΎΠΏΠΊΠ° - ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π½Π΅ Π½ΡƒΠΆΠ½Π°! if (RadioButtons[0].RadioButtonValue) return String.Empty; if (String.IsNullOrEmpty(Name) || Name.Trim().Length <= 3) { return "ΠšΠ»ΠΈΡ‡ΠΊΠ° Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ 4-Ρ… символов"; } return String.Empty; } } //INPC public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName]string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } 

    Very little had to change. Xaml such

     <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="2*" /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="20"> <RadioButton Content="НС ΠΆΠΈΠ²ΠΎΡ‚Π½ΠΎΠ΅" GroupName="Animal" IsChecked="{Binding RadioButtons[0].RadioButtonValue, Mode=TwoWay}"></RadioButton> <RadioButton Content="ΠšΠΎΡ€ΠΎΠ²Π°" GroupName="Animal" IsChecked="{Binding RadioButtons[1].RadioButtonValue, Mode=TwoWay}"></RadioButton> <RadioButton Content="Π‘ΠΎΠ±Π°ΠΊΠ°" GroupName="Animal" IsChecked="{Binding RadioButtons[2].RadioButtonValue, Mode=TwoWay}"></RadioButton> <RadioButton Content="Кошка" GroupName="Animal" IsChecked="{Binding RadioButtons[3].RadioButtonValue, Mode=TwoWay}"></RadioButton> </StackPanel> <StackPanel Grid.Row="1"> <TextBlock Text="ΠšΠ»ΠΈΡ‡ΠΊΠ°" Margin="100,0,0,0" /> <TextBox Width="200" Height="23" Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/> </StackPanel> </Grid> 

    The whole example can be downloaded here.

    • Thank you for your work! But I needed a much simpler solution that I found on my own. I give it as an answer. I probably confused you with the wrong question. But it was the experiments on your answer proposed earlier that helped me find this solution. Note - without there surrogate fields! - Andrey Kutasevich