Асинхронное программирование в 1с77 без внешних компонент. Обратные вызовы.

Программирование - Практика программирования

Обратные вызовы call back 1с77 асинхронность асинхронные вызовы

8
Пример построения программного кода для достижения функционала обратных вызовов (call back) во внешних обработках исключительно штатными средствами. Тестировалось на платформе 1с77 релиз 027. Конфигурация значения не имеет.

Принято считать, что асинхронные вызовы в программном коде – это что-то новое, модное, молодежное. Ряд публикаций на этом ресурсе освещает новый подход к программированию: как правильно открывать форму и получать от нее ответ, как правильно взаимодействовать с пользователем, будь то вопрос или форма выбора файла. Специально для этого в платформу 8х были добавлены новые методы и объекты. А что же с 1с77? Есть ли там эта асинхронность? А если и есть, то в каких задачах она используется?

На самом деле асинхронные вызовы были и в платформе 1с77. Тривиальные задачи: обработка подбора, обработка выбора значения, обработка внешнего события, обработка ожидания. Это все асинхронные вызовы. Основным отличием этих методов является то, что после совершения асинхронного вызова функции выполнение программного кода не останавливается. А результат работы этой функции нужно отлавливать в другой процедуре (как правило предопределенной), которая в свою очередь вызывается тем объектом, к которому мы обратились асинхронным вызовом.

Зачем это все надо? Давайте рассмотрим обработку подбора. Из формы документа мы запускаем форму подбора. Когда на форме подбора мы совершаем выбор элемента, то в форме документа выполняется процедура ОбработкаПодбора. То есть мы смогли вызвать процедуру другой формы, хотя у 1с версии 77 есть ограничение, что из других модулей можно вызывать только экспортные процедуры глобального модуля. При помощи обратного вызова ОбработкаПодбора мы смогли обойти это органичение. А что если мы хотим описать свои процедуры обратного вызова? Что если у нас есть основная форма обработки, в которой собран ключевой функционал бэкэнда нашей разработки. А для взаимодействия с пользователем мы хотим написать отдельные формы плагины, по одной на каждую задачу. Каким образом использовать функционал основной обработки находясь в контексте дополнительной формы? 1с77 не позволяет в обработку добавить дополнительные формы и модуля объекта у внешней обработки тоже нет. Мы хотим обойти и это ограничение. Разделить функциональность нашего проекта на отдельные обработки. По 1-й на каждую задачу. И здесь нам поможет принцип обратного вызова.

Итак, задача состоит в том, чтобы передать из дополнительной формы обработки в основную команду на выполнение процедуры. После выполнения этой процедуры основная форма должна оповестить дополнительную о результате выполнения, иными словами так же передать на выполнение команду в обратную сторону. Как это можно сделать?

  1. Установить 1С++, написать классы и не морочить уважаемым людям голову)
  2. Написать внешнюю компоненту, которая генерирует ВнешнееСобытие() и ловить результат Обработчиком внешнего события.
  3. Использовать обработку предопределенного события формы, путем воздействия на эту форму.

Как вы уже поняли из заголовка публикации, я буду описывать третий метод. Идея не нова. Такой же принцип используют, например, для групповой печати документов. Передают специальный параметр при открытии формы документа, а в процедуре ПриОткрытии() отлавливают этот параметр, выполняют печать, и ставят СтатусВозврата(0), чтобы форма документа не мигала на экране. Кроме того похожим способом можно провести один документ при проведении другого, хотя если написать в лоб, то 1с нам это запретит.

Использовать мы будем событие закрытия формы и предопределенную процедуру ПриЗакрытии(). Чтобы форма не закрывалась при этом, мы установим СтатусВозврата(0). Для обмена информацией между вызовами будем использовать переменную модуля формы основной обработки кэш. В этой переменной хранится общая для всего проекта структура-список значений, наподобие глРасшифровка, которая используется при работе с отчетами в типовых конфигурациях. Прелесть объекта «СписокЗначений» в том, что через переменную всегда передается ссылка на единожды созданный объект, где бы мы не обратились к ней, а также в том, что можно устанавливать и получать значения по текстовому ключу, как в структуре.

Для обращения к переменной кэш на основной форме напишем функцию:

Функция кэш()
	Если ТипЗначенияСтр(кэш) = "" Тогда
		кэш = СоздатьОбъект("СписокЗначений");
	КонецЕсли;
	Возврат кэш;
КонецФункции

Чтобы обращаться к этой переменной в дополнительной форме мы ее передаем через параметр открытия формы. Поскольку после открытия формы ее параметр изменяет тип на «ГрупповойКонтекст» мы не можем поместить в параметр непосредсвенно саму переменную кэш. Для этого мы создаем список значений, добавляем в него кэш() и передаем этот список как параметр открытия формы.

Процедура для открытия дополнительной формы:

Функция ОткрытьДополнительнуюФорму(имяФормы)
	имяФайлаФормы = имяФормы+".ert";
	путьКФорме = РасположениеОбработки()+"\"+имяФайлаФормы;
	Если ФС.СуществуетФайл(путьКФорме) = 0 Тогда
		Сообщить("Отсутствует файл: """+имяФайлаФормы+""" в каталоге поставки обработки","!");
		Возврат 0;
	КонецЕсли; 
	кэш().Установить("КонтекстОсновной",Контекст);
	пар = СоздатьОбъект("СписокЗначений");
	пар.Установить("КэшОсновной",кэш());	
	Возврат ОткрытьФорму("Отчет",пар,путьКФорме);	
КонецФункции

Функция РасположениеОбработки() возвращает путь к папке где лежит текущая форма. Вот ее код:

Функция РасположениеОбработки()
	расположение = кэш().Получить("РасположениеОбработки");
	Если ТипЗначенияСтр(расположение) = "" Тогда
		РасположениеФайла(расположение);
		кэш().Установить("РасположениеОбработки",расположение);
	КонецЕсли;
	Возврат расположение;
КонецФункции

Для обращения к кэшу в дополнительной форме используется функция

Функция кэш()
	Возврат Форма.Параметр.Получить("КэшОсновной");
КонецФункции

Для корректного описания запроса от одной формы к другой нам нужен список-структура. Я его называю «ПараметрЗакрытия». В нем указывается имя функции которую мы хотим запустить, контекст текущей формы, куда мы хотим передать ответ, данные которые должна обрабатывать эта функция.

Код функции которая возвращает параметр закрытия (в основной и в дополнительной форме):

Функция ПараметрЗакрытия(устПараметр = "")
	парамЗакрытия = кэш().Получить("ПараметрЗакрытия");
	Если ПустоеЗначение(устПараметр) = 0 Тогда
		парамЗакрытия = устПараметр;
		парамЗакрытия.Установить("Обработан",0);
		кэш().Установить("ПараметрЗакрытия",парамЗакрытия);
	КонецЕсли;
	Возврат парамЗакрытия;
КонецФункции

Через параметр «устПараметр» передается списокзначений. При создании параметра закрытия в нем устанавливается признак «Обработан» = 0, который по факту срабатывания процедуры «ПриЗакрытии()» устанавливается сразу в 1. Это предотвращает ситуацию, когда из-за ошибки форму обработки невозможно закрыть.

Обработка параметра закрытия выполняется в предопределенной процедуре «ПриЗакрытии()». Ниже приведен код этой процедуры (одинаковый для любой формы)

Процедура ПриЗакрытии()
	Если ТипЗначенияСтр(ПараметрЗакрытия()) = "СписокЗначений" Тогда
		Если Число(ПараметрЗакрытия().Получить("Обработан")) = 1 Тогда
			//Для случая когда из-за ошибки параметр закрытия не удален из кэша 
			Возврат;
		КонецЕсли;		
		СтатусВозврата(0);
		имяФункции = ПараметрЗакрытия().Получить("Функция");
		ПараметрЗакрытия().Установить("Обработан",1);
		результатКолБэк = Число(Шаблон("["+имяФункции+"]"));       
		Если Лев(имяФункции,13) = "КБ_Результат(" Тогда
			Возврат;
		КонецЕсли;		
		контВызывающего = ПараметрЗакрытия().Получить("КонтФормы");		
		Если ТипЗначенияСтр(контВызывающего) = "ГрупповойКонтекст" Тогда
			//Вызов колбэка вызывающей формы для передачи результата
			ПараметрЗакрытия().Установить("Обработан",0);
			ПараметрЗакрытия().Установить("Функция","КБ_Результат("+результатКолБэк+")");
			ПараметрЗакрытия().Установить("Операция",имяФункции);
			ПараметрЗакрытия().Установить("КонтФормы","");
			контВызывающего.Форма.Закрыть(0);
		КонецЕсли;			
	КонецЕсли;
КонецПроцедуры

Здесь проверяется, существует ли параметр закрытия в кэше и не обработан ли он уже. Затем получаем имя функции которую надо выполнить и через конструкцию Шаблон() выполняем её. Важно – функция может вернуть только примитивный тип, да и тот через Шаблон() будет преобразован в строку. Если надо передать что-то посложнее, добавьте эти данные в параметр закрытия. Если в параметре закрытия по ключу «КонтФормы» передан контекст формы, то вызываем процедуру ПриЗакрытии() уже этой формы и отправляем команду выполнить процедуру «КБ_Результат()». Естественно, если мы хотим отловить результат обратного вызова на той форме эта процедура должна быть описана. Для определения, по какой функции был получен результат мы копируем имя функции в ключ «Операция». До выполнения обратного вызова обязательно проверяем или до этого мы не отрабатывали результат от другой формы (функцию «КБ_Результат()»).

Код для вызова функции через параметр закрытия:

Процедура ДобавитьЗапись()
	парамЗакрытия = СоздатьОбъект("СписокЗначений");
	парамЗакрытия.Установить("Функция","КБ_ДобавитьЗапись()");
	парамЗакрытия.Установить("КонтФормы",Контекст);
	парамЗакрытия.Установить("КБ_Код",Код);
	парамЗакрытия.Установить("КБ_Наим",СокрЛП(Наименование));
	ПараметрЗакрытия(парамЗакрытия);
	контОсновной().Форма.Закрыть(0);
КонецПроцедуры

КонтОсновной() возвращает контекст основной формы, где расположена фукнция «КБ_ДобавитьЗапись()», которую мы хотим выполнить. Он также получается из кэш(), а устанавливается туда при открытии дополнительной формы (см. выше).

Функция контОсновной()
	Возврат кэш().Получить("КонтекстОсновной");
КонецФункции

Код функции «КБ_ДобавитьЗапись()» из основной формы

Функция КБ_ДобавитьЗапись()
	код = ПараметрЗакрытия().Получить("КБ_Код");
	найСтр = 0;
	Если тбРезультат.НайтиЗначение(код,найСтр,"Код") = 1 Тогда
		ПараметрЗакрытия().Установить("ОписаниеОшибки","Запись с кодом: "+код+" уже добавлена!");
		Возврат 0;
	КонецЕсли;
	тбРезультат.НоваяСтрока();
	тбРезультат.Код = код;
	тбРезультат.Наименование = ПараметрЗакрытия().Получить("КБ_Наим");
	Возврат 1;
КонецФункции

тбРезультат – это таблица на основной форме с колонками «Код» и «Наименование». При выполнении этой функции туда будут добавляться записи. Для примера ситуация, когда мы пытаемся добавить существующий код, считается ошибочной и возвращается 0. Чтобы передать вызывающей форме описание ошибки это описание устанавливается в параметре закрытия по ключу «ОписаниеОшибки».

Код функции «КБ_Результат()» на дополнительной форме.

Функция КБ_Результат(результат)
	//Перехват колбэка по результату вызова колбэка основной
	операция = ПараметрЗакрытия().Получить("Операция");
	Если результат = 1 Тогда
		Если операция = "КБ_ДобавитьЗапись()" Тогда
		    Форма.тРезультат.Заголовок("Запись добавлена успешно");
		КонецЕсли;
	Иначе
		Форма.тРезультат.Заголовок("ОШИБКА: "+РазделительСтрок+ПараметрЗакрытия().Получить("ОписаниеОшибки"));
	КонецЕсли;			
КонецФункции

Текст тРезультат расположен на дополнительной форме для отображения сообщений при обработке обратного вызова.

Отдельно стоит остановиться на понятии асинхронности. Когда мы вызываем «контФормы.Форма.Закрыть(0)» код не ожидает завершения выполнения предопределенной процедуры «ПриЗакрытии()» в форме из «контФормы». Эти операции выполняются параллельно. Поэтому, например, нельзя запихнуть такой вызов в цикл. Чтобы выполнить такой вызов в несколько итераций стоит вспомнить, что то, что решается через цикл, можно решить рекурсивно и наоборот. Поэтому используем рекурсию. Данные для отправки загоним в объект который поддерживает интерфейс итератора, т.е. конструкцию Выбрать() и Следующий(). Это может быть таблица значений, база данных дбф, справочник, документ. В процедуре которая совершает асинхронный вызов позиционируемся на следующую запись. Если дошли до конца, то выходим, если нет – совершаем асинхронный вызов. В процедуре обработки результата вызываем описанную выше процедуру.

Для примера код добавления записи в цикле и обработки результата:

Процедура ДобавитьСледующуюЗапись()
	Если НаборЗаписей().ПолучитьСтроку() = 0 Тогда
		Возврат;
	КонецЕсли;
	парамЗакрытия = СоздатьОбъект("СписокЗначений");
	парамЗакрытия.Установить("Функция","КБ_ДобавитьЗаписьЦикл()");
	парамЗакрытия.Установить("КонтФормы",Контекст);
	парамЗакрытия.Установить("КБ_Код",НаборЗаписей().Код);
	парамЗакрытия.Установить("КБ_Наим",СокрЛП(НаборЗаписей().Наименование));
	ПараметрЗакрытия(парамЗакрытия);
	контОсновной().Форма.Закрыть(0);
КонецПроцедуры

Процедура ДобавитьЗаписиВЦикле()
    НаборЗаписей(квоЗаписей).ВыбратьСтроки();
	ДобавитьСледующуюЗапись();	
КонецПроцедуры

Функция КБ_Результат(результат)
	//Перехват колбэка по результату вызова колбэка основной
	операция = ПараметрЗакрытия().Получить("Операция");
	Если результат = 1 Тогда
		Если операция = "КБ_ДобавитьЗаписьЦикл()" Тогда
			ДобавитьСледующуюЗапись();	
		КонецЕсли;
	Иначе
		Форма.тРезультат.Заголовок("ОШИБКА: "+РазделительСтрок+ПараметрЗакрытия().Получить("ОписаниеОшибки"));
	КонецЕсли;			
КонецФункции

Функция НаборЗаписей() создает таблицу значений с данными если она еще не создана и возвращает ее. Приведенный пример показывает, как можно совершать асинхронный вызов в цикле. На самом деле так делать не стоит, поскольку скорость выполнения этого метода очень низкая. Много времени теряется при передаче управления между формами. Плюс интерфейс программы неприятно мигает. Лучше формировать набор данных и передавать его целиком при вызове. Рекомендация примерно, как про запросы в цикле.

В заключение хочется добавить, что скорее всего подобный подход будет воспринят специалистами как баловство. У него действительно есть недостатки перед классами 1С++. В первую очередь это низкая скорость работы, и, наверное, плохая читаемость кода. Но в то же время бывает, что клиенты не хотят использовать внешние компоненты. Да и при разработке тиражных разработок желательно, по возможности, обходиться штатными средствами платформы. Когда даешь обработку пользователю, хочется, чтобы запустил – заработало.

8

Скачать файлы

Наименование Файл Версия Размер
Асинхронное программирование в 1с77 без внешних компонент. Обратные вызовы.:
.zip 6,72Kb
06.10.18
2
.zip 6,72Kb 2 Скачать

См. также

Специальные предложения

Комментарии
Избранное Подписка Сортировка: Древо
1. dmarenin 74 06.10.18 20:46 Сейчас в теме
за труд +, но все таки это псевдоасинхронные вызовы ибо в вм 1с (7.7) исполняется один поток на контекст исполнения(те делит контекст исполнения между вызовами), автор сделал интерпретацию евент лупа например как в js, и py(тут есть и гил для распределения выполнения потоков) или я не правильно понял?
2. Vortigaunt 23 08.10.18 09:56 Сейчас в теме
(1) Благодаря вашему комментарию узнал новый термин) Поверхностно ознакомился с понятием event loop, и да - это похоже на то, что я реализовал. Для каждого вызова получается свой контекст, поскольку вызовы обрабатываются в разных обработках.
Не смог расшифровать вот это:
(тут есть и гил для распределения выполнения потоков)

Объясните пожалуйста.
3. BigClock 08.10.18 14:27 Сейчас в теме
(2) Насколько я понимаю, гил - это Global Interpreter Lock: https://ru.wikipedia.org/wiki/Global_Interpreter_Lock
4. bulpi 137 09.10.18 12:38 Сейчас в теме
Красиво. Изящно. Но поддерживать это сможете только Вы. Я с ужасом представляю, как меня подрядят на разбор чего-то такого :)
5. sttt 113 16.10.18 22:20 Сейчас в теме
Далеко не асинхронное, v77 не умеет в типовом исполнении.
1cpp может выполнять асинхронный код, с помощью поддерживающих
технологию компонент, но тоже какие-то проблемы вроде были.
Не все умеют нормально работать

А пример, как куча мала, лучше реальный пример запостить, в виде обработки с замерами
Оставьте свое сообщение