Бывают списки, в которых со временем может появиться очень много записей, и для удобства работы с ними лучше использовать постраничный просмотр.
Также в этом уроке мы рассмотрим работу с изображениями, передачу файлов на сервер и получение их на форму.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Постраничный просмотр
Рассмотрим паттерн Pagination на примере формы списка клиентов.
Подготовка формы
Перейдем в файл списка клиентов (TemplateClientList.xml) и в ContentPanel добавим описание панели PagePanel с объектами, необходимыми для постраничного просмотра списка клиентов:
Скачайте архив с изображением для кнопок постраничного просмотра и распакуйте его в папку \Template\Projects\1. Template\Forms\Images\16x16.
Количество записей в ответе от сервера
Первым делом в запрос ClientSelectSqlQuery для списка клиентов добавим поле Count, в котором будем возвращать общее количество записей в результате запроса:
Template.xml
<SqlQuery Name="ClientSelectSqlQuery">
<Text>
SELECT
client.client_id AS "ClientId",
client.title AS "Name",
city.title AS "CityTitle",
client.phone AS "Phone",
client.archive AS "Archive",
COUNT(*) OVER() AS "Count"
FROM
template.client
LEFT JOIN template.city USING(city_id)
WHERE
{SearchKeywords} ISNULL OR client.title ilike '%' || {SearchKeywords} ||'%'
ORDER BY client.title;
</Text>
</SqlQuery>
Создадим условие для проверки количества строк в ClientPrimaryGetDataConnection, используя для этого get-проперти Count:
Таким образом, если сервер не вернул никаких строк, то берем ноль, иначе будем брать значение из поля Count, которое соответствует количеству записей в базе данных.
Создадим условие проверки этой переменной, с помощью которого будем блокировать кнопки переключения по страницам:
Но переход между страницами возможен и при ручном вводе значения в поле CurrentPageTextBox. Для этого создадим условие для проверки события нажатия клавиши Enter при редактировании текстового поля:
В ClientPrimaryGetDataConnection добавим параметры Limit (количество записей на странице) и Page (номер текущей страницы).
Так же необходимо переделать фильтрацию архивных и актуальных записей. Для этого из колонки Archive в таблице ClientDatabaseTable удалим тэг <Filter>, и добавим в ClientPrimaryGetDataConnection одноименный параметр.
В запрос ClientSelectSqlQuery добавим переменные для новых параметров:
Template.xml
<SqlQuery Name="ClientSelectSqlQuery">
<Text>
SELECT
client.client_id AS "ClientId",
client.title AS "Name",
city.title AS "CityTitle",
client.phone AS "Phone",
client.archive AS "Archive",
COUNT(*) OVER() AS "Count"
FROM
template.client
LEFT JOIN template.city USING(city_id)
WHERE
({Archive} ISNULL OR client.archive = {Archive}) AND
({SearchKeywords} ISNULL OR client.title ilike '%' || {SearchKeywords} ||'%')
ORDER BY client.title
LIMIT {Limit}
OFFSET GREATEST(({Page} - 1) * {Limit}, 0);
</Text>
</SqlQuery>
Запустите проект и проверьте загрузку формы и работу постраничного просмотра списка клиентов.
Самостоятельно
Реализуйте постраничный просмотр на главной форме.
Загрузка файлов
Дальше рассмотрим возможность загрузки файлов на сервер, скачивание и просмотр файлов через клиентское приложение.
В карточке клиента реализуем возможность добавлять фото, которое будем сохранять на сервер.
База данных
В таблицу template.client добавим колонку photo_file_id, которую через внешний ключ привяжем к таблице public.file:
ALTER TABLE template.client
ADD COLUMN photo_file_id bigint;
ALTER TABLE template.client
ADD CONSTRAINT fk_file_client_id FOREIGN KEY (photo_file_id) REFERENCES public.file (file_id) ON UPDATE NO ACTION ON DELETE NO ACTION;
В таблице public.file хранится вся необходимая информация о загруженном файле: имя файла без расширения (name), полный путь до файла на сервере (path), дата загрузки (date) и уникальный guid-идентификатор.
Подготовка формы
Начнем с того, что на форму карточки клиентов добавим графический элемент PictureBox, в котором будет отображаться фото клиента.
Перейдем в файл TemplateClientEdit.xml
Внутри панели ContentPanel создадим две панели: MainPanel, в которую перенесем все имеющиеся поля на форме, и PhotoPanel, в которой будут располагаться объекты для работы с изображением:
Скачайте архив с фото, которое будем использовать в качестве заглушки, и распакуйте его в папку \Template\Projects\1. Template\Forms\Images.
В качестве значения тэга <Image> мы будем передавать ссылку с GUID файла, расположенного на сервере. По этой ссылке объект PictureBox самостоятельно скачает файл с сервера.
В тэгах <NullImage> и <ErrorImage> указан относительный путь до изображения, которое будет отображаться пользователю, если тэг <Image> имеет значение NULL, или изображение загружено с ошибкой.
Скорректируем запрос ClientByIdSelectSqlQuery, добавив в него поля PhotoGuid и PhotoGuidPath. Первое поле будет содержать чистый guid файла, который нам понадобится для скачивания изображения перед открытием на клиентской машине. Второе поле - guid-ссылка на файл, которую будем передавать в PhotoPictureBox.
Template.xml
<SqlQuery Name="ClientByIdSelectSqlQuery">
<Text>
SELECT
client.title AS "Name",
client.city_id AS "CityId",
client.date_of_birth AS "DateOfBirth",
client.email AS "Email",
client.phone AS "Phone",
F.guid AS "PhotoGuid",
'guid://' || F.guid AS "PhotoGuidPath"
FROM
template.client
LEFT JOIN public.file F ON client.photo_file_id = F.file_id
WHERE
client.client_id = {ClientId};
</Text>
</SqlQuery>
Добавим новые поля в ClientPrimaryGetDataConnection и укажем PhotoGuidPath в качестве значения тэга <Image> PhotoPictureBox.
Запустите проект и проверьте расположение объектов на форме.
Редактирование изображения
Изображение будем выбирать с помощью диалогового окна выбора файлов, которое будем открывать по команде типа FileDialogShowCommand. Создадим такую команду:
Добавьте вызов этой команды на кнопку PhotoEditButton.
Обращаясь к параметру FullPath результата выполнения команды FileDialogShowCommand, мы получим полный путь до выбранного файла, который и передадим в наш объект PhotoPictureBox через set-проперти Image:
Создадим условия для проверки наличия пользовательского изображения в поле PhotoPictureBox. Для этого через get-проперти CurrentImageSource будем получать источник отображаемого изображения и сравнивать его со значениями, возвращаемыми get-проперти NullImage и ErrorImage.
Для удобства использования объединили обе проверки в одно условие PhotoPictureBoxIsNotEmptyCondition, которое укажем на кнопке PhotoDeleteButton в тэге <Enabled>.
Создадим команду для удаления пользовательского изображения из PhotoPictureBox.
Серверная часть сохранит файл в хранилище, имя которого указывается в тэге <Storage>. Так как у нас этот тэг отсутствует, то в качестве каталога для сохранения файлов будет использоваться хранилище с именем Default.
Имена хранилищ и пути до их папок указываются в файле настроек серверной части (appsettings.json):
Хранилище Default привязано к папке Upload в каталоге, куда была развернута серверная часть приложения. В поле "Format" задается формат вложенных папок.
При успешном сохранении сервер сгенерирует уникальный guid-идентификатор, который вернется клиенту результатом команды UploadFileCommand.
Необходимая информация о загруженном файле будет сохранена в таблицу public.file.
Скорректируем команду SaveSequentialCommand, добавив вызов команды на передачу файла на сервер:
Добавим в ClientInsertSetDataConnection и ClientUpdateSetDataConnection параметр PhotoGuid, в котором будем передавать guid-идентификатор загруженного файла:
<SqlQuery Name="ClientInsertSqlQuery">
<Text>
INSERT INTO template.client(
city_id,
date_of_birth,
title,
email,
phone,
photo_file_id
)
VALUES (
{CityId},
{DateOfBirth},
{Name},
{Email},
{Phone},
(SELECT F.file_id FROM public.file F WHERE F.guid = {PhotoGuid})
)
RETURNING client_id;
</Text>
</SqlQuery>
ClientUpdateSqlQuery
Template.xml
<SqlQuery Name="ClientUpdateSqlQuery">
<Text>
UPDATE template.client
SET
city_id = {CityId},
date_of_birth = {DateOfBirth},
title = {Name},
email = {Email},
phone = {Phone},
photo_file_id = (SELECT F.file_id FROM public.file F WHERE F.guid = {PhotoGuid})
WHERE
client_id = {ClientId};
</Text>
</SqlQuery>
Самостоятельно
В таком сохранении есть один большой недочет: при сохранении изменений в других полях (например, изменили дату рождения), если у клиента ранее была сохранена фотография, то команда PhotoPictureBoxUploadFileCommand будет выполняться повторно. Это приведет к очередному сохранению файла на сервер, а значит добавлению копии файла и перезаписи photo_file_id у записи в template.client. Придумайте и реализуйте механизм, блокирующий перезапись файла, если файл не изменялся.
Учтите один момент: сейчас в качестве параметра PhotoGuid передается результат команды PhotoPictureBoxUploadFileCommand, если она не будет вызываться, то параметре будет передаваться пустое значение, что приведет к удалению ссылки на фото у записи в template.client.
Просмотр изображения
Теперь нам необходимо реализовать просмотр изображения по двойному клику по объекту PhotoPictureBox. При этом будем различать две ситуации в зависимости от источника изображения:
из каталога на локальной машине;
по guid-ссылке файла на сервере.
Путь до файла на локальной машине
Когда мы добавляем новое изображение в PhotoPictureBox, get-проперти CurrentImageSource возвращает полный путь до файла на локальной машине. В таком случае мы можем открыть изображение с помощью команды типа ApplicationRunCommand, которая будет запускать подходящее приложение относительно переданного в тэг <Application> файла.
Условие PhotoPictureBoxIsNotEmptyCondition, указанное во вложенном тэге <Condition>, ограничит выполнение команды, если в объекте нет пользовательского изображения.
По guid-ссылке файла
Если в PhotoPictureBox передается guid-ссылка на файл, расположенный на сервере, то мы не можем воспользоваться командой ApplicationRunCommand - она не распознает эту ссылку. В этом случае нам нужно скачать файл, воспользовавшись командой типа DownloadFileCommand.
В этом уроке мы рассмотрели паттерн Pagination, применение которого позволяет просматривать большой объем записей, разбив их на страницы. Познакомились с объектом PictureBox для отображения изображений на форме, а также рассмотрели команды UploadFileCommand и DownloadFileCommand для загрузки файлов на сервер и скачивания их на клиентскую машину соответственно.
Это был заключительный урок в базовом блоке. В следующих уроках уделим внимание режимам загрузки данных и многопользовательскому режиму.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.