Урок 4. Паттерн onClose

На прошлом уроке мы расширили приложение, добавив форму списка клиентов и карточку редактирования клиента. Список клиентов стал главной формой, а доступ к форме списка городов осуществляется через карточку клиента.

На этом уроке рассмотрим подход к улучшению опыта взаимодействия пользователя с формами для редактирования данных, а именно, паттерн onClose для проверки изменений на форме и уведомления о них пользователя при закрытии формы без сохранения.

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

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

Паттерн onClose

Суть паттерна заключается в том, чтобы при закрытии формы без сохранения проверять, были ли изменения на форме, и предупреждать о них пользователя. Тем самым исключать неприятную ситуацию потери данных, когда пользователь случайно закрыл форму, не сохранив изменения. Вопрос, который будем задавать пользователю, может выглядеть следующим образом:

По кнопке "Да" мы будем сохранять изменения перед тем, как закрыть форму. По кнопке "Нет" будет закрывать форму без сохранения. А по кнопке "Отмена" будем оставаться на форме и ждать дальнейших действий пользователя.

Перейдем в файл карточки клиента (TemplateClientEdit.xml). На ее примере будем рассматривать паттерн.

Проверка изменений на форме

Для отслеживания изменений у формы есть get-проперти FormChanged, которое возвращает значение True, если изменилось значение хотя бы одного объекта.

Также есть set-проперти FormChanged, с его помощью можно вручную сбрасывать признак изменений на форме, но не сами изменения.

<Command Name="ResetFormChangedValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Form>
    <Property Name="FormChanged">False</Property>
  </Form>
</Command>

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

TemplateClientEdit.xml
<Condition Name="FormChangedTrueEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Form>
        <Property Name="FormChanged" />
      </Form>
    </Item>
    <Item>True</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Значение свойства формы FormChanged определяется совокупностью get-проперти ValueChanged всех объектов на форме.

Свойство ValueChanged обозначает, было ли изменено значение объекта в процессе работы.

Чтобы форма исключала объект из проверки для определения значения свойства FormChanged, необходимо при описании объекта в тэге <MyObject> указать необязательный атрибут ChangeForm со значением False.

Также у тэга <MyObject> есть необязательный вложенный тэг <Change>, который задает настройки изменения свойства ValueChanged:

<Change User="True" Source="False" ValueSet="True" />

  • Атрибут User относится к графическому интерфейсу и определяет, будет ли свойство ValueChanged иметь значение True, если пользователь изменит значение объекта;

  • Атрибут Source определяет, какое значение будет присвоено свойству ValueChanged, если значение объекта обновится из источника. Если атрибут Source имеет значение False, и при этом значение из источника обновится, то ValueChanged присваивается значение False;

  • Атрибут ValueSet также определяет, какое значение будет присвоено свойству ValueChanged, если значение объекта будет присвоено из команды ValueSetCommand. Если атрибут ValueSet имеет значение False, и при этом значение было присвоено из команды ValueSetCommand, то свойство ValueChanged будет иметь значение False.

У формы есть get-проперти ChangedObjects, которое возвращает список измененных объектов в виде строки:

<Form>
  <Property Name="ChangedObjects" />
</Form>

С помощью этого get-проперти можно проверить, какие объекты формы изменились и повлияли на результат get-проперти формы FormChanged.

Для вывода сообщений пользователю используется команда MessageBoxCommand, которая открывает диалоговое окно сообщения с набором кнопок. Давайте создадим такую команду:

TemplateClientEdit.xml
<Command Name="SaveOnCloseMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Сохранение</Caption>
  <Text>Форма содержит несохраненные изменения.\rСохранить их перед закрытием?</Text>
  <Icon Type="Question" />
  <Buttons Type="YesNoCancel" />
</Command>

В атрибуте Type тэга <Buttons> указываем нужный нам набор кнопок.

Теперь нам нужен Execution, который по условию будет вызывать команду SaveOnCloseMessageBoxCommand.

