Средства разработки приложений

         

Описание демонстрационного проекта


Кроме исходного кода модулей библиотека AcedUtils включает пример, реализующий полноценное приложение для работы с данными на основе коллекций из модуля AcedStorage. Пример включает несколько отчетов, которые строятся с помощью функций из модуля AcedExcelReport. Рассмотрим подробнее задачу, решаемую этим приложением, и некоторые особенности реализации хранилища данных на основе файлового сервера.

Программа предназначена для учета товаров, поступающих от различных поставщиков. Товары классифицируются по категориям. Основные типы и наборы данных, используемые приложением, описываются в модуле DataTypes, являющемся аналогом модуля данных. Структура базы данных состоит из трех таблиц: поставщики товаров (коллекция Suppliers), категории товаров (коллекция Categories) и сами товары (коллекция Products). Каждая из этих сущностей описывается классом, производным от класса TSerializableObject из модуля AcedStorage. Атрибуты сущностей, т.е. поля таблиц базы данных, представляются свойствами этих классов. Обычно каждому такому свойству соответствует private-поле в классе объекта, используемое для хранения значения этого свойства. Кроме того, в модуле DataTypes имеется класс TPictureObject, предназначенный для сохранения в бинарном потоке и чтения из потока данных объекта типа TPicture. Этот класс используется для хранения изображения, ассоциированного с категорией товаров, которая представляется классом TCategoryObject.

Класс TSupplierObject описывает поставщика товара. Он включает свойства: ID – уникальный идентификатор записи (наследуется от класса TSerializableObject); CompanyName – наименование поставщика; Country – страна поставщика; CityRegion – строка с названием города и, возможно, области; Address – адрес поставщика; PostalCode – почтовый индекс; PhoneFax – номера телефонов и факсов; HttpEmail – адреса электронной почты и web-сайта поставщика; ContactPerson – представитель поставщика, с которым поддерживается контакт; Comments – дополнительная информация о поставщике.
Как и во всяком классе, производном от TSerializableObject, в классе TSupplierObject перекрываются методы: Load, Save, Equals, Clone базового класса. В частности, в методе Load выполняется чтение состояния объекта из бинарного потока. Значения всех полей, в том числе FID, унаследованного из TSerializableObject, считываются методами класса TBinaryReader. Метод Save помещает соответствующие данные в бинарный поток, представляемый классом TBinaryWriter. На примере класса TSupplierObject демонстрируется один из приемов оптимального использования пространства в бинарном потоке. Вместо того, чтобы сохранять все подряд значения полей, в методе Save создается переменная Flags, содержащая битовую карту используемых полей, т.е. полей, значения которых отличны от значений по умолчанию. Эта битовая карта помещается в бинарный поток в виде однобайтного значения. Затем в потоке сохраняются значения только тех полей, которые помечены как используемые. В методе Load сначала считывается битовая карта, а затем значения полей, которые помечены в ней единичными битами. В методе Equals класса TSupplierObject выполняется сравнение значений каждого из полей объекта с соответствующими полями другого экземпляра того же класса. Если все поля обоих экземпляров равны, функция возвращает True, в противном случае – False. Метод Clone создает новый экземпляр класса TSupplierObject и копирует в него значения всех полей данного объекта, включая поле FID, унаследованное от класса-предка. Класс TCategoryObject предназначен для описания категории товара. Он содержит наименование категории (свойство CategoryName), комментарий (свойство Comments) и рисунок, иллюстрирующий данную категорию (свойство Picture). Кроме того, класс TCategoryName включает свойство ID, унаследованное от класса-предка TSerializableObject. Аналогично TSupplierObject, в данном классе перекрываются методы Load, Save, Equals и Clone базового класса. Кроме того, перекрывается виртуальный конструктор Create для создания объекта типа TPictureObject, представляющего рисунок, и метод Destroy для освобождения этого объекта.


