> For the complete documentation index, see [llms.txt](https://wfsys.gitbook.io/wt-practice/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wfsys.gitbook.io/wt-practice/main/lesson_pagination.md).

# Урок 14. Постраничный просмотр

Бывают списки, в которых со временем может появиться очень много записей, и для удобства работы с ними лучше использовать постраничный просмотр.

Также в этом уроке мы рассмотрим работу с изображениями, передачу файлов на сервер и получение их на форму.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](/wt-practice/main/lesson_self-work_cash.md#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](/wt-practice/main/lesson_list_form.md#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Постраничный просмотр <a href="#pagination" id="pagination"></a>

Рассмотрим паттерн Pagination на примере формы списка клиентов.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FeBOYc5InnxJt44vecKer%2Fimage.png?alt=media&amp;token=2375dd95-ca9a-43f0-8fd1-2ed9cc31dae2" alt=""><figcaption></figcaption></figure>

### Подготовка формы <a href="#preparing-form" id="preparing-form"></a>

Перейдем в файл списка клиентов (TemplateClientList.xml) и в ContentPanel добавим описание панели PagePanel с объектами, необходимыми для постраничного просмотра списка клиентов:

<details>

<summary>PagePanel</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="PagePanel" Type="Panel" Assembly="BaseControls">
  <Top>
    <Object Name="ClientDatabaseTable">
      <Property Name="Bottom" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>Ceiling(({0} - {1})/2)</Expression>
      <Items>
        <Item>
          <Object Name="ContentPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>
          <Object Name="PagePanel">
            <Property Name="Width" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>30</Height>
  <Width>
    <Object Name="TotalItemsLabel">
      <Property Name="Right" />
    </Object>
  </Width>

  <MyObject Name="FirstPageButton" Type="Button" Assembly="BaseControls">
    <Top>5</Top>
    <Left>0</Left>
    <Height>
      <Object Name="CurrentPageTextBox">
        <Property Name="Height" />
      </Object>
    </Height>
    <Width>
      <Object Name="FirstPageButton">
        <Property Name="Height" />
      </Object>
    </Width>
    <FlatStyle>Flat</FlatStyle>
    <FlatBorderSize>0</FlatBorderSize>
    <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
    <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
    <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
    <BackgroundImageLayout>Center</BackgroundImageLayout>
    <BackgroundImage>Images\16x16\arrow-collapse-left.png</BackgroundImage>
    <Commands> </Commands>
  </MyObject>

  <MyObject Name="PrevPageButton" Type="Button" Assembly="BaseControls">
    <Top>
      <Object Name="FirstPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 3</Expression>
        <Items>
          <Item>
            <Object Name="FirstPageButton">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Height>
      <Object Name="FirstPageButton">
        <Property Name="Height" />
      </Object>
    </Height>
    <Width>
      <Object Name="FirstPageButton">
        <Property Name="Width" />
      </Object>
    </Width>
    <FlatStyle>Flat</FlatStyle>
    <FlatBorderSize>0</FlatBorderSize>
    <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
    <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
    <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
    <BackgroundImageLayout>Center</BackgroundImageLayout>
    <BackgroundImage>Images\16x16\arrow-left.png</BackgroundImage>
    <Commands> </Commands>
  </MyObject>

  <MyObject Name="CurrentPageTextBox" Type="TextBox" Assembly="BaseControls" ChangeForm="False">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 3</Expression>
        <Items>
          <Item>
            <Object Name="PrevPageButton">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Width>30</Width>
    <MaxLength>3</MaxLength>
    <AllowedSymbols>0123456789</AllowedSymbols>
    <TextAlign>Right</TextAlign>
    <Text> </Text>
  </MyObject>

  <MyObject Name="TotalPagesLabel" Type="Label" Assembly="BaseControls" ChangeForm="False">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Object Name="CurrentPageTextBox">
        <Property Name="Right" />
      </Object>
    </Left>
    <Height>20</Height>
    <Width>40</Width>
    <TextAlign>MiddleLeft</TextAlign>
    <Text> </Text>
  </MyObject>

  <MyObject Name="NextPageButton" Type="Button" Assembly="BaseControls">
    <Top>
      <Object Name="FirstPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 3</Expression>
        <Items>
          <Item>
            <Object Name="TotalPagesLabel">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Height>
      <Object Name="FirstPageButton">
        <Property Name="Height" />
      </Object>
    </Height>
    <Width>
      <Object Name="FirstPageButton">
        <Property Name="Width" />
      </Object>
    </Width>
    <FlatStyle>Flat</FlatStyle>
    <FlatBorderSize>0</FlatBorderSize>
    <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
    <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
    <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
    <BackgroundImageLayout>Center</BackgroundImageLayout>
    <BackgroundImage>Images\16x16\arrow-right.png</BackgroundImage>
    <Commands> </Commands>
  </MyObject>

  <MyObject Name="LastPageButton" Type="Button" Assembly="BaseControls">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 3</Expression>
        <Items>
          <Item>
            <Object Name="NextPageButton">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Height>
      <Object Name="FirstPageButton">
        <Property Name="Height" />
      </Object>
    </Height>
    <Width>
      <Object Name="FirstPageButton">
        <Property Name="Width" />
      </Object>
    </Width>
    <FlatStyle>Flat</FlatStyle>
    <FlatBorderSize>0</FlatBorderSize>
    <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
    <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
    <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
    <BackgroundImageLayout>Center</BackgroundImageLayout>
    <BackgroundImage>Images\16x16\arrow-collapse-right.png</BackgroundImage>
    <Commands> </Commands>
  </MyObject>

  <MyObject Name="PerPageLabel" Type="Label" Assembly="BaseControls">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 20</Expression>
        <Items>
          <Item>
            <Object Name="LastPageButton">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Height>20</Height>
    <Width>110</Width>
    <TextAlign>MiddleRight</TextAlign>
    <Text>строк на странице</Text>
  </MyObject>

  <MyObject Name="ItemsPerPageComboBox" Type="ComboBox" Assembly="BaseControls" ChangeForm="False">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Object Name="PerPageLabel">
        <Property Name="Right" />
      </Object>
    </Left>
    <Width>50</Width>
    <ValueList>
      <Structure Type="Table">
        <Row>
          <Item />
          <Item>[Все]</Item>
        </Row>
        <Row>
          <Item>5</Item>
          <Item>5</Item>
        </Row>
        <Row>
          <Item>10</Item>
          <Item>10</Item>
        </Row>
        <Row>
          <Item>50</Item>
          <Item>50</Item>
        </Row>
        <Row>
          <Item>100</Item>
          <Item>100</Item>
        </Row>
      </Structure>
    </ValueList>
    <Value>50</Value>
  </MyObject>

  <MyObject Name="TotalItemsLabel" Type="Label" Assembly="BaseControls" ChangeForm="False">
    <Top>
      <Object Name="PrevPageButton">
        <Property Name="Top" />
      </Object>
    </Top>
    <Left>
      <Calculate>
        <Expression>{0} + 3</Expression>
        <Items>
          <Item>
            <Object Name="ItemsPerPageComboBox">
              <Property Name="Right" />
            </Object>
          </Item>
        </Items>
      </Calculate>
    </Left>
    <Height>20</Height>
    <Width>60</Width>
    <TextAlign>MiddleLeft</TextAlign>
    <Text> </Text>
  </MyObject>

</MyObject>
```

{% endcode %}

</details>

Скачайте архив с изображением для кнопок постраничного просмотра и распакуйте его в папку **\Template\Projects\1. Template\Forms\Images\16x16**.

{% file src="/files/f7iI5GFtOFxYCosQnYb7" %}
Иконки для кнопок
{% endfile %}

### Количество записей в ответе от сервера <a href="#number-of-records-in-response" id="number-of-records-in-response"></a>

Первым делом в запрос **ClientSelectSqlQuery** для списка клиентов добавим поле **Count**, в котором будем возвращать общее количество записей в результате запроса:

{% code title="Template.xml" %}

```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>
```

{% endcode %}

Создадим условие для проверки количества строк в **ClientPrimaryGetDataConnection**, используя для этого get-проперти [Count](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/primary_dc#get_count):

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="NumberOfClientsEqual0Condition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
        <Property Name="Count" />
      </DataConnection>
    </Item>
    <Item>0</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Создадим переменную для хранения общего количества записей клиентов в базе данных:

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="NumberOfClientsVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Switch>
      <Case>
        <When>
          <Condition Name="NumberOfClientsEqual0Condition" />
        </When>
        <Then>0</Then>
      </Case>
      <Case>
        <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
          <Fields>
            <Field Name="Count" />
          </Fields>
        </DataConnection>
      </Case>
    </Switch>
  </Value>
</MyObject>
```

{% endcode %}

Таким образом, если сервер не вернул никаких строк, то берем ноль, иначе будем брать значение из поля Count, которое соответствует количеству записей в базе данных.

Создадим условие проверки этой переменной, с помощью которого будем блокировать кнопки переключения по страницам:

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="NumberOfClientsVariableEqual0Condition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="NumberOfClientsVariable" />
    </Item>
    <Item>0</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Для объекта TotalItemsLabel скорректируем значение тэга `<Text>`, указав количество строк которые вернул сервер:

<details>

<summary>TotalItemsLabel</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="TotalItemsLabel" Type="Label" Assembly="BaseControls" ChangeForm="False">
  <Top>
    <Object Name="PrevPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 3</Expression>
      <Items>
        <Item>
          <Object Name="ItemsPerPageComboBox">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>20</Height>
  <Width>80</Width>
  <TextAlign>MiddleLeft</TextAlign>
  <Text>
    <String>
      <Format>из {0}</Format>
      <Items>
        <Item>
          <Object Name="NumberOfClientsVariable" />
        </Item>
      </Items>
    </String>
  </Text>
</MyObject>
```

{% endcode %}

</details>

### Переход между страницами <a href="#jump-between-pages" id="jump-between-pages"></a>

Создадим переменную для хранения номера текущей страницы:

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="CurrentPageVariable" Type="Variable" Assembly="SimpleControls">
  <Value>1</Value>
</MyObject>
```

{% endcode %}

Укажем эту переменную в тэге `<Text>` объекта CurrentPageTextBox.

<details>

<summary>CurrentPageTextBox</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="CurrentPageTextBox" Type="TextBox" Assembly="BaseControls" ChangeForm="False">
  <Top>
    <Object Name="PrevPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 3</Expression>
      <Items>
        <Item>
          <Object Name="PrevPageButton">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Width>30</Width>
  <MaxLength>3</MaxLength>
  <AllowedSymbols>0123456789</AllowedSymbols>
  <TextAlign>Right</TextAlign>
  <Text>
    <Object Name="CurrentPageVariable" />
  </Text>
</MyObject>
```

{% endcode %}

</details>

Создадим переменную для расчета количества страниц:

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="TotalPagesVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Switch>
      <Case>
        <When>
          <Condition Name="ItemsPerPageComboBoxIsNullCondition" />
        </When>
        <Then>1</Then>
      </Case>
      <Case>
        <Calculate>
          <Expression>Ceiling({0}/{1})</Expression>
          <Items>
            <Item>
              <Object Name="NumberOfClientsVariable" />
            </Item>
            <Item>
              <Object Name="ItemsPerPageComboBox" />
            </Item>
          </Items>
        </Calculate>
      </Case>
    </Switch>
  </Value>
</MyObject>
```

{% endcode %}

Где условие **ItemsPerPageComboBoxIsNullCondition** имеет вид:

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="ItemsPerPageComboBoxIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="ItemsPerPageComboBox" />
    </Item>
  </Items>
</Condition>
```

{% endcode %}

Укажем переменную **TotalPagesVariable** в тэге `<Text>` объекта TotalPagesLabel.

<details>

<summary>TotalPagesLabel</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="TotalPagesLabel" Type="Label" Assembly="BaseControls" ChangeForm="False">
  <Top>
    <Object Name="PrevPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Object Name="CurrentPageTextBox">
      <Property Name="Right" />
    </Object>
  </Left>
  <Height>20</Height>
  <Width>40</Width>
  <TextAlign>MiddleLeft</TextAlign>
  <Text>
    <String>
      <Format>/ {0}</Format>
      <Items>
        <Item>
          <Switch>
            <Case>
              <When>
                <Condition Name="NumberOfClientsVariableEqual0Condition" />
              </When>
              <Then>1</Then>
            </Case>
            <Case>
              <Object Name="TotalPagesVariable" />
            </Case>
          </Switch>
        </Item>
      </Items>
    </String>
  </Text>
</MyObject>
```

{% endcode %}

</details>

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

{% code title="TemplateClientList.xml" %}

```xml
<Command Name="CurrentPageVariableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="CurrentPageVariable">
    <Input />
  </Object>
</Command>
```

{% endcode %}

Команда для перехода на предыдущую страницу:

{% code title="TemplateClientList.xml" %}

```xml
<Command Name="CurrentPageVariableDecValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="CurrentPageVariable">
    <Formula>
      <Minus DataType="IntegerDataType">
        <Item>
          <Object Name="CurrentPageVariable" />
        </Item>
        <Item>1</Item>
      </Minus>
    </Formula>
  </Object>
</Command>
```

{% endcode %}

Команда для перехода на следующую страницу:

{% code title="TemplateClientList.xml" %}

```xml
<Command Name="CurrentPageVariableIncValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="CurrentPageVariable">
    <Formula>
      <Plus DataType="IntegerDataType">
        <Item>
          <Object Name="CurrentPageVariable" />
        </Item>
        <Item>1</Item>
      </Plus>
    </Formula>
  </Object>
</Command>
```

{% endcode %}

Условие проверки, является ли текущая страница первой из доступных:

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="CurrentPageVariableEqual1Condition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CurrentPageVariable" />
    </Item>
    <Item>1</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Условие проверки, является ли текущая страница последней из доступных:

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="CurrentPageVariableEqualMaxCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CurrentPageVariable" />
    </Item>
    <Item>
      <Object Name="TotalPagesVariable" />
    </Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Теперь можем скорректировать описание кнопок перехода по страницам:

<details>

<summary>FirstPageButton</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="FirstPageButton" Type="Button" Assembly="BaseControls">
  <Top>5</Top>
  <Left>0</Left>
  <Height>
    <Object Name="CurrentPageTextBox">
      <Property Name="Height" />
    </Object>
  </Height>
  <Width>
    <Object Name="FirstPageButton">
      <Property Name="Height" />
    </Object>
  </Width>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>0</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <BackgroundImage>Images\16x16\arrow-collapse-left.png</BackgroundImage>
  <Commands>
    <Command Name="CurrentPageVariableValueSetCommand">1</Command>
  </Commands>
  <Enabled>
    <And>
      <Not>
        <Condition Name="CurrentPageVariableEqual1Condition" />
      </Not>
      <Not>
        <Condition Name="NumberOfClientsVariableEqual0Condition" />
      </Not>
    </And>
  </Enabled>
</MyObject>
```

{% endcode %}

</details>

<details>

<summary>PrevPageButton</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="PrevPageButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="FirstPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 3</Expression>
      <Items>
        <Item>
          <Object Name="FirstPageButton">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>
    <Object Name="FirstPageButton">
      <Property Name="Height" />
    </Object>
  </Height>
  <Width>
    <Object Name="FirstPageButton">
      <Property Name="Width" />
    </Object>
  </Width>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>0</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <BackgroundImage>Images\16x16\arrow-left.png</BackgroundImage>
  <Commands>
    <Command Name="CurrentPageVariableDecValueSetCommand" />
  </Commands>
  <Enabled>
    <And>
      <Not>
        <Condition Name="CurrentPageVariableEqual1Condition" />
      </Not>
      <Not>
        <Condition Name="NumberOfClientsVariableEqual0Condition" />
      </Not>
    </And>
  </Enabled>
</MyObject>
```

{% endcode %}

</details>

<details>

<summary>NextPageButton</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="NextPageButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="FirstPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 3</Expression>
      <Items>
        <Item>
          <Object Name="TotalPagesLabel">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>
    <Object Name="FirstPageButton">
      <Property Name="Height" />
    </Object>
  </Height>
  <Width>
    <Object Name="FirstPageButton">
      <Property Name="Width" />
    </Object>
  </Width>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>0</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <BackgroundImage>Images\16x16\arrow-right.png</BackgroundImage>
  <Commands>
    <Command Name="CurrentPageVariableIncValueSetCommand" />
  </Commands>
  <Enabled>
    <And>
      <Not>
        <Condition Name="CurrentPageVariableEqualMaxCondition" />
      </Not>
      <Not>
        <Condition Name="NumberOfClientsVariableEqual0Condition" />
      </Not>
    </And>
  </Enabled>
</MyObject>
```

{% endcode %}

</details>

<details>

<summary>LastPageButton</summary>

{% code title="TemplateClientList.xml" %}

```xml
<MyObject Name="LastPageButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="PrevPageButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 3</Expression>
      <Items>
        <Item>
          <Object Name="NextPageButton">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>
    <Object Name="FirstPageButton">
      <Property Name="Height" />
    </Object>
  </Height>
  <Width>
    <Object Name="FirstPageButton">
      <Property Name="Width" />
    </Object>
  </Width>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>0</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <BackgroundImage>Images\16x16\arrow-collapse-right.png</BackgroundImage>
  <Commands>
    <Command Name="CurrentPageVariableValueSetCommand">
      <Object Name="TotalPagesVariable" />
    </Command>
  </Commands>
  <Enabled>
    <And>
      <Not>
        <Condition Name="CurrentPageVariableEqualMaxCondition" />
      </Not>
      <Not>
        <Condition Name="NumberOfClientsVariableEqual0Condition" />
      </Not>
    </And>
  </Enabled>
</MyObject>
```

{% endcode %}

</details>

Но переход между страницами возможен и при ручном вводе значения в поле CurrentPageTextBox. Для этого создадим условие для проверки события нажатия клавиши Enter при редактировании текстового поля:

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="CurrentPageTextBoxKeyEnterPressCondition" Type="KeyDownCondition" Assembly="Conditions">
  <Object Name="CurrentPageTextBox" />
  <Key Value="Enter" />
</Condition>
```

{% endcode %}

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

{% code title="TemplateClientList.xml" %}

```xml
<Condition Name="CurrentPageTextBoxNotLessMinCondition" Type="NotLessCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CurrentPageTextBox" />
    </Item>
    <Item>1</Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>

<Condition Name="CurrentPageTextBoxNotGreaterMaxCondition" Type="NotGreaterCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CurrentPageTextBox" />
    </Item>
    <Item>
      <Object Name="TotalPagesVariable" />
    </Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Создадим Execution, в котором будет отрабатывать событие нажатия клавиши Enter в текстовом поле **CurrentPageTextBox**:

{% code title="TemplateClientList.xml" %}

```xml
<Execution>
  <ConditionExpression>
    <Condition Name="CurrentPageTextBoxKeyEnterPressCondition" />
  </ConditionExpression>
  <Commands>
    <If>
      <When>
        <And>
          <Condition Name="CurrentPageTextBoxNotGreaterMaxCondition" />
          <Condition Name="CurrentPageTextBoxNotLessMinCondition" />
        </And>
      </When>
      <Then>
        <Command Name="CurrentPageVariableValueSetCommand">
          <Object Name="CurrentPageTextBox" />
        </Command>
      </Then>
      <ElseIf>
        <When>
          <Not>
            <Condition Name="CurrentPageTextBoxNotLessMinCondition" />
          </Not>
        </When>
        <Then>
          <Command Name="CurrentPageVariableValueSetCommand">1</Command>
        </Then>
      </ElseIf>
      <ElseIf>
        <When>
          <Not>
            <Condition Name="CurrentPageTextBoxNotGreaterMaxCondition" />
          </Not>
        </When>
        <Then>
          <Command Name="CurrentPageVariableValueSetCommand">
            <Object Name="TotalPagesVariable" />
          </Command>
        </Then>
      </ElseIf>
    </If>
  </Commands>
</Execution>
```

{% endcode %}

### Запрос на просмотр  <a href="#sqlquery-to-view" id="sqlquery-to-view"></a>

В **ClientPrimaryGetDataConnection** добавим параметры **Limit** (количество записей на странице) и **Page** (номер текущей страницы).

Так же необходимо переделать фильтрацию архивных и актуальных записей. Для этого из колонки **Archive** в таблице **ClientDatabaseTable** удалим тэг `<Filter>`, и добавим в **ClientPrimaryGetDataConnection** одноименный параметр.

{% code title="TemplateClientList.xml" %}

```xml
<DataConnection Name="ClientPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="ClientSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="ClientId" />
      <Field Name="Name" />
      <Field Name="CityTitle" />
      <Field Name="Phone" />
      <Field Name="Archive" />
      <Field Name="Count" />
    </Fields>
    <Parameters>
      <Parameter NativeName="Archive">
        <Value>
          <Object Name="ArchiveFilterComboBox" />
        </Value>
      </Parameter>
      <Parameter NativeName="SearchKeywords" RefreshQuery="False">
        <Value>
          <Object Name="SearchKeywordsVariable" />
        </Value>
      </Parameter>
      <Parameter NativeName="Limit">
        <Value>
          <Object Name="ItemsPerPageComboBox" />
        </Value>
      </Parameter>
      <Parameter NativeName="Page">
        <Value>
          <Object Name="CurrentPageVariable" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>
```

{% endcode %}

В запрос **ClientSelectSqlQuery** добавим переменные для новых параметров:

{% code title="Template.xml" %}

```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>
```

{% endcode %}

Запустите проект и проверьте загрузку формы и работу постраничного просмотра списка клиентов.

### Самостоятельно <a href="#self-work-pagination" id="self-work-pagination"></a>

Реализуйте постраничный просмотр на главной форме.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FqNeR1K6kx8WQ64gfKf04%2Fimage.png?alt=media&amp;token=861a22e4-ece9-49fb-9617-58872bee92a0" alt=""><figcaption></figcaption></figure>

## Загрузка файлов <a href="#uploading-files" id="uploading-files"></a>

Дальше рассмотрим возможность загрузки файлов на сервер, скачивание и просмотр файлов через клиентское приложение.

В карточке клиента реализуем возможность добавлять фото, которое будем сохранять на сервер.&#x20;

### База данных <a href="#database" id="database"></a>

В таблицу **template.client** добавим колонку **photo\_file\_id**, которую через внешний ключ привяжем к таблице **public.file**:

```sql
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](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox), в котором будет отображаться фото клиента.

