Juri Shutenko Personal Homepage. Visual FoxPro.

Часть 2. DataEnvironment, как встроенный объект формы - I.

Для начала посмотрим на объект DataEnviroment (далее по тексту - DE), являющийся встроенным объектом форм. В этот объект мы можем вставить два типа объектов - курсоры (cursors) и отношения (relations), причем у курсоров в качестве источников данных используются таблицы базы данных, представления и свободные таблицы. На рисунках приведенных ниже, показаны свойства курсоров для таблицы базы данных и свободной таблицы.

Нетрудно заметить, что в свойстве Database указан полный путь к базе данных (верхний рисунок), а в свойстве CursorSource для свободной таблицы указан полный путь к ней.
 
Реально в мемо-поле файла формы хранятся относительные пути, в чем несложно убедиться, открыв форму как таблицу и посмотрев мемо-поле Properties для соответствующих объектов.
 
Для объекта, использующего таблицу базы данных в мемо-поле Properties файла формы определены следующие свойства (поскольку использовался файл формы реального проекта, то и значения свойств соответствуют реальным для данныз проектов):

Alias = "webprojects"
Database = ..\data\common.dbc
CursorSource = "webprojects"
Name = "Cursor1"

а для свободной таблицы следующие:

Alias = "extscripts"
CursorSource = ..\data\extscripts.dbf
Name = "Cursor4"

Какие преимущества дает использование встроенного объекта DE?. 1. При редактировании формы в дизайнере форм механизм установки источников данных для объектов формы (controls) упрощается до выбора требуемого поля или набора полей из combobox доступного в поле ContolSource илм RowSource объектов, которые могут быть связаны с источнкиами данных.

Предположим, что приложение должно обеспечить работу в структуре концерна, то есть при наличии головного и дочерних предприятий, или, что тождественно, приложение должно работать с одинаковыми базами данных и по ходу работы необходимо переключаться между подразделениями. При этом абсолютно не имеет значения - являются-ли эти базы данных структурными единицами одной большой базы данных (в идеологическом понимании этого термина) или эти базы индивидуальные, а также сколько их - одна, две, три, десятки. И вот здесь DE предоставляет нам очень гибкий механизм, обеспечивающий такую работу.

Исходя из вышесказанного следует, что мы должны как-то менять пути. И тут вроде бы появляется "проблема" - DE инициализуется первым и, по-мнению некоторых, событие формы Load как бы уже "опаздывает".

Что происходит в действительности и чем мы можем воспользоваться при разработке общей формы для работы с разными базами данных?

Что говорит нам трэкинг? Примерно следующее

frm_DEExample.Load()
frm_DEExample.dataenvironment.cursor1.Init()
frm_DEExample.dataenvironment.cursor2.Init()
frm_DEExample.dataenvironment.relation1.Init()
frm_DEExample.dataenvironment.cursor3.Init()
frm_DEExample.dataenvironment.relation2.Init()
frm_DEExample.dataenvironment.cursor4.Init()
frm_DEExample.dataenvironment.Init()
...
frm_DEExample.cus_inifile.Init()
...
frm_DEExample.Init()

А теперь давайте посмотрим на события и методы объекта. Ниже приведен screen-shot PEM окна для DE:

DataEnvironment PEM window

Что мы имеем? В свойствах нас интересует AutoOpenTables; в событиях и методах - OpenTables, BeforeOpenTables и Init.
 
При запуске формы:

  • первым загружается DE
  • если свойство AutoOpenTables установлено в True (.T.), то DE вызывает метод OpenTables,
  • после чего срабатывает событие BeforeOpenTables,
  • далее срабатывает событие Load формы,
  • после чего срабатывают события Init для помещенных в DE курсоров и установленных между ними отношений
  • и уже затем срабатывает событие DE Init.

Объект DE нам не подходит "однозначно" (© Жириновский - если уйдет из политики, кто нас будет развлекать?), по той простой причине, что мы не имеем к нему доступа в дизайнере классов в случае проектирования класса на основе Form (в девятой версии этой возиожности также не прибавилось).
 
Начиная с 8-й версии (про 7-ю не знаю), мы можем создать класс DE, но не о нем сейчас речь, ведь многие работают с 6-й и, стало быть, нужен универсальный подход. Отсюда вывод: самое подходящее для нас событие - Load формы. Его преимущества просты - к моменту срабатывания этого события объект DE уже создан и все, что нам нужно для перестройки окружения, мы можем прописать в ассоциированном с этим событием методе и далее использовать класс на основе этой формы для создания форм, позволяющих переключаться во время работы на разные базы данных.
 
Поскольку передать параметры форме мы можем только для события Init, то, очевидно, нам требуется какой-то внешний объект. Для 6-й версии это может быть объект приложения, в 8-й появился замечательный класс - Empty, легкий и быстрый. Его мы можем создать при запуске приложения и обращаться к нему отовсюду, откуда пожелаем. Чтобы не обижать пользователей разных версий я приведу примеры как для 6-й, так и для 8-й.

VFP 8.0 (9.0). Использование классов Empty и Collection.

При запуске программы используется основной модуль, в котором создается объект loApp на базе класса Empty, в который заносится маска названий каталогов, в которых хранятся базы данных и позиция маски в названии каталога.

Я использую два принципа именования каталогов данных - либо Data1,Data2,Data3...DataN, либо Имя_предприятия_data, Имя_предприятия_data,Имя_предприятия_data... Имя_предприятия_data, где имена соответствуют конкретным именам (в случае наименований на русском используется транслитерация).

Собранные с помощью объекта FileSystemObject каталоги помещаются в коллекцию объектов размещения, при этом в качестве ключа используется условный код окружения данных, который в дальнейшем используется в программе для идентификации баз данных предприятий (организаций) концерна. Код может выглядеть так (реальный код несколько сложнее, вследствие того, что объект приложения заполняется данными либо из файла предустановок или конфигурации, либо из реестра машины - а именно: маска, позиция и так далее):

loApp=Createobject("Empty")
loDEFolders=Createobject("Collection")
loFSO=Createobject("Scripting.FileSystemObject")

AddProperty(loApp,"appPath",Addbs(Justpath(Sys(16))))
AddProperty(loApp,"scDataFolderMask","DATA")
AddProperty(loApp,"scDataFolderMaskPosition","L")
AddProperty(loApp,"scCurrentDataFolder","")
loAppFolder=loFSO.GetFolder(loApp.AppPATH)
lni=1
lcKeyPrefix="DE"
lcKey=""
For Each loFolder In loAppFolder.Subfolders
   If loApp.scDataFolderMaskPosition="L"
      If Upper(;
            Left(loFolder.Name,Len(loApp.scDataFolderMask));
         )=loApp.scDataFolderMask
         lcKey=lcKeyPrefix+Ltrim(Str(lni))
         loDEFolders.Add(Addbs(loApp.AppPATH+loFolder.Name),lcKey)
         lni=lni+1
      Endif
   Else
      If Upper(;
            Right(loFolder.Name,Len(loApp.scDataFolderMask));
         )=loApp.scDataFolderMask
         lcKey=lcKeyPrefix+Ltrim(Str(lni))
         loDEFolders.Add(Addbs(loApp.AppPATH+loFolder.Name),lcKey)
         lni=lni+1
      Endif
   Endif
Endfor
loApp.scCurrentDataFolder=lcKeyPrefix+[1]

Теперь когда у нас все подготовлено, вернемся к форме. Поскольку к моменту срабатывания события Load формы объект DataEnvironment создан, мы можем обратиться к его членам и установить для них требуемые свойства. Но перед этим, независимо от установки свойства AutoOpenTables объекта DE, вызовем его метод CloseTables. Почему? Объясню чуть позже, а может вы уже и сами догадались. Итак, работаем c объектами объекта DE:

This.DataEnvironment.CloseTables
Local lni, loDEObject, lcCurrentDBCPath, lcNewDatabasePropValue
lcCurrentDBCPath=loDEFolders(loApp.scCurrentDataFolder)

For lni=1 To This.DataEnvironment.Objects.Count
   loDEObject=This.DataEnvironment.Objects(lni)
   If loDEObject.BaseClass="Cursor"
      If PEMSTATUS(loDEObject,"Database",0)
         If Upper(Addbs(Justpath(loDEObject.Database)))!=Upper(lcCurrentDBCPath)
            lcNewDatabasePropValue = ;
               Strtran(;
               loDEObject.Database,;
               Addbs(Justpath(loDEObject.Database)),;
               lcCurrentDBCPath;
               )
            loDEObject.Database=lcNewDatabasePropValue
            *!* Wait ;
            *!*    loDEObject.Name + ;
            *!*    [ is member of ]+;
            *!*    loDEObject.Database ;
            *!*    Window Timeout 2
         Endif
      Else && here i deal with a free table
         If Upper(Addbs(Justpath(loDEObject.CursorSource)))!=Upper(lcCurrentDBCPath)
            lcNewCursorSourcePropValue = ;
               Strtran(;
               loDEObject.CursorSource,;
               Addbs(Justpath(loDEObject.CursorSource)),;
               lcCurrentDBCPath;
               )
            loDEObject.CursorSource=lcNewCursorSourcePropValue
         Endif
      Endif
   Endif
Endfor
This.DataEnvironment.OpenTables

Ну вот дело и сделано. Что далее? У меня имеется таблица, в которую вносятся и добавляются предприятия, для которых устанавливается соответствие имени и окружения. Последнее - окружение - из меню-ли, из формы-ли переключения задач, переписывается при надобности в значение свойства scCurrentDataFolder объекта loApp.

Чуть не забыл о предудыщих версиях.

Если разработка ведется на 6-й версии, то вместо класса Empty используется Custom.

loApp=Createobject("Custom")
loFSO=Createobject("Scripting.FileSystemObject")
loApp.AddProperty("appPath",Addbs(Justpath(Sys(16))))
loApp.AddProperty("scDataFolderMask","DATA")
loApp.AddProperty("scDataFolderMaskPosition","L")
loApp.AddProperty("scCurrentDataFolder","")

Оставшаяся часть кода, приведенного для 8-й (9-й) версии - аналогична.

Что же касается объекта Collection, то тут на ваше усмотрение - кому как удобно. Можно использовать тот же Custom, но проще использовать курсор или массив. В случае Custom добавляется свойство с именем окружения, а в качестве значения - каталог хранения данных и затем имя каталога извлекается с помощью макроподстановки. Раньше я использовал курсор. С выходом восьмерки быстренько все переделал под коллекции.

А далее? А далее сплошные удобства для работы. Но об этом в следующей статье.

Селектор для быстрого перехода на сайты, связанные с Visual FoxPro.