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!