Перейдем в файл TemplateClientEdit.xml&#x20;

Внутри панели **ContentPanel** создадим две панели: **MainPanel**, в которую перенесем все имеющиеся поля на форме, и **PhotoPanel**, в которой будут располагаться объекты для работы с изображением:

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2Fn5Na2Otig5lcilp0dzm8%2Fimage.png?alt=media&amp;token=b9379fa8-b92d-4064-a4fa-431e4cd35ec3" alt=""><figcaption></figcaption></figure>

Скачайте архив с фото, которое будем использовать в качестве заглушки, и распакуйте его в папку **\Template\Projects\1. Template\Forms\Images**.

{% file src="/files/hl7ZLnz2f9BoSVCib2is" %}

Добавим в PhotoPanel объект типа PictureBox:

{% code title="TemplateClientEdit.xml" %}

```xml
<MyObject Name="PhotoPictureBox" Type="PictureBox" Assembly="BaseControls">
  <Change User="True" Source="False" ValueSet="False" />
  <Top>5</Top>
  <Left>10</Left>
  <Height>
    <Calculate>
      <Expression>{0} - {1}*2 - 5</Expression>
      <Items>
        <Item>
          <Object Name="PhotoEditButton">
            <Property Name="Top" />
          </Object>
        </Item>
        <Item>
          <Object Name="PhotoPictureBox">
            <Property Name="Top" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Height>
  <Width>
    <Calculate>
      <Expression>{0} - {1}*2</Expression>
      <Items>
        <Item>
          <Object Name="PhotoPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>
          <Object Name="PhotoPictureBox">
            <Property Name="Left" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Width>
  <TabIndex>1</TabIndex>
  <Image> </Image>
  <SizeMode>Zoom</SizeMode>
  <NullImage>Images\no_photo.png</NullImage>
  <ErrorImage>Images\no_photo.png</ErrorImage>
</MyObject>
```

{% endcode %}

В качестве значения тэга `<Image>` мы будем передавать ссылку с GUID файла, расположенного на сервере. По этой ссылке объект PictureBox самостоятельно скачает файл с сервера.

В тэгах `<NullImage>` и `<ErrorImage>` указан относительный путь до изображения, которое будет отображаться пользователю, если тэг `<Image>` имеет значение NULL, или изображение загружено с ошибкой.

Скорректируем запрос **ClientByIdSelectSqlQuery**, добавив в него поля **PhotoGuid** и **PhotoGuidPath**. Первое поле будет содержать чистый guid файла, который нам понадобится для скачивания изображения перед открытием на клиентской машине. Второе поле - guid-ссылка на файл, которую будем передавать в PhotoPictureBox.

<pre class="language-xml" data-title="Template.xml"><code class="lang-xml">&#x3C;SqlQuery Name="ClientByIdSelectSqlQuery">
  &#x3C;Text>
<strong>    SELECT
</strong>      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};
  &#x3C;/Text>
&#x3C;/SqlQuery>
</code></pre>

Добавим новые поля в **ClientPrimaryGetDataConnection** и укажем PhotoGuidPath в качестве значения тэга `<Image>` PhotoPictureBox.

Добавим кнопки добавления и удаления изображения:

{% code title="TemplateClientEdit.xml" %}

```xml
<MyObject Name="PhotoEditButton" Type="Button" Assembly="BaseControls">
  <Bottom>
    <Calculate>
      <Expression>{0} - 10</Expression>
      <Items>
        <Item>
          <Object Name="PhotoPanel">
            <Property Name="Height" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Bottom>
  <Left>10</Left>
  <Height>22</Height>
  <Width>
    <Calculate>
      <Expression>Ceiling(({0} - {1}*2 - 5)/2)</Expression>
      <Items>
        <Item>
          <Object Name="PhotoPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>
          <Object Name="PhotoEditButton">
            <Property Name="Left" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Width>
  <TabIndex>2</TabIndex>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>1</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImage>Images\16x16\pencil.png</BackgroundImage>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <Hint>Редактировать фотографию</Hint>
  <Commands> </Commands>
</MyObject>

<MyObject Name="PhotoDeleteButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="PhotoEditButton">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Calculate>
      <Expression>{0} + 5</Expression>
      <Items>
        <Item>
          <Object Name="PhotoEditButton">
            <Property Name="Right" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Left>
  <Height>
    <Object Name="PhotoEditButton">
      <Property Name="Height" />
    </Object>
  </Height>
  <Width>
    <Object Name="PhotoEditButton">
      <Property Name="Width" />
    </Object>
  </Width>
  <TabIndex>3</TabIndex>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>1</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <BackgroundImage>Images\16x16\close.png</BackgroundImage>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <Hint>Удалить фотографию</Hint>
  <Commands> </Commands>
</MyObject>
```

{% endcode %}

Запустите проект и проверьте расположение объектов на форме.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FOBwbCWiy44Jb4PpyooVT%2Fimage.png?alt=media&amp;token=8d64caea-c04d-41e6-9516-79a427f50e45" alt=""><figcaption></figcaption></figure>

### Редактирование изображения <a href="#editing-image" id="editing-image"></a>

Изображение будем выбирать с помощью диалогового окна выбора файлов, которое будем открывать по команде типа [FileDialogShowCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/file_dialog_show_command). Создадим такую команду:

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoFileDialogShowCommand" Type="FileDialogShowCommand" Assembly="Commands">
  <Title>Выбор фотографии клиента</Title>
  <Type Value="Open" />
  <Filter>Photo Files|*.jpeg;*.jpg;*.png</Filter>
</Command>
```

{% endcode %}

Добавьте вызов этой команды на кнопку PhotoEditButton.

Обращаясь к параметру **FullPath** результата выполнения команды **FileDialogShowCommand**, мы получим полный путь до выбранного файла, который и передадим в наш объект **PhotoPictureBox** через set-проперти [Image](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox#set_image):

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoPictureBoxPhotoValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="PhotoPictureBox">
    <Property Name="Image">
      <Command Name="PhotoFileDialogShowCommand" Parameter="FullPath" />
    </Property>
  </Object>
</Command>
```

{% endcode %}

{% hint style="info" %}
Команда FileDialogShowCommand возвращает словарь со следующими элементами:

* OK - признак того, что в диалоговом окне пользователем был выбран хотя бы 1 файл;
* Path - массив путей до папок, в которых расположены выбранные файлы, или путь до папки, в котором расположен 1 выбранный файл;
* FileName - массив имен выбранных файлов (только имена, без расширения) или имя 1 выбранного файла;
* FileNameExtension - массив расширений выбранных файлов (с точкой в начале) или расширение 1 выбранного файла;
* FullFileName - массив полных имен выбранных файлов (имя и расширение) или полное имя 1 выбранного файла;
* FullPath - массив полных путей до выбранных файлов (путь и полное имя) или полный путь до 1 выбранного файла.
  {% endhint %}

Создадим Execution для обработки результата выполнения команды выбора файла:

{% code title="TemplateClientEdit.xml" %}

```xml
<Execution>
  <ConditionExpression>
    <Command Name="PhotoFileDialogShowCommand" Parameter="OK" />
  </ConditionExpression>
  <Commands>
    <Command Name="PhotoPictureBoxPhotoValueSetCommand" />
    <Command Name="FormChangedValueSetCommand">True</Command>
  </Commands>
</Execution>
```

{% endcode %}

<details>

<summary>FormChangedValueSetCommand</summary>

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="FormChangedValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Form>
    <Property Name="FormChanged">
      <Input />
    </Property>
  </Form>
</Command>
```

{% endcode %}

</details>

### Удаление изображения <a href="#deleting-image" id="deleting-image"></a>

Создадим условия для проверки наличия пользовательского изображения в поле PhotoPictureBox. Для этого через get-проперти [CurrentImageSource](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox#get_current_image_source) будем получать источник отображаемого изображения и сравнивать его со значениями, возвращаемыми get-проперти [NullImage](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox#get_null_image) и [ErrorImage](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox#get_error_image).

{% code title="TemplateClientEdit.xml" %}

```xml
<Condition Name="PhotoPictureBoxIsNullOrEmptyCondition" Type="IsNullOrEmptyCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="CurrentImageSource" />
      </Object>
    </Item>
  </Items>
</Condition>

<Condition Name="PhotoPictureBoxIsNotNullImageCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="CurrentImageSource" />
      </Object>
    </Item>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="NullImage" />
      </Object>
    </Item>
  </Items>
</Condition>

<Condition Name="PhotoPictureBoxIsNotErrorImageCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="CurrentImageSource" />
      </Object>
    </Item>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="ErrorImage" />
      </Object>
    </Item>
  </Items>
</Condition>

<Condition Name="PhotoPictureBoxIsNotEmptyCondition" Type="NestedCondition" Assembly="Conditions">
  <ConditionExpression>
    <And>
      <Not>
        <Condition Name="PhotoPictureBoxIsNullOrEmptyCondition" />
      </Not>
      <Condition Name="PhotoPictureBoxIsNotNullImageCondition" />
      <Condition Name="PhotoPictureBoxIsNotErrorImageCondition" />
    </And>
  </ConditionExpression>
</Condition>
```

{% endcode %}

Для удобства использования объединили обе проверки в одно условие PhotoPictureBoxIsNotEmptyCondition, которое укажем на кнопке PhotoDeleteButton в тэге `<Enabled>`.

Создадим команду для удаления пользовательского изображения из PhotoPictureBox.&#x20;

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoPictureBoxNullValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="PhotoPictureBox">
    <Property Name="Image">
      <Object Name="PhotoPictureBox">
        <Property Name="NullImage" />
      </Object>
    </Property>
  </Object>
</Command>
```

{% endcode %}

Добавьте вызов этой команды на кнопку PhotoDeleteButton.

{% hint style="info" %}
Для удаления файла с сервера используйте команду [DeleteFileCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/delete_file_command).
{% endhint %}

### Сохранение изображения <a href="#saving-image" id="saving-image"></a>

Для отправки файла на сервер создадим команда типа [UploadFileCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/upload_file_command):

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoPictureBoxUploadFileCommand" Type="UploadFileCommand" Assembly="Commands">
  <FileName>
    <Object Name="PhotoPictureBox">
      <Property Name="CurrentImageSource" />
    </Object>
  </FileName>
</Command>
```

{% endcode %}

Серверная часть сохранит файл в хранилище, имя которого указывается в тэге [`<Storage>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/upload_file_command#storage). Так как у нас этот тэг отсутствует, то в качестве каталога для сохранения файлов будет использоваться хранилище с именем **Default**.&#x20;

{% hint style="info" %}
Имена хранилищ и пути до их папок указываются в файле настроек серверной части ([appsettings.json](https://wfsys.gitbook.io/wt-practice/beginning/project/configuration_files/appsettings.json)):

```json
"Storages": {
  "Default": {
    "Path": "Upload",
    "Format": "yyyy\\\\MM\\\\dd"
  }
}
```

Хранилище **Default** привязано к папке **Upload** в каталоге, куда была развернута серверная часть приложения. В поле "Format" задается формат вложенных папок.
{% endhint %}

При успешном сохранении сервер сгенерирует уникальный guid-идентификатор, который вернется клиенту результатом команды UploadFileCommand.

Необходимая информация о загруженном файле будет сохранена в таблицу **public.file**.

Скорректируем команду SaveSequentialCommand, добавив вызов команды на передачу файла на сервер:

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <If>
      <When>
        <Condition Name="PhotoPictureBoxIsNotEmptyCondition" />
      </When>
      <Then>
        <Command Name="PhotoPictureBoxUploadFileCommand" />
      </Then>
    </If>
    <If>
      <When>
        <Parameter Name="Edit" />
      </When>
      <Then>
        <Command Name="ClientUpdateSaveCommand" />
      </Then>
      <Else>
        <Command Name="ClientInsertSaveCommand" />
        <Command Name="ClientIdValueSetCommand" />
      </Else>
    </If>
    <Command Name="UpdatedTrueValueSetCommand" />
    <Command Name="FormCloseCommand" />
  </Commands>
</Command>
```

{% endcode %}

Добавим в ClientInsertSetDataConnection и ClientUpdateSetDataConnection параметр PhotoGuid, в котором будем передавать guid-идентификатор загруженного файла:

```xml
<Parameter NativeName="PhotoGuid">
  <Value>
    <Switch>
      <Case>
        <When>
          <Condition Name="PhotoPictureBoxIsNotEmptyCondition" />
        </When>
        <Then>
          <Command Name="PhotoPictureBoxUploadFileCommand" />
        </Then>
      </Case>
    </Switch>
  </Value>
</Parameter>
```

Скорректируем запросы:

<details>

<summary>ClientInsertSqlQuery</summary>

{% code title="Template.xml" %}

```xml
<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>
```

{% endcode %}

</details>

<details>

<summary>ClientUpdateSqlQuery</summary>

{% code title="Template.xml" %}

```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>
```

{% endcode %}

</details>

### Самостоятельно <a href="#self-work-save-file" id="self-work-save-file"></a>

В таком сохранении есть один большой недочет: при сохранении изменений в других полях (например, изменили дату рождения), если у клиента ранее была сохранена фотография, то команда PhotoPictureBoxUploadFileCommand будет выполняться повторно. Это приведет к очередному сохранению файла на сервер, а значит добавлению копии файла и перезаписи *photo\_file\_id* у записи в *template.client*. Придумайте и реализуйте механизм, блокирующий перезапись файла, если файл не изменялся.

Учтите один момент: сейчас в качестве параметра PhotoGuid передается результат команды PhotoPictureBoxUploadFileCommand, если она не будет вызываться, то параметре будет передаваться пустое значение, что приведет к удалению ссылки на фото у записи в *template.client*.

### Просмотр изображения <a href="#view-image" id="view-image"></a>

Теперь нам необходимо реализовать просмотр изображения по двойному клику по объекту PhotoPictureBox. При этом будем различать две ситуации в зависимости от источника изображения:

* из каталога на локальной машине;
* по guid-ссылке файла на сервере.

#### Путь до файла на локальной машине <a href="#path-to-file-on-local-machine" id="path-to-file-on-local-machine"></a>

Когда мы добавляем новое изображение в PhotoPictureBox, get-проперти [CurrentImageSource](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox#get_current_image_source) возвращает полный путь до файла на локальной машине. В таком случае мы можем открыть изображение с помощью команды типа [ApplicationRunCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/application_run_command), которая будет запускать подходящее приложение относительно переданного в тэг `<Application>` файла.

Создадим такую команду:

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoApplicationRunCommand" Type="ApplicationRunCommand" Assembly="Commands">
  <Condition Name="PhotoPictureBoxIsNotEmptyCondition" />
  <Application>
    <Object Name="PhotoPictureBox">
      <Property Name="CurrentImageSource" />
    </Object>
  </Application>
</Command>
```

{% endcode %}

Условие **PhotoPictureBoxIsNotEmptyCondition**, указанное во вложенном тэге `<Condition>`, ограничит выполнение команды, если в объекте нет пользовательского изображения.

#### По guid-ссылке файла <a href="#by-file-guid" id="by-file-guid"></a>

Если в PhotoPictureBox передается guid-ссылка на файл, расположенный на сервере, то мы не можем воспользоваться командой ApplicationRunCommand - она не распознает эту ссылку. В этом случае нам нужно скачать файл, воспользовавшись командой типа [DownloadFileCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/download_file_command).

Создадим такую команду:

{% code title="TemplateClientEdit.xml" %}

```xml
<Command Name="PhotoDownloadFileCommand" Type="DownloadFileCommand" Assembly="Commands">
  <Condition Name="PhotoPictureBoxIsNotEmptyCondition" />
  <FileGuid>
    <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
      <Fields>
        <Field Name="PhotoGuid"/>
      </Fields>
    </DataConnection>
  </FileGuid>
  <AllowOverwrite>True</AllowOverwrite>
</Command>
```

{% endcode %}

После скачивания файла с сервера команда сама откроет этот файл в подходящем приложении.

#### Реализация <a href="#implementation" id="implementation"></a>

Создадим условие типа [StartsWithCondition](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/conditions/comparison_condition/starts_with_condition), чтобы различать guid-ссылки на файла:

{% code title="TemplateClientEdit.xml" %}

```xml
<Condition Name="CurrentImageSourceIsGuidStartsWithCondition" Type="StartsWithCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhotoPictureBox">
        <Property Name="CurrentImageSource" />
      </Object>
    </Item>
    <Item>guid://</Item>
  </Items>
</Condition>
```

{% endcode %}

Создадим Execution для обработки двойного клика по PhotoPictureBox для просмотра изображения в стороннем приложении:

{% code title="TemplateClientEdit.xml" %}

```xml
<Execution>
  <ConditionExpression>
    <Condition Name="PhotoPictureBoxDoubleClickCondition" />
  </ConditionExpression>
  <Commands>
    <If>
      <When>
        <Condition Name="CurrentImageSourceIsGuidStartsWithCondition" />
      </When>
      <Then>
        <Command Name="PhotoDownloadFileCommand" />
      </Then>
      <Else>
        <Command Name="PhotoApplicationRunCommand" />
      </Else>
    </If>
  </Commands>
</Execution>
```

{% endcode %}

## Итоги <a href="#results" id="results"></a>

В этом уроке мы рассмотрели паттерн Pagination, применение которого позволяет просматривать большой объем записей, разбив их на страницы. Познакомились с объектом  [PictureBox](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/picturebox) для отображения изображений на форме, а также рассмотрели команды  [UploadFileCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/upload_file_command) и [DownloadFileCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/download_file_command) для загрузки файлов на сервер и скачивания их на клиентскую машину соответственно.

Это был заключительный урок в базовом блоке. В следующих уроках уделим внимание режимам загрузки данных и многопользовательскому режиму.

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson14-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson14-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson14-answer.zip</a></td></tr></tbody></table>
