При разработке в среде «1С:Предприятие 8.3» (особенно при доработке типовых конфигураций, таких как «Зарплата и управление персоналом» через механизм расширений) начинающие программисты часто сталкиваются с трудностями при работе с контекстом управляемых форм. Типичная задача — заполнить добавленную колонку в табличной части документа по нажатию кнопки — вызывает массу вопросов, связанных с тем, где должен выполняться код (на клиенте или на сервере) и как правильно обращаться к данным формы.
В этой статье мы подробно разберем, как реализовать этот функционал, проанализируем основные ошибки, с которыми сталкиваются разработчики, и напишем оптимальный, производительный код для обхода табличной части документа.
Давайте проанализируем код, который изначально пытался использовать автор темы:
&НаСервереБезКонтекста
Процедура Расш1_ЗаполнитьЗаказыПередНаСервере
Сообщить("Кнопка работает");
КонецПроцедуры
&НаКлиенте
Процедура Расш1_ЗаполнитьЗаказыПеред(Команда)
Расш1_ЗаполнитьЗаказыПередНаСервере;
КонецПроцедуры
В чем заключается критическая ошибка данного подхода? Автор использовал директиву компиляции &НаСервереБезКонтекста. Процедуры и функции с этой директивой выполняются на стороне сервера, но при этом они не имеют доступа к реквизитам формы. Это означает, что внутри такой процедуры переменная Объект (и, соответственно, табличная часть Объект.Начисления) просто не существует. Платформа выдаст ошибку компиляции: «Переменная не определена (Объект)».
Директива &НаСервереБезКонтекста используется для выполнения быстрых серверных вызовов (например, получить значение из базы данных по ссылке), когда передавать всю форму на сервер не имеет смысла из-за экономии трафика. Но если наша цель — изменить данные на самой форме (заполнить строки табличной части, которые видит пользователь), нам необходимо использовать контекстный вызов с директивой &НаСервере.
При контекстном вызове состояние всех реквизитов формы передается на сервер, там мы их модифицируем, и измененная форма возвращается обратно клиенту.
В управляемом приложении все данные документа, открытого в форме, содержатся в основном реквизите формы, который по умолчанию называется Объект (тип ДанныеФормыСтруктура). Получить ключевые свойства документа очень просто:
Объект.Номер. Это свойство можно читать как на клиенте, так и на сервере.Объект.Ссылка. Обратите внимание: если пользователь только что создал новый документ и еще не записал его, Объект.Ссылка будет содержать пустую ссылку соответствующего типа (Документы.НачислениеЗарплатыИВзносов.ПустаяСсылка()).Рассмотрим, как это выглядит в коде:
&НаСервере
Процедура ПолучитьДанныеДокумента()
// Получаем номер текущего документа
ТекущийНомер = Объект.Номер;
// Получаем ссылку
ТекущаяСсылка = Объект.Ссылка;
Если ТекущаяСсылка.Пустая() Тогда
// Документ еще не записан в базу данных
Сообщить("Внимание! Документ новый и еще не записан.");
Иначе
Сообщить("Работаем с документом номер: " + ТекущийНомер);
КонецЕсли;
КонецПроцедуры
Для того чтобы обойти строки табличной части Начисления текущего документа, мы используем стандартный цикл платформы 1С — Для Каждого ... Из ... Цикл.
Важный момент: обращаться к коллекции строк табличной части на сервере нужно через конструкцию Объект.Начисления. Каждая строка в этом цикле будет иметь тип ДанныеФормыЭлементКоллекции, и мы сможем напрямую обращаться к ее колонкам (как к стандартным, так и к созданным через расширение).
Простейший пример обхода и изменения ячеек выглядит так:
Для Каждого СтрокаТЧ Из Объект.Начисления Цикл
// Заполняем реквизит "НомерЗаказа" в каждой строке
СтрокаТЧ.НомерЗаказа = "Значение_По_Умолчанию";
КонецЦикла;
По задумке автора, данные для колонки НомерЗаказа должны подтягиваться из справочника по определенному условию.
Рассмотрим первый вариант решения (простой) — поиск элемента справочника непосредственно в цикле по реквизиту. Этот вариант подходит, если строк в документе немного (до нескольких десятков). Мы будем искать нужный заказ в справочнике (например, по сотруднику, указанному в строке начисления):
&НаСервере
Процедура ЗаполнитьЗаказыПростымСпособом()
Для Каждого СтрокаТЧ Из Объект.Начисления Цикл
// Проверяем, заполнен ли сотрудник в строке
Если ЗначениеЗаполнено(СтрокаТЧ.Сотрудник) Тогда
// Ищем заказ в справочнике "Заказы" по реквизиту "Сотрудник"
НайденныйЗаказ = Справочники.Заказы.НайтиПоРеквизиту("Сотрудник", СтрокаТЧ.Sотрудник);
// Если заказ найден, записываем его в нашу новую колонку
Если ЗначениеЗаполнено(НайденныйЗаказ) Тогда
СтрокаТЧ.НомерЗаказа = НайденныйЗаказ;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Однако если в документе «Начисление зарплаты и взносов» содержатся сотни или тысячи строк (что часто бывает в крупных организациях), поиск справочника в цикле приведет к критическому падению производительности. На каждую строку система будет делать отдельный запрос к базе данных (так называемая проблема "запросов в цикле").
Рассмотрим второй вариант решения (профессиональный и оптимизированный) — получение всех данных одним запросом перед обходом табличной части:
&НаСервере
Процедура ЗаполнитьЗаказыОптимизированно()
// 1. Выгружаем уникальных сотрудников из табличной части в массив
МассивСотрудников = Новый Массив;
Для Каждого СтрокаТЧ Из Объект.Начисления Цикл
Если ЗначениеЗаполнено(СтрокаТЧ.Сотрудник) Тогда
МассивСотрудников.Добавить(СтрокаТЧ.Сотрудник);
КонецЕсли;
КонецЦикла;
// Исключаем дубликаты сотрудников для оптимизации запроса
ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивСотрудников);
Если МассивСотрудников.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// 2. Одним запросом выбираем соответствие сотрудников и их заказов из справочника
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Заказы.Сотрудник КАК Сотрудник,
| Заказы.Ссылка КАК Заказ
|ИЗ
| Справочник.Заказы КАК Заказы
|ГДЕ
| Заказы.Сотрудник В (&МассивСотрудников)";
Запрос.УстановитьПараметр("МассивСотрудников", МассивСотрудников);
// 3. Сохраняем результаты запроса во временную структуру Соответствие для быстрого поиска в памяти
СоответствиеЗаказов = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СоответствиеЗаказов.Вставить(Выборка.Сотрудник, Выборка.Заказ);
КонецЦикла;
// 4. Обходим ТЧ и быстро заполняем колонку из подготовленного Соответствия без обращений к БД
Для Каждого СтрокаТЧ Из Объект.Начисления Цикл
Если ЗначениеЗаполнено(СтрокаТЧ.Сотрудник) Тогда
НайденныйЗаказ = СоответствиеЗаказов.Получить(СтрокаТЧ.Сотрудник);
Если НайденныйЗаказ <> Неопределено Тогда
СтрокаТЧ.НомерЗаказа = НайденныйЗаказ;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Когда мы программно изменяем данные в табличной части на сервере, платформа изменяет значения в памяти. Однако пользователь должен увидеть, что документ изменился (в заголовке окна появится символ «*», а кнопка «Записать» станет активной).
Для этого в конце серверной процедуры обязательно нужно взвести флаг модифицированности формы, выполнив простую команду: ЭтаФорма.Модифицированность = Истина;. Если этого не сделать, при закрытии формы система может не предложить пользователю сохранить изменения, и проделанная работа по заполнению колонок будет утеряна.
Ниже представлен готовый к использованию код, который необходимо разместить в модуле расширения формы документа «Начисление зарплаты и взносов»:
&НаКлиенте
Процедура Расш1_ЗаполнитьЗаказыПеред(Команда)
// Вызываем контекстную серверную процедуру для обработки данных формы
Расш1_ЗаполнитьЗаказыНаСервере();
КонецПроцедуры
&НаСервере
Процедура Расш1_ЗаполнитьЗаказыНаСервере()
// Выполняем обход табличной части документа
Для Каждого СтрокаТЧ Из Объект.Начисления Цикл
// Алгоритм поиска заказа в справочнике (замените "Заказы" на фактическое имя вашего справочника)
Если ЗначениеЗаполнено(СтрокаТЧ.Сотрудник) Тогда
// Выполняем поиск по реквизиту "Сотрудник"
НайденныйЗаказ = Справочники.Заказы.НайтиПоРеквизиту("Сотрудник", СтрокаТЧ.Сотрудник);
// Если значение найдено, записываем его в добавленный реквизит "НомерЗаказа"
Если ЗначениеЗаполнено(НайденныйЗаказ) Тогда
СтрокаТЧ.НомерЗаказа = НайденныйЗаказ;
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Уведомляем форму, что данные были изменены пользователем программно
ЭтаФорма.Modифицированность = Истина;
// Выводим информационное сообщение пользователю
ПоказатьОповещениеПользователя("Заполнение завершено", , "Колонка ""Номер заказа"" успешно заполнена!", КартинкаОповещения.Информация);
КонецПроцедуры
Используя этот шаблон, вы сможете легко и быстро автоматизировать заполнение любых аналитических колонок в ваших документах, избежав при этом типичных ошибок с контекстом выполнения процедур в управляемых формах 1С.