Для начала было перерыто множество форумов. По большей части в них предлагали хранить состояние в дополнительной колонке. Но в моем дереве свыше 400 тыс. записей, а это дополнительный расход памяти. Поэтому я пошел другим путем.
Анализ задачи привел к следующим выводам:
- хранить нужно список только развернутых строк дерева;
- если строка сворачивается, хранить ее в списке больше не нужно;
- список развернутых строк дерева храним на клиенте, относительно всего дерева он обычно очень небольшой;
- если есть выделенная строка, после обновления дерева выделяем ее снова;
- поскольку после обновления дерева идентификаторы в ДанныхФормыДерево изменяются, нам нужно это проконтролировать.
Для клиентской стороны была введена переменная типа Соответствие. Она будет хранить для развернутых строк их уникальные значения (как ключ) и идентификаторы строк (как значения). Важно то, что нельзя в качестве ключей использовать идентификаторы строк. Как я написал выше, они изменяются системой при обновлении дерева, и повлиять на это невозможно.
&НаКлиенте
Перем СоответствиеИдентификаторовРазвернутыхСтрокДерева;
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Это соответствие будет хранить идентификаторы строк дерева
СоответствиеИдентификаторовРазвернутыхСтрокДерева = Новый Соответствие;
КонецПроцедуры
У таблицы управляемой формы, связанной с ДеревомЗначений, появляются два события - ПередРазворачиванием() и ПередСворачиваением(). В эти события помещаем алгоритмы добавления/удаления данных по разворачиваемой/сворачиваемой строке дерева.
&НаКлиенте
Процедура ДеревоЗначенийПередРазворачиванием(Элемент, Строка, Отказ)
// Запоминаем идентификатор строки развернутого узла
ТекущаяСтрока = ДеревоЗначений.НайтиПоИдентификатору(Строка);
Если ТекущаяСтрока <> Неопределено Тогда
НайденноеЗначение = СоответствиеИдентификаторовРазвернутыхСтрокДерева.Получить(ТекущаяСтрока.Ссылка);
Если НайденноеЗначение = Неопределено Тогда
СоответствиеИдентификаторовРазвернутыхСтрокДерева.Вставить(ТекущаяСтрока.Ссылка, Строка);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ДеревоЗначенийПередСворачиванием(Элемент, Строка, Отказ)
// Если узел свернут, хранить его состояние нам уже не требуется
ТекущаяСтрока = ДеревоЗначений.НайтиПоИдентификатору(Строка);
Если ТекущаяСтрока <> Неопределено Тогда
СоответствиеИдентификаторовРазвернутыхСтрокДерева.Удалить(ТекущаяСтрока.Ссылка);
КонецЕсли;
КонецПроцедуры
При открытии формы дерево заполняется автоматически в событии ПриСозданииНаСервере() и находится с свернутом состоянии.
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Обновляем дерево значений
ОбновитьДеревоЗначенийНаСервере();
КонецПроцедуры
Пользователь может самостоятельно обновить данные, например, по нажатию кнопки. Опишем программное обновление дерева на клиентской стороне.
&НаКлиенте
Процедура ОбновитьДеревоЗначений(Команда)
// Запоминаем текущий выделенный элемент дерева
ТекущиеДанные = Элементы.ДеревоЗначений.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
ТекущаяСтрока = ТекущиеДанные.Ссылка;
Иначе
ТекущаяСтрока = Неопределено;
КонецЕсли;
// Для экономии памяти очищаем содержимое дерева на форме
ДеревоЗначений.ПолучитьЭлементы().Очистить();
// Выполняем обновление дерева иерархии
ОбновитьДеревоЗначенийНаСервере(СоответствиеИдентификаторовРазвернутыхСтрокДерева, ТекущаяСтрока);
// Пробегаемся по списку и восстанавливаем развернутые узлы
Для Каждого ЭлементСоответствия Из СоответствиеИдентификаторовРазвернутыхСтрокДерева Цикл
Элементы.ДеревоЗначений.Развернуть(ЭлементСоответствия.Значение);
КонецЦикла;
// Выделяем ранее запомненный элемент
Если ТекущаяСтрока <> Неопределено И ТекущаяСтрока > -1 Тогда
Элементы.ДеревоЗначений.ТекущаяСтрока = ТекущаяСтрока;
КонецЕсли;
КонецПроцедуры
Обновление данных дерева выполняем на сервере.
&НаСервере
Процедура ОбновитьДеревоЗначенийНаСервере(СоответствиеИдентификаторовСтрок = Неопределено, ТекущаяСтрока = Неопределено)
// Определим значение пустой ссылки
ПустаяСсылка = XMLЗначение(Тип("СправочникСсылка.Номенклатура"), "00000000-0000-0000-0000-000000000000");
// Создадим дерево, значения которого в последующем выгрузим в реквизит формы
ДеревоОбъект = Новый ДеревоЗначений;
ДеревоОбъект.Колонки.Добавить("Картинка", Новый ОписаниеТипов("Картинка"));
ДеревоОбъект.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ДеревоОбъект.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(100, ДопустимаяДлина.Переменная)));
// Выполняем запрос и заполняем дерево
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Родитель КАК Родитель,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления
|
|УПОРЯДОЧИТЬ ПО
| Номенклатура.ЭтоГруппа,
| Номенклатура.Наименование
|ИТОГИ ПО
| Ссылка ИЕРАРХИЯ";
СоответствиеНоменклатурыИСтрокДерева = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Ссылка = Выборка.Ссылка;
СсылкаНаРодителя = Выборка.Родитель;
// Отсеиваем самый верхний уровень и дубли
Если Ссылка = ПустаяСсылка ИЛИ СоответствиеНоменклатурыИСтрокДерева.Получить(Ссылка) <> Неопределено Тогда
Продолжить;
КонецЕсли;
// Определяем строку дерева, куда поместим новый элемент
Если СсылкаНаРодителя <> ПустаяСсылка Тогда
Родитель = СоответствиеНоменклатурыИСтрокДерева.Получить(СсылкаНаРодителя);
Иначе
Родитель = ДеревоОбъект;
КонецЕсли;
НовыйЭлемент = Родитель.Строки.Добавить();
НовыйЭлемент.Наименование = Выборка.Наименование;
НовыйЭлемент.Ссылка = Ссылка;
СоответствиеНоменклатурыИСтрокДерева.Вставить(Ссылка, НовыйЭлемент);
КонецЦикла;
// Если это не первое формирование дерева, то нужно искать развернутые узлы
Если СоответствиеИдентификаторовСтрок <> Неопределено Тогда
// Создаем таблицу, которая будет хранить информацию по строкам с иерархией
ТаблицаРазвернутыхСтрокДерева = Новый ТаблицаЗначений;
ТаблицаРазвернутыхСтрокДерева.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Неопределено,Число", Новый КвалификаторыЧисла(10)));
ТаблицаРазвернутыхСтрокДерева.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаРазвернутыхСтрокДерева.Колонки.Добавить("Развернут", Новый ОписаниеТипов("Булево"));
// Добавим индекс для колонок "Ссылка" и "Идентификатор", т.к. по ним идет поиск
ТаблицаРазвернутыхСтрокДерева.Индексы.Добавить("Ссылка");
ТаблицаРазвернутыхСтрокДерева.Индексы.Добавить("Идентификатор");
// Перебираем записи соответствия. Если у элемента есть подчиненные, проверяем
// его на вхождение в соответствие развернутых строк. Если есть совпадение,
// добавляем в таблицу строк дерева.
СтруктураОтбора = Новый Структура("Ссылка", Неопределено);
Для Каждого ЭлементСоответствия Из СоответствиеНоменклатурыИСтрокДерева Цикл
Если ЭлементСоответствия.Значение.Строки.Количество() > 0 Тогда
Ссылка = ЭлементСоответствия.Ключ;
// Ищем в развернутых узлах
Идентификатор = СоответствиеИдентификаторовСтрок.Получить(Ссылка);
Если Идентификатор <> Неопределено Тогда
// Исключаем дубли в таблице
НайденнаяСтрока = ТаблицаРазвернутыхСтрокДерева.Найти(Ссылка, "Ссылка");
Если НайденнаяСтрока = Неопределено Тогда
НоваяСтрока = ТаблицаРазвернутыхСтрокДерева.Добавить();
НоваяСтрока.Ссылка = Ссылка;
НоваяСтрока.Развернут = Истина;
НоваяСтрока.Идентификатор = Неопределено;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Очищаем за ненадобностью соответствие
СоответствиеНоменклатурыИСтрокДерева.Очистить();
// Сортируем по колонке Наименование и удалем ее за ненадобностью
ДеревоОбъект.Строки.Сортировать("Наименование", Истина);
ДеревоОбъект.Колонки.Удалить(ДеревоОбъект.Колонки.Наименование);
// Записываем дерево в реквизит формы
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДеревоЗначений");
// Очищаем за ненадобностью дерево
ДеревоОбъект.Строки.Очистить();
Если СоответствиеИдентификаторовСтрок <> Неопределено ТОгда
// Пробегаемся по дереву формы, чтобы получить индексы строк
ОбновитьТаблицуИдентификаторовСтрокДереваНаСервере(ТаблицаРазвернутыхСтрокДерева, ДеревоЗначений, ТекущаяСтрока);
// Далее мы обновляем идентификаторы развернутых строк дерева, чтобы потом
// использовать для восстановления состояния дерева. Эта процедура
// необходима из-за того, что после каждого перестроения дерева индексы
// строк изменяются.
СоответствиеИдентификаторовСтрок.Очистить();
Для Каждого СтрокаТаблицы Из ТаблицаРазвернутыхСтрокДерева Цикл
СоответствиеИдентификаторовСтрок.Вставить(СтрокаТаблицы.Ссылка, СтрокаТаблицы.Идентификатор);
КонецЦикла;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Процедура ОбновитьТаблицуИдентификаторовСтрокДереваНаСервере(ТаблицаРазвернутыхСтрокДерева, ДеревоЗначений, ТекущаяСтрока = Неопределено)
// Получаем все элементы строки дерева значений
СтрокаДереваЗначений = ДеревоЗначений.ПолучитьЭлементы();
Если СтрокаДереваЗначений.Количество() = 0 Тогда Возврат; КонецЕсли;
// Определяем, нужно ли искать выделенный на форме элемент
ИскатьТекущуюСтроку = (ТекущаяСтрока <> Неопределено И ТипЗнч(ТекущаяСтрока) = Тип("СправочникСсылка.Номенклатура"));
СтруктураОтбора = Новый Структура("Идентификатор", Неопределено);
Для Каждого ЭлементСтрокиДерева Из СтрокаДереваЗначений Цикл
// Обновляем идентификаторы строк
НайденнаяСтрока = ТаблицаРазвернутыхСтрокДерева.Найти(ЭлементСтрокиДерева.Ссылка, "Ссылка");
Если НайденнаяСтрока <> Неопределено Тогда
НайденнаяСтрока.Идентификатор = ЭлементСтрокиДерева.ПолучитьИдентификатор();
КонецЕсли;
Если ИскатьТекущуюСтроку Тогда
// Ищем новый индекс выделенной в элементе строки
Если ЭлементСтрокиДерева.Ссылка = ТекущаяСтрока Тогда
ТекущаяСтрока = ЭлементСтрокиДерева.ПолучитьИдентификатор();
КонецЕсли;
КонецЕсли;
// Чтобы исключить ситуацию, когда все индексы найдены, а перебор дерева
// еще идет, проверим строки со значением Неопределено.
Если НЕ ИскатьТекущуюСтроку И ТаблицаРазвернутыхСтрокДерева.НайтиСтроки(СтруктураОтбора).Количество() = 0 Тогда
Прервать;
КонецЕсли;
ОбновитьТаблицуИдентификаторовСтрокДереваНаСервере(ТаблицаРазвернутыхСтрокДерева, ЭлементСтрокиДерева, ТекущаяСтрока);
КонецЦикла;
КонецПроцедуры
Кратко опишу, что происходит. Запросом формируем иерархию справочника Номенклатура и строим дерево. Чтобы исключить повторное добавление в дерево уже имеющихся элементов, отсекаем, используя СоответствиеНоменклатурыИСтрокДерева. Также попутно из этого соответствия получаем строку-родитель дерева для нового элемента.
После того, как дерево построено, формируем таблицу с данными развернутых строк. Эта часть актуальна для перестроения дерева. При первом построении она пропускается, поскольку у нас нету развернутых узлов.
Когда таблица создана, перегружаем дерево в реквизит формы ДеревоЗначений. Затем методом ОбновитьТаблицуИдентификаторовСтрокДереваНаСервере() перебираем данные реквизита ДеревоЗначений (он имеет тип ДанныеФормыДерево) и ищем номенклатуру из дерева в таблице ТаблицаРазвернутыхСтрокДерева. Если совпадения находятся, изменяем идентификаторы в строках ТаблицыРазвернутыхСтрокДерева. Если у пользователя была выделена строка, также ищем новый идентификатор этой строки и запоминаем его. В методе ОбновитьТаблицуИдентификаторовСтрокДереваНаСервере() установлено ограничение, которое срабатывает, если обновлены идентификаторы всех развернутых строк.
Далее управление возвращается в процедуру ОбновитьДеревоЗначенийНаСервере(), где клиентская переменная соответствия идентификаторов строк дерева обновляется актуальными данными. На клиенте строки дерева разворачиваются согласно данным соответствия, выделяется строка, обновление дерева завершается.
Сам алгоритм достаточно простой, несмотря на немаленький объем кода. Использовать его можно в любом дереве. Достаточно определить уникальное значение для каждой строки дерева.
Работает код тоже быстро. В частности, через веб-клиента на компьютере средней производительности дерево из 400 тыс. строк строится примерно за 2 минуты, перестраивается за 40 секунд.
Я постарался подробно прокомментировать непонятные моменты. Думаю, что-то окажется вам полезным. Буду благодарен за конструктивные предложения и примеры других реализаций. На комментарии вроде "да ты нифига не умеешь, парниша" отвечать не буду принципиально. Спасибо за внимание.