Рассмотрим частую задачу в разработке на 1С: необходимо ограничить варианты выбора в одном поле в зависимости от того, что указано в другом. Классический пример — форма договора контрагента. У нас есть владелец договора (контрагент) и вид договора. Задача — сделать так, чтобы при выборе контрагента с типом "Клиент" пользователю были доступны для выбора только договоры с видом "С клиентом", а другие варианты были скрыты.
Начинающие разработчики часто пытаются решить эту проблему "в лоб" — отлавливая событие изменения вида договора и принудительно исправляя его, если он не соответствует типу контрагента. Это плохой подход с точки зрения пользовательского опыта: программа сначала дает выбрать неверный вариант, а потом его исправляет. Правильный путь — изначально не давать пользователю возможности выбрать некорректное значение, то есть динамически фильтровать список выбора.
Проанализируем ошибки в первоначальном коде и разберем правильные, стандартные способы решения этой задачи.
В теме на форуме автор попытался использовать событие ПриИзменении для реквизита ВидДоговора. Давайте посмотрим на его код и разберем, почему он не работает и почему сам подход неоптимален.
Первая попытка на клиенте:
&НаКлиенте
Процедура ВидДоговораПриИзменении(Элемент)
Если Объект.Владелец = ПредопределенноеЗначение("Перечисление.ТипыКонтрагентов.Клиент") Тогда
Объект.ВидДоговора = ("Перечисление.ВидыДоговоров.СКлиентом");
КонецЕсли;
КонецПроцедуры
Здесь сразу несколько ошибок:
Объект.Владелец — это ссылка на элемент справочника "Контрагенты". Мы не можем напрямую сравнивать ссылку на справочник со значением перечисления ТипыКонтрагентов. Чтобы получить тип контрагента, нужно обратиться к его реквизиту, например, Объект.Владелец.ТипКонтрагента.("Перечисление.ВидыДоговоров.СКлиентом") присваивает реквизиту строку, а не значение перечисления. Правильно было бы использовать ПредопределенноеЗначение().Вторая попытка на сервере:
&НаСервере
Процедура ПриИзмененииВидДоговораНаСервере()
Если Объект.Владелец.ТипКонтрагента = ПредопределенноеЗначение("Перечисление.ТипыКонтрагентов.Клиент") Тогда
Объект.ВидДоговора = ПредопределенноеЗначение("Перечисление.ВидыДоговоров.СКлиентом");
КонецЕсли;
КонецПроцедуры
Здесь тоже есть ошибки, на которые справедливо указали на форуме:
ПредопределенноеЗначение() работает только на клиенте. На сервере для обращения к предопределенным значениям перечислений используется прямой синтаксис: Перечисления.ИмяПеречисления.ИмяЗначения. В нашем случае это было бы Перечисления.ТипыКонтрагентов.Клиент и Перечисления.ВидыДоговоров.СКлиентом.ПриИзменении.Теперь, когда мы разобрали ошибки, перейдем к правильным методам решения.
Это самый простой, быстрый и предпочтительный способ, если структура метаданных это позволяет. Он не требует написания кода и настраивается прямо в конфигураторе. Идея в том, чтобы "связать" поле для выбора с другим полем на форме.
Когда этот способ подойдет? Он идеально работает, если у значения, которое мы выбираем (в нашем случае это ВидДоговора, являющийся перечислением), есть реквизит, который можно сопоставить с полем-источником (Владелец или его реквизиты). В нашем конкретном случае вид договора — это перечисление, у которого нет реквизитов, поэтому этот способ напрямую не применим. Но если бы виды договоров хранились в справочнике, и у этого справочника был бы реквизит "ТипКонтрагента", этот метод был бы идеален.
Разберем по шагам, как это настраивается для подходящих случаев:
Объект.Владелец.ТипКонтрагента. Платформа автоматически установит фильтр для списка выбора по этому значению.Этот способ элегантен и не требует программирования, но его применимость ограничена структурой данных. Для нашей задачи с перечислением он не подходит, поэтому рассмотрим более универсальный программный метод.
Это самый гибкий и мощный способ, который подойдет для большинства ситуаций. Мы перехватываем момент, когда пользователь нажимает на кнопку выбора, и перед открытием формы выбора динамически формируем для нее нужный фильтр.
Для поля "ВидДоговора" в форме элемента справочника "ДоговорыКонтрагентов" создадим обработчик события НачалоВыбора.
Алгоритм действий будет следующим:
СтандартнаяОбработка = Ложь;. Это говорит платформе, что мы сами решим, как и какую форму выбора открывать.ТипКонтрагента хранится в базе данных, для его получения нужно сделать вызов на сервер, чтобы избежать лишних запросов и оптимизировать производительность.ОткрытьФорму(), передавая в него имя формы выбора и созданную нами структуру с параметрами отбора.Посмотрим на пример реализации этого алгоритма.
&НаКлиенте
Процедура ВидДоговораНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
// 1. Отменяем стандартное открытие формы выбора.
СтандартнаяОбработка = Ложь;
// Проверяем, выбран ли владелец. Если нет, то фильтровать не по чему.
Если НЕ ЗначениеЗаполнено(Объект.Владелец) Тогда
// Открываем форму выбора без всяких отборов.
ОткрытьФорму("Перечисление.ВидыДоговоров.ФормаВыбора",, Элемент);
Возврат;
КонецЕсли;
// 2. Получаем тип контрагента-владельца на сервере.
ТипКонтрагентаВладельца = ПолучитьТипКонтрагентаНаСервере(Объект.Владелец);
// 3. Создаем структуру для отбора.
// В нашем случае мы хотим отобрать конкретные значения, а не по реквизиту.
// Поэтому будем формировать не структуру отбора, а массив допустимых значений.
СписокВыбора = Новый Массив;
// В зависимости от типа контрагента наполняем массив допустимыми видами договоров.
Если ТипКонтрагентаВладельца = Перечисления.ТипыКонтрагентов.Клиент Тогда
СписокВыбора.Добавить(Перечисления.ВидыДоговоров.СКлиентом);
// Можно добавить и другие виды, если они допустимы для клиента.
// СписокВыбора.Добавить(Перечисления.ВидыДоговоров.Прочее);
ИначеЕсли ТипКонтрагентаВладельца = Перечисления.ТипыКонтрагентов.Поставщик Тогда
СписокВыбора.Добавить(Перечисления.ВидыДоговоров.СПоставщиком);
Иначе
// Для всех остальных типов разрешаем, например, "Прочее"
СписокВыбора.Добавить(Перечисления.ВидыДоговоров.Прочее);
КонецЕсли;
// 4. Открываем форму выбора, передавая ей наш список.
// Стандартная форма выбора перечисления умеет работать со списком выбора.
ПараметрыФормы = Новый Структура("ВыборЗначений", СписокВыбора);
ОткрытьФорму("Перечисление.ВидыДоговоров.ФормаВыбора", ПараметрыФормы, Элемент);
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьТипКонтрагентаНаСервере(СсылкаНаКонтрагента)
// Это оптимальный способ получить один реквизит объекта, не считывая весь объект.
// Для простых случаев можно и просто "разыменовать" ссылку:
// Возврат СсылкаНаКонтрагента.ТипКонтрагента;
// Но для высокой производительности лучше использовать ЗначениеРеквизитаОбъекта.
Возврат ОбщегоНазначения.ЗначениеРеквизитаОбъекта(СсылкаНаКонтрагента, "ТипКонтрагента");
// Если нет модуля ОбщегоНазначения, используем простой вариант:
// Возврат СсылкаНаКонтрагента.ТипКонтрагента;
КонецФункции
Важные моменты в этом коде:
&НаСервереБезКонтекста: Серверная функция объявлена как безконтекстная. Это означает, что она не имеет доступа к данным формы (реквизиту Объект). Это правильно, так как все необходимые данные (ссылка на контрагента) мы передаем ей через параметр. Такие вызовы работают быстрее.ОткрытьФорму: Мы явно указываем имя формы Перечисление.ВидыДоговоров.ФормаВыбора и передаем ей параметры. Стандартная форма выбора перечисления автоматически обработает параметр ВыборЗначений и покажет пользователю только те элементы, которые содержатся в переданном массиве.В заключение, как верно подметили на форуме, отличный способ научиться правильным техникам — это изучать код типовых конфигураций (например, "Бухгалтерия предприятия" или "Управление торговлей"). Подобные механизмы фильтрации списков там используются повсеместно и реализованы в соответствии с лучшими практиками разработки на платформе 1С.