В окне WPF имеется множество контролов разных типов в перемешку. Конкретнее: анкета из 25 вопросов, в каждом из которых 10-12 вариантов, отраженных RadioButton'ами или CheckBox'ами, плюс 2-3 дополнительных контрола типа TextBox, к каждому вопросу.
Из-за огромного количества bool'евых свойств, они binding-ны не на отдельные bool, а на массивы bool[]. TextBox'ы, разумеется, привязаны к обычным строковым свойствам. Например (удалил из разметки стили и координаты, как несущественное):
<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}"/>
ВАЖНО: По условиям задачи, все контролы должны быть открыты. Я не могу запрещать ввод в дополнительные поля в зависимости от состояния галочек.
Потребовалось создать систему валидации, которая будет красить красным ошибочные логические соотношения, которых возможно туева куча. Например, если стоит галочка "не родственник", то красим красным поле RelationOther, если оно не пустое.
Также существуют логические соотношения разных вопросов между собой. Например, если в вопросе 2 (два RadioButton'а - "пол") выбрано "мужской" (не женский), то любое заполнение следующих 4 вопросов - красное (вопросы только для женщин).
Решил использовать систему валидации на основе IDataErrorInfo, как "родную" для WPF. Разметка, ссылающаяся на валидацию, которая "поджигает" событие валидации, видна выше. А вот фрагмент ViewModel:
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;
// ...
}
ПРОБЛЕМА: Любые изменения в таких контролах, как TextBox, SpinEdit (DevExpress) и т.д., короче - которые маппятся не на массивы bool[], приводят к вызову индексатора, в котором я могу анализировать текущую ситуацию и ругаться на поле при необходимости. Однако, любые изменения состояния "галочек" (массивов bool[]) не приводят к поджиганию события, в индексатор мы не попадаем, и ничего не работает.
В итоге: если сначала выставить галочки, а потом заполнять TextBox'ы, валидация происходит, но если сначала заполнить TextBox'ы, а потом галочками создать невалидность - никаких ошибок.
Мне уже писали здесь пример, как решить проблему через суррогатное поле, куда каждая галочка сливает свое значение. Я пример воспроизвел - работает. Но в примере все bool'евы поля одиночные. Попытался сделать их массивами, как у меня, и пример перестал работать.
К сожалению, нужно исправить валидацию быстро, костыли допускаются. Разворачивать все массивы в одиночные поля - не вариант. Их будет более 200. Многовато для свойств ViewModel. Помогите найти быстрое решение, пожалуйста! Можно даже на костылях!
Развернутый ответ по просьбе участника.
У меня в XAML была привязка к bool[]. При переходе на ObservableCollection привязка вообще никак не поменялась. То есть, View я вообще не трогал!
Вот такая привязка в XAML была и осталась:
<RadioButton IsChecked="{Binding RelationTypeMap[0]}">
<RadioButton.Content>
<TextBlock Text="муж, жена" />
</RadioButton.Content>
</RadioButton>
// ... и так далее ....
<RadioButton IsChecked="{Binding RelationTypeMap[10]}">
<RadioButton.Content>
<TextBlock Text="не родственник" />
</RadioButton.Content>
</RadioButton>
Только ранее RelationTypeMap была bool[], а теперь стала:
ObservableCollection<bool> _relationTypeMap;
public ObservableCollection<bool> RelationTypeMap
{
get => _relationTypeMap;
set => Set("RelationTypeMap", ref _relationTypeMap, value);
}
В конструкторе объявил подписку на событие:
RelationTypeMap.CollectionChanged += RelationTypeMap_CollectionChanged;
В обработчике события:
private void RelationTypeMap_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
=> OnReplacePropertiesNotification(e, "RelationOther");
В унаследованном абстрактном классе (абстрактный класс не обязателен; можно код этого метода прямо вставить в обработчик, но мне так было удобнее):
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);
}
И сама валидация:
#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;
Цель была в том, чтобы валидация поля RelationOther срабатывала не только по изменению контрола, связанного с этим полем, но и по изменению RadionButton'ов, привязанных к RelationTypeMap, которые логически связаны с RelationOther. И эта цель полностью достигнута.
Хотя, разумеется, пришлось повторить подписку на событие и обработчик для остальных 24 коллекций bool-ов, на которые привязаны другие группы RadioButton'ов.
Не знаю почему у вас не получилось переделать пример под свой вариант. Там всего то надо было создать отдельный класс для радиокнопки, плюс вспомнить о применении делегатов в C#.
Создадим такой класс, который будет соответствовать одной радиокнопке
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;
}
Вьюмодель будет такой, суррогатное свойство осталось на месте
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));
}
}
Совсем немного надо было изменить. Xaml такой
<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>
Весь пример можно качнуть здесь
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
Есть стандартный код:
Есть файл кастомных конфигураций для нескольких клиентов, которые я держу в памяти через Single TonДолго все работало без проблем но тут внезапно...
Код который у меня, ошибка на последней строчке