Следует обратить внимание на то, как в этом классе сохраняется и считывается значение уникального идентификатора записи – поля FID базового класса TSerializableObject. Предполагается, что число возможных категорий товара не превышает 255, поэтому идентификатор записи хранится в виде байта, а не значения типа Integer. При этом сама коллекция категорий товаров представляется экземпляром класса TBytePrimaryKeyCollection. Класс TProductObject содержит свойства, используемые при описания товара: ID – уникальный идентификатор записи; ProductName – наименование товара; SupplierID – внешний ключ, содержащий ссылку на элемент коллекции Suppliers, представляющий поставщика товара; CategoryID – внешний ключ, содержащий ссылку на элемент коллекции Categories, представляющий категорию товара; QuantityPerUnit – строка, описывающая единицу измерения товара; UnitPrice – цена единицы товара; UnitsInStock – количество товара на складе; UnitsOnOrder – ожидаемое количество товара; Discontinued – признак того, что поставки товара прекращены; Little – признак того, что на складе осталось мало соответствующего товара. На примере класса TProductObject демонстрируется работа с версиями объектов. Предполагается, что в первоначальной версии данного класса отсутствовало свойство Little, оно было добавлено во второй версии. Сохраняемая версия элементов коллекции определяется параметром Version при вызове конструктора класса данной коллекции. Так, при вызове конструктора для коллекции Products во втором параметре передается значение 2. В методе Load класса TProductObject проверяется значение параметра Version. Если версия равна 1, данные сохранены в исходном формате, т.е. без поля FLittle. Если сохраненная версия данных равна 2, в бинарном потоке присутствует значение поля FLittle, которое должно быть прочитано методом ReadBoolean класса TBinaryReader. Класс TPictureObject, используемый для хранения рисунка, иллюстрирующего категорию товара, может сам по себе найти применение во многих приложениях.


Он позволяет сохранять в бинарном потоке растровый рисунок (TBitmap), метафайл (TMetafile) или иконку (TIcon), которые представляются классом TPicture из модуля Graphics. Причем, данные изображения постоянно находятся в упакованном виде и распаковываются только, когда нужно загрузить изображение в объект TPicture. Метод Load класса TPictureObject считывает изображение из объекта TPicture, бинарного потока, представленного классом TBinaryReader, или другого экземпляра класса TPictureObject. Метод Save сохраняет изображение в потоке. Метод Assign загружает рисунок в объект TPicture. Функция Equals возвращает True, если данный экземпляр класса TPictureObject, содержит то же самое изображение, что и экземпляр, переданный в функцию как параметр. Свойства IsBitmap, IsMetafile, IsIcon класса TPictureObject позволяют проверить тип изображения, представленного объектом. Свойство Bytes возвращает указатель на массив байт, содержащий упакованное изображение. Свойство Length – длину этого массива в байтах. Коллекции Suppliers, Categories, Products создаются процедурой CreateCollections в модуле DataTypes, которая вызывается из раздела инициализации этого модуля. Перед созданием каждой из коллекций создаются индексы, предназначенные для упорядочивания элементов коллекций по какому-либо признаку. Так, поставщики сортируются по значению свойства CompanyName, т.е. по наименованию поставщика, список категорий – по наименованию категории, товары – по наименованию товара, идентификатору поставщика и идентификатору категории. Индексы по наименованиям являются уникальными, что обеспечивает отсутствие в коллекциях элементов с дублирующимися наименованиями. Индексы по внешним ключам в коллекции товаров используются для проверки наличия подчиненных записей в таблице товаров при попытке удаления записи из главной таблицы – коллекции поставщиков или коллекции категорий товаров. Так как количество поставщиков может быть большим, для коллекции Suppliers включается режим хеширования элементов по значению уникального идентификатора (свойство MaintainHash устанавливается в True).


Уничтожаются коллекции процедурой FreeCollections, вызываемой из раздела finalization модуля DataTypes. Для удобства загрузки и сохранения коллекций в модуле DataTypes определены глобальные функции, такие как LoadSuppliers, LoadProducts, SaveSuppliers и т.п., предназначенные для работы с данными в режиме многопользовательского доступа. Функция LoadSuppliers и аналогичные ей функции для загрузки коллекций Categories и Products просто вызывают метод LoadFile соответствующей коллекции и передают в него имя файла данных и, в случае коллекции поставщиков, пароль для дешифрования файла данных. Функции для сохранения изменений в файле реализуют более сложный алгоритм. Рассмотрим его на примере функции SaveSuppliers, используемой для сохранения на диске коллекции поставщиков. В начале функции с помощью свойства HasChanges проверяется наличие в памяти каких-либо кэшированных изменений для коллекции Suppliers. Если изменений нет, то отсутствует необходимость перезаписи файла данных. Если изменения есть, соответствующий файл данных открывается методом OpenFile коллекции. При этом указывается пароль, используемый для шифрования и дешифрования данных коллекции, а также указывается константа dcmFast, включающая режим быстрого сжатия данных для экономии места на диске и сокращения объема информации, пересылаемой по сети. В случае невозможности открытия файла данных (например, если этот файл открыт для записи другим пользователем и его приложение почему-то зависло), кэш изменений очищает и на экран выводится сообщение об ошибке. После того, как файл данных успешно открыт и, при наличии в нем изменений, перечитан с диска, для коллекции Suppliers вызывается метод ApplyChanges. Этот метод применяет кэшированные изменения к набору данных коллекции. Если изменения применены успешно, коллекция сохраняется на диске вызовом метода SaveIfChanged. Если в процессе применения изменений возникли какие-либо проблемы, данные коллекции перечитываются с диска методом UndoIfChanged, а затем на экран выводится сообщение об ошибке.


Метод UndoIfChanged перечитывает данные с диска только в случае, если коллекция содержит изменения, т.е. ее свойство Changed равно True. В конце работы файл данных должен быть обязательно закрыт методом CloseFile коллекции. Для большей надежности вызов метода CloseFile помещается в раздел finally блока try-finally структурированной обработки исключений. Рассмотрим, как выполняется добавление, изменение, удаление записей на примере коллекции поставщиков. Корректировка списка поставщиков производится с помощью формы TSuppliersForm, определенной в модуле SuppliersUnit. При этом используется глобальная переменная SupplierObject типа TSupplierObject, объявленная в модуле DataTypes. Когда пользователь хочет добавить в список нового поставщика и нажимает соответствующую кнопку, вызывается функция NewItem коллекции Suppliers, создающая новый экземпляр класса TSupplierObject, который затем присваивается глобальной переменной SupplierObject. Для ввода данных о новом поставщике на экране отображается форма TSupplierForm из модуля SupplierUnit. Эта форма позволяет пользователю скорректировать значения свойств экземпляра класса TSupplierObject, назначенного переменной SupplierObject. Когда пользователь заполнит все необходимые поля и нажмет кнопку для сохранения изменений, выполняется предварительная проверка введенных данных. Например, проверяется, что наименование поставщика не является пустой строкой и что это наименование не дублируется в списке. Позже, при сохранении данных, это привело бы к ошибке, вызванной нарушением уникальности индекса, и откату всех изменений. Такие ошибки удобнее выявлять на этапе предварительной проверки, чтобы позволить пользователю исправить некорректно введенные данные. Если предварительная проверка прошла успешно, форма ввода данных закрывается, для коллекции Suppliers вызывается метод EndEdit, сохраняющий информацию о добавлении записи в кэше изменений. Затем вызывается рассмотренная выше глобальная функция SaveSuppliers из модуля DataTypes для применения кэшированных изменений к набору данных и сохранения коллекции на диске.


Если пользователь отказался от сохранения изменений закрытием по Escape формы ввода данных, вызывается метод CancelEdit коллекции Suppliers для уничтожения объекта, присвоенного переменной SupplierObject. Корректировка записи о поставщике отличается от добавления нового поставщика только тем, что в начале вместо NewItem вызывается функция BeginEdit коллекции поставщиков Suppliers, в которую передается идентификатор редактируемого элемента коллекции. Когда пользователь удаляет поставщика из коллекции Suppliers, на экране сначала появляется окно для подтверждения удаления записи. Затем открывается файл данных для коллекции товаров Products вызовом метода OpenFile. Используя индекс по внешнему ключу SupplierID коллекции Products, проверяется наличие в списке товаров элемента, ссылающегося на удаляемого поставщика. Если такой товар присутствует в списке, поставщик не может быть удален. Если на этого поставщика никто не ссылается, для коллекции Suppliers вызывается метод Delete, в который передается идентификатор удаляемого элемента. Затем вызывается функция SaveSuppliers для фактического удаления записи из набора данных и сохранения измененной коллекции на диске. Файл данных коллекции Products закрывается только после сохранения всех изменений в коллекции Suppliers. Это нужно, чтобы другой пользователь не мог в момент удаления поставщика добавить товар, который на него ссылается. С этой же целью в момент сохранения коллекции товаров Products вызовом глобальной функции SaveProducts для каждого добавленного или измененного товара проверяется наличие в коллекции Suppliers поставщика, идентификатор которого назначен свойству SupplierID данного товара.

Содержание раздела