На форме уже есть Execution, который обрабатывает событие закрытия формы:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <Or>
      <Condition Name="FormClosingCondition" />
      <Condition Name="EscapeKeyDownCondition" />
    </Or>
  </ConditionExpression>
  <Commands>
    <Command Name="FormCloseCommand" />
  </Commands>
</Execution>

В нем обрабатывается не только закрытие по крестику, которое отслеживает условие FormClosingCondition. Также отслеживается нажатие клавиши Esc. За это отвечает условие EscapeKeyDownCondition типа KeyDownCondition.

Давайте переделаем этот Execution так, чтобы он учитывал наличие изменений на форме и вызывал команду с диалоговым окном:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <And>
      <Or>
        <Condition Name="FormClosingCondition" />
        <Condition Name="EscapeKeyDownCondition" />
      </Or>
      <Condition Name="FormChangedTrueEqualCondition" />
    </And>
  </ConditionExpression>
  <Commands>
    <Command Name="SaveOnCloseMessageBoxCommand" />
  </Commands>
</Execution>

Обработка ответа пользователя на сообщение

Создадим условия, которые будут проверять, какую кнопку в диалоговом окне нажал пользователь. Команда MessageBoxCommand возвращает значение нажатой кнопки:

  • Yes - нажали кнопку "Да";

  • No - нажали кнопку "Нет";

  • Cancel - нажали кнопку "Отмена".

Достаточно создать два условия для кнопок "Да" и "Нет":

TemplateClientEdit.xml
<Condition Name="SaveOnCloseMessageBoxCommandYesEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <Items>
    <Item>
      <Command Name="SaveOnCloseMessageBoxCommand" />
    </Item>
    <Item>Yes</Item>
  </Items>
</Condition>

<Condition Name="SaveOnCloseMessageBoxCommandNoEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <Items>
    <Item>
      <Command Name="SaveOnCloseMessageBoxCommand" />
    </Item>
    <Item>No</Item>
  </Items>
</Condition>

Создадим Execution для обработки нажатия кнопки "Да" диалогового окна:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <Condition Name="SaveOnCloseMessageBoxCommandYesEqualCondition" />
  </ConditionExpression>
  <Commands>
    <Command Name="SaveSequentialCommand" />
  </Commands>
</Execution>

А для обработки нажатия кнопки "Нет" диалогового окна создадим Execution, который помимо этого условия будет отрабатывать закрытие формы без изменений:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <Or>
      <And>
        <Or>
          <Condition Name="FormClosingCondition" />
          <Condition Name="EscapeKeyDownCondition" />
        </Or>
        <Not>
          <Condition Name="FormChangedTrueEqualCondition" />
        </Not>
      </And>
      <Condition Name="SaveOnCloseMessageBoxCommandNoEqualCondition" />
    </Or>
  </ConditionExpression>
  <Commands>
    <Command Name="FormCloseCommand" />
  </Commands>
</Execution>

Запустите приложение. Убедитесь, что формы успешно загружены. Проверьте только что добавленный функционал.

Стоит отметить, что такой подход отлично работает, когда на форме нет полей, обязательных для заполнения, и полей, для которых важна корректность введенных данных. Если такие поля есть и заполнены некорректно, то при нажатии на кнопку "Да" диалогового окна в базу запишутся некорректные данные. Следовательно, при закрытии формы без сохранения мы должны проверять еще и корректность введенных данных.

Обязательные поля

Так как сущность "Клиент" будет одной из основных в нашем приложении, и клиент, что логично, не может быть без имени, поле "Имя (ФИО)" необходимо сделать обязательным для заполнения. Также сделаем обязательным поле "Город", так как дальнейшая логика будет привязываться к городам клиентов.

Дополнительной гарантией корректности данных будут ограничения на колонки в таблице template.client в базе данных. Перейдем в программу для управления СУБД PostgreSQL, и для нашей базы данных template_project выполним запросы, представленные ниже. Перед этим очистите таблицу от данных, либо заполните эти колонки во всех строках какой-либо информацией, чтобы не возникло ошибок с заданием ограничений на колонки.

ALTER TABLE template.client
   ALTER COLUMN city_id SET NOT NULL;
ALTER TABLE template.client
   ALTER COLUMN title SET NOT NULL;

Проверка обязательных полей

Для проверки текстового поля NameTextBox понадобится условие типа IsNullOrEmptyCondition:

TemplateClientEdit.xml
<Condition Name="NameTextBoxIsNullOrEmptyCondition" Type="IsNullOrEmptyCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="NameTextBox"/>
    </Item>
  </Items>
</Condition>

А для проверки выпадающего списка создадим условие типа IsNullCondition:

TemplateClientEdit.xml
<Condition Name="CityComboBoxIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="CityComboBox"/>
    </Item>
  </Items>
</Condition>

Чтобы сообщить пользователю, что поля обязательны для заполнения, воспользуемся конструкцией Checking. Для этого перед тэгом <Executions> разместим тэг <Checkings>, в который расположим следующий код:

TemplateClientEdit.xml
<Checking>
  <Object Name="NameTextBox" />
  <ConditionExpression>
    <Condition Name="NameTextBoxIsNullOrEmptyCondition" />
  </ConditionExpression>
  <AsteriskHint>Пожалуйста, заполните это поле</AsteriskHint>
</Checking>

<Checking>
  <Object Name="CityComboBox" />
  <ConditionExpression>
    <Condition Name="CityComboBoxIsNullCondition" />
  </ConditionExpression>
  <AsteriskHint>Пожалуйста, выберите элемент в этом списке</AsteriskHint>
</Checking>

Назначение конструкции <Checking>в том, что при выполнении условия, описанного в тэге <ConditionExpression>, слева от объекта, указанного в тэге <Object>, будет отображаться красная полоска. При наведении курсора мыши на полоску будет всплывать подсказка с текстом из тэга <AsteriskHint>.

Предупреждение пользователя

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

Чтобы проверить, все ли поля заполнены, достаточно узнать, сработал ли хотя бы один Checking. Для этого у формы есть свойство CheckingFired. Создадим EqualCondition, в котором проверим это свойство. И если оно равно False, то все поля корректно заполнены.

TemplateClientEdit.xml
<Condition Name="MandatoryFieldsAreFilledEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Form>
        <Property Name="CheckingFired" />
      </Form>
    </Item>
    <Item>False</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Это условие можно просто указать в тэге <Enabled> кнопки SaveButton, чтобы она стала неактивной. Так мы поступали с кнопками редактирования и удаления записей на формах списков клиентов и городов. Но такой подход не позволяет пользователю узнать, где он допустил ошибку, и почему кнопка неактивна.

Можно создать команду MessageBoxCommand, которая предупредит пользователя, если он попытается сохранить некорректные данные. Для этого в тэге <Commands> кнопки SaveButton нужно создать блок <If>, который определит, какая команда будет выполняться: либо команда сохранения данных, либо команда отображения сообщения о некорректном заполнении полей.

Но есть более изящный способ. У кнопки Button есть режим DisabledMode, который включается через тэг <DisabledMode>. Кнопка всегда остается активной, даже если Enabled равен False. При нажатии на "неактивную" кнопку (Enabled = False) будет показываться сообщение с текстом из тэга <DisabledText>.

Скорректируем описание кнопки "Сохранить" с использованием DisabledMode. Для этого в тэге <Enabled> пропишем условие MandatoryFieldsAreFilledEqualCondition и добавим тэги <DisabledMode> и <DisabledText>.

Таким образом, общий синтаксис кнопки SaveButton будет выглядеть так:

TemplateClientEdit.xml
<MyObject Name="SaveButton" Type="Button" Assembly="BaseControls">
  <Top>5</Top>
  <Right>
    <Formula>
      <Minus DataType="IntegerDataType">
        <Item>
          <Object Name="FootPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>5</Item>
      </Minus>
    </Formula>
  </Right>
  <Height>30</Height>
  <Width>200</Width>
  <TextAlign>MiddleCenter</TextAlign>
  <FontStyle>ButtonFontStyle</FontStyle>
  <ImageAlign>MiddleLeft</ImageAlign>
  <Image>Images\24x24\content-save.png</Image>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>1</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <Text>Сохранить</Text>
  <TabIndex>1</TabIndex>
  <Enabled>
    <Condition Name="MandatoryFieldsAreFilledEqualCondition" />
  </Enabled>
  <DisabledMode>True</DisabledMode>
  <DisabledText>Одно или несколько полей заполнены некорректно.</DisabledText>
  <Commands>
    <Command Name="SaveSequentialCommand" />
  </Commands>
</MyObject>

Запустите приложение. Убедитесь, что формы успешно загружены. Проверьте только что добавленный функционал.

Паттерн onClose и обязательные поля

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

По кнопке "Да" мы будем закрыть форму без сохранения изменений. По кнопке "Нет" будем оставаться на форме и ждать дальнейших действий пользователя.

Давайте опишем команду MessageBoxCommand, которая будет отображать это диалоговое окно:

TemplateClientEdit.xml
<Command Name="CloseOnCloseMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Закрытие</Caption>
  <Text>При закрытии все несохраненные изменения будут утеряны.\rВы уверены, что хотите закрыть форму?</Text>
  <Icon Type="Question" />
  <Buttons Type="YesNo" />
</Command>

Создадим условие проверки нажатия кнопки "Да" диалогового окна:

TemplateClientEdit.xml
<Condition Name="CloseOnCloseMessageBoxCommandYesEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <Items>
    <Item>
      <Command Name="CloseOnCloseMessageBoxCommand" />
    </Item>
    <Item>Yes</Item>
  </Items>
</Condition>

Скорректируем наш первый Execution следующим образом:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <And>
      <Or>
        <Condition Name="FormClosingCondition" />
        <Condition Name="EscapeKeyDownCondition" />
      </Or>
      <Condition Name="FormChangedTrueEqualCondition" />
    </And>
  </ConditionExpression>
  <Commands> 
    <If>
      <When>
        <Condition Name="MandatoryFieldsAreFilledEqualCondition" />
      </When>
      <Then>
        <Command Name="SaveOnCloseMessageBoxCommand" />
      </Then>
      <Else>
        <Command Name="CloseOnCloseMessageBoxCommand" />
      </Else>
    </If>
  </Commands>
</Execution>

А в Execution на закрытие формы добавим условие CloseOnCloseMessageBoxCommandYesEqualCondition:

TemplateClientEdit.xml
<Execution>
  <ConditionExpression>
    <Or>
      <And>
        <Or>
          <Condition Name="FormClosingCondition" />
          <Condition Name="EscapeKeyDownCondition" />
        </Or>
        <Not>
          <Condition Name="FormChangedTrueEqualCondition" />
        </Not>
      </And>
      <Condition Name="SaveOnCloseMessageBoxCommandNoEqualCondition" />
      <Condition Name="CloseOnCloseMessageBoxCommandYesEqualCondition" />
    </Or>
  </ConditionExpression>
  <Commands>
    <Command Name="FormCloseCommand" />
  </Commands>
</Execution>

Запустите приложение. Убедитесь, что формы успешно загружены. Проверьте только что добавленный функционал.

На этом мы закончили рассматривать паттерн onClose.

Использование маски в текстовом поле

Давайте для поля "Контактный телефон" реализуем проверку формата введенного номера. Для этого в описание объекта PhoneTextBox добавим тэг <Mask> со значением PHONE. Таким образом, синтаксис текстового поля будет выглядеть так:

TemplateClientEdit.xml
<MyObject Name="PhoneTextBox" Type="TextBox" Assembly="BaseControls">
  <Top>
    <Object Name="PhoneLabel">
      <Property Name="Bottom" />
    </Object>
  </Top>
  <Left>
    <Object Name="PhoneLabel">
      <Property Name="Left" />
    </Object>
  </Left>
  <Width>
    <Calculate>
      <Expression>{0} - {1} - 10</Expression>
      <Items>
        <Item>
          <Object Name="ContentPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>
          <Object Name="PhoneTextBox">
            <Property Name="Left" />
          </Object>
        </Item>
      </Items>
    </Calculate>
  </Width>
  <Text>
    <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
      <Fields>
        <Field Name="Phone" />
      </Fields>
    </DataConnection>
  </Text>
  <Mask>PHONE</Mask>
</MyObject>

Запустите приложение и откройте карточку клиента. Проверьте, как теперь отображается поле "Контактный телефон".

Маска телефонного номера задает формат и ограничивает ввод символов в текстовое поле. Но нам нужно проверять, что пользователь ввел все необходимые данные. Для этого у объекта TextBox есть свойство MaskCompleted. Создадим условие для проверки этого свойства:

TemplateClientEdit.xml
<Condition Name="PhoneMaskNotCompletedCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhoneTextBox">
        <Property Name="MaskCompleted" />
      </Object>
    </Item>
    <Item>False</Item>
  </Items>
  <DataType Type="BooleanDataType" />
</Condition>

Так как поле "Контактный телефон" не является обязательным для заполнения, то полноту введенных данных необходимо проверять только в том случае, если пользователь начал заполнять поле. Для этого создадим Condition, в котором будем проверять, пустое ли поле:

TemplateClientEdit.xml
<Condition Name="PhoneIsEmptyCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PhoneTextBox" />
    </Item>
    <Item>+</Item>
  </Items>
</Condition>

Значение объекта PhoneTextBox сравниваем с символом "+", т.к. маска начинается с этого символа.

Создадим Checking для PhoneTextBox:

TemplateClientEdit.xml
<Checking>
  <Object Name="PhoneTextBox" />
  <ConditionExpression>
    <And>
      <Not>
        <Condition Name="PhoneIsEmptyCondition" />
      </Not>
      <Condition Name="PhoneMaskNotCompletedCondition" />
    </And>
  </ConditionExpression>
  <AsteriskHint>Некорректный номер телефона</AsteriskHint>
</Checking>

Запустите приложение и откройте карточку клиента. Проверьте отображение сообщения об ошибке при заполнении текстового поля "Контактный телефон".

Давайте поправим один момент. Сейчас при сохранении информации о клиенте, если поле "Контактный телефон" оставили пустым, в базу запишется строка "+". Чтобы в таком случае в таблицу template.client в базе данных в колонку phone писалось значение NULL, необходимо в ClientInsertSetDataConnection и ClientUpdateSetDataConnection изменить описание параметра Phone на следующий код:

<Parameter NativeName="Phone">
  <Value>
    <Switch>
      <Case>
        <When>
          <Not>
            <Condition Name="PhoneIsEmptyCondition" />
          </Not>
        </When>
        <Then>
          <Object Name="PhoneTextBox" />
        </Then>
      </Case>
    </Switch>
  </Value>
</Parameter>

Самостоятельно

  • На форме списка клиентов (TemplateStart.xml) на кнопки редактирования и удаления записей добавьте режим DisabledMode;

  • На форме списка городов (TemplateCityList.xml) на кнопки редактирования и удаления записей добавьте режим DisabledMode;

  • На форме карточки города (TemplateCityEdit.xml) поле "Наименование" сделайте обязательным для заполнения. Не забудьте про ограничение на колонку в таблице в базе данных;

  • На форме карточки города (TemplateCityEdit.xml) на кнопку "Сохранить" добавьте режим DisabledMode;

  • На форме карточки города (TemplateCityEdit.xml) реализуйте паттерн onClose;

  • На форме списка городов (TemplateCityList.xml) реализуйте паттерн onClose.

Итоги

На уроке мы рассмотрели паттерн onClose, суть которого заключается в том, чтобы определить, были ли изменения на форме, и предупредить пользователя, если он захочет закрыть форму без сохранения.

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

Ответы

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

Last updated