Немного об использовании ADO в Дельфи.

Что такое ADO?

ADO (Active Data Objects) - это высокоуровневый компонент технологии доступа к данным от Microsoft. (т.н. MDAC - Microsoft Data Access Components) Другие компоненты - это старый добрый ODBC и новый низкоуровневый интерфейс OLE DB.

Данными для ADO могут быть как привычные таблицы Access или серверные базы MS SQL или Oracle, так и несколько экзотичные Microsoft Active Directory Service, XML-файлы и т.п.

ADO- более новая технология чем ODBC, работает через интерфейс OLE DB.

Впрочем, можно использовать ADO и через ODBC - хотя стоит ли? OLE DB превосходит ODBC по скорости и, похоже, Microsoft основную ставку делает именно на парочку ADO+OLE DB. Более того, похоже, и Borland делает ставку на ADO, старик BDE готовится к отправке на пенсию, а компоненты для работы с ADO от Borland (ADOExpress) теперь входят в поставку Delphi. (существуют также Delphi-компоненты для работы с ADO от сторонних фирм). Так что если вы хотите работать с MS SQL 7 или MS SQL 2000 без ограничений, которые накладывает BDE (BDE по-прежнему полностью поддерживает только MS SQL 6.5 и обновления BDE пока не предвидится) то вам прямая дорога к изучению ADO Express. Что касается баз Access то я очень бы посоветовал обратить на них внимание программистам, уставшим от «парадоксального» поведения Paradox+BDE. Начиная с версии Access 2000 Microsoft удалось догнать и перегнать старый, добрый Paradox (не менявшийся, насколько мне известно, уже довольно давно).

Примечание:
На сайте Delphi3000 как-то опубликовали статью, сравнивающую Access и Paradox - и хотя сравнение это не совсем технически корректное обсуждение этой статьи, на мой взгляд, интересное: http://www.delphi3000.com/articles/article_1193.asp

Что такое ADO Express?

Это VCL-компоненты от Borland соответствующие иерархии компонентов ADO от Microsoft и упакованные так, чтоб их было удобно использовать программистам, привыкшим к BDE - TADOConnection, TADOCommand, TADODataSet, TADOTable, TADOQuery, TADOStoredProc и TRDSConnection.

Примечание:
В Delphi 6 будут включены компоненты dbGo - это ничто иное, как переименованный ADO Express - Microsoft теперь не разрешает другим фирмам использовать слово ADO в названии своих продуктов.
TADOConnection - это что-то вроде описателя адреса, по которому находятся данные. Главное его свойство ConnectionString (строка подключения) - это несколько строк вида «имя параметра = значение», которые описывают, где находятся данные и как к ним подключаться.
Главные значения, которые всегда должны быть в строке подключения - это имя поставщика данных (Data Provider) и имя базы данных. Пример:
Фрагмент строки подключения к базе Access:
P r o v i d e r = M i c r o s o f t . J e t . O L E D B . 4 . 0 ;  
D a t a   S o u r c e = C : \ d b 1 . m d b ; 

Microsoft.Jet.OLEDB.4.0 - это OLE DB поставщик данных, для баз Access, существует множество других провайдеров, например S Q L O L E D B . 1 - провайдер для MS SQL.
Кроме имени поставщика данных строка подключения может хранить еще массу параметров (например, имя пользователя и пароль, величину тайм-аута, права доступа и т.п.) перечень этих свойств зависит от конкретного Data Provider'а.

Придется запоминать кучу всех этих параметров строк подключения?
Нет, конечно. В поставку Windows входит специальный редактор строк подключения. Самый лучший, на мой взгляд, способ - это хранить строку подключения в отдельном файле - это позволит, при необходимости, изменить параметры подключения (например, путь к базе данных) без внесения изменений в приложение. Для хранения строк подключения предназначены файлы с расширением UDL (universal data link).
Создать UDL-файл проще простого - запустите Проводник (Explorer) Windows, щелкните в нужном каталоге правой кнопкой и выберите из контекстного меню NEW-> Microsoft Data Link File. Если вы не видите в меню такого пункта - не расстраивайтесь - просто создайте пустой текстовый файл и смените его расширение на UDL - щелкните по нему дважды - и перед вами редактор строк подключения во всей красе.

Выберите провайдера (для баз Access - провайдер JET, для MS SQL - OLE DB Provider for SQL server).

Нажмите кнопку далее
Н
а рисунке внешний вид редактора для баз Acces - введите путь к базе, имя пользователя и пароль (если нужно) и нажмите кнопку «Проверить подключение», чтоб убедиться, что техника Вам подвластна:

 

После того как UDL-файл готов можно использовать его в Delphi - создайте новый проект, поместите на форму (или на Data Module) компонент ADOConnection, щелкните по свойству ConnectionString и укажите путь к UDL-файлу откуда этот String нужно взять:

 

 

Все остальные компоненты ADO Express также имеют свойство ConnectionString - и в принципе, можно вообще обойтись без компонента ADOConnection - просто в каждом из используемых компонетов ADOQuery, ADOCommand и т.д. указать путь к этому UDL- файлу. Однако компонент ADOConnection содержит еще парочку полезных свойств - он позволяет указывать свойства «курсора» и управлять транзакциями.

Простой пример.
Для разминки бросьте на форму компонент TADOConnection (назовем его ADOConnection1), укажите ConnectionString на какой-нибудь UDL-файл, скажем к базе из Access, установите его свйоство Connected в True. Добавьте компонент TADOTable (пусть это будет ADOTable1), установите его свойство ADOConnection равным имени только что добавленного ADOConnection1, свойство Active в True.
Теперь добавьте компонент TDataSource (с закладки Data Access). Свойство DataSet этого DataSource установите равным ADOTable1.
Бросьте на форму компонент TDBGrid с закладки DataControls. Установите его свойство DataSource в DataSource1. Вы должны увидеть данные в сетке DBGrid'a. Запустите программу и поиграйтесь, редактируя данные.

Примечание:
В Интернете довольно много примеров описания того, как начать программировать с ADO, с примерами программ
Н
апример:
Статья Дмитрия Сидорова Поддержка технологии ADO
И
ли перевод статьи Marco Cantu Дилемма доступа к данным.
И Опыт использования ADO для доступа к базам данных форматов MS Access, xBase и Paradox Иванова Д.М.

 Про курсоры и транзакции.

Немного теории. Данные, как известно, хранятся в таблицах. Как правило, считывать сразу всю информацию из таблицы никому не нужно - поэтому считывается только часть, ограниченная каким-то условием (это условие задается обычно в SQL с помощью запросов с условием - Where) и называется Result Set. Result Set может быть очень большим - а программе как правило нужно иметь возможность изменять только одну строку из него, перемещаясь по нему в одном направлении (только вперед) или в обоих направлениях. Такую возможность предоставляет так называемый «курсор» - он и называется так потому, что как бы указывает на то - с какой строкой из результирующего набора работает программа.

Курсоры бывают разных видов. Во-первых, курсор может находиться как в памяти сервера, так и в памяти компьютера, который подключился к серверу (клиента). Местоположением курсора управляет свойство CursorLocation - оно может принимать два значения - clUseServer (курсор находится на сервере) и clUseClient (курсор находится на клиенте). Несмотря на то, что серверные курсоры кажутся заманчивыми - их используют не так уж часто - во-первых, не все поставщики данных их поддерживают, во-вторых, они здорово потребляют ресурсы сервера и при большом количестве клиентов их лучше не использовать.
В случае если курсор находится на сервере, на клиент может передаваться только одна строка таблицы, или несколько строк - этим управляет свойство Cache Size, по умолчанию оно равно 1.

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

Forward-Only

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

Static

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

Dynamic

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

Keyset

Этот вид курсора - нечто среднее между статическими и динамическими курсорами. Запоминаются ключевые поля всех записей на момент открытия курсора. Изменения, внесенные другими пользователями видны, если они не изменяют ключевые поля. Записи, добавленные другими пользователями не видны до тех пор, пока курсор не переоткрыть. При удалении записи, возникает «дыра» - ключевое поле записи остается в курсоре, но самой записи в таблице уже нет. Этот вид курсоров лучше использовать, только если записи в основном изменяются, но не удаляются и добавляются.

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

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

 

ltPessimistic

Пессимистическая блокировка. Как только пользователь начинает редактировать запись на эту запись тут же накладывается блокировка и другие пользователи не могут ее изменять.

ltOptimistic

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

ltBatchOptimistic

Пакетно-оптимистическая блокировка. Используется при пакетных изменениях

ltReadOnly

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

Теперь о транзакциях. Компонент ADOСonnection имеет три метода для работы с транзакциями:

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

После вызова метода BeginTrans свойство InTransaction компонента ADOConnection будет равно True до тех пор, пока транзакция не будет либо подтверждена, либо отменена.

С транзакциями связано также свойство IsolationLevel - оно показывает можно ли считывать данные, измененные в процессе транзакции, если транзакция еще не подтверждена. Значение этого свойства - ilReadCommitted означает что считываются только изменения, подтвержденные CommitTransaction, значение ilReadUncommitted - считываются неподтвержденные изменения, ilRepeatableRead - изменения, внесенные другими транзакциями не видны до тех пор, пока не переоткрыть набор данных.

 Коротко о других компонентах ADO

Самый базовый объект ADO - это ADOCommand - команда, которая передается серверу, для того чтобы считать или изменить данные (ADO всегда работает в режиме клиент-сервер, даже с локальными). После выполнения команды база данных может вернуть записи (если это была команда на считывание данных) или ничего не вернуть (если это, скажем, была команда на удаление данных). Команды - это ничто иное, как строки, написанные на языке SQL - если вы не знаете этот язык - изучите его - уверяю вас - в нем гораздо меньше ключевых слов чем в Delphi, который вы уже знаете.
Ссылка на учебники по SQL на русском языке на «Сервере Информационных Технологий»

Пара компонентов TADOTable и TADOQuery - предназначены для того чтобы программистам привыкших работать с BDE и компонентами TTable и TQuery было бы легче перейти на ADO. Они имеют сходные с TTable и TQuery методы - но, увы все же не совпадают с ними полностью - поэтому перевести проект, использующий BDE на ADO одним щелчком кнопки как правило не получится.

Компонент TADODataset - это компонент, который имеет возможности TADOTable и TADOQuery одновременно - т.е. может быть использован как для выполнения SQL- запроса так и для прямого обращения к какой-либо таблице.

TADOStoredProc - компонент для работы с хранимыми процедурами.

В следующей части мы поговорим о свойстве Properties компонентов ADO, которые, к сожалению, почти не освещены в справке от Borland и о некоторых отличиях, с которыми придется столкнуться программисту, переносящему проект с BDE на ADO.  

Часть 2

Я полагаю, что те, кто читают эти строки могут написать небольшую программку на Delphi, подключающуюся к базе данных с использованием ADO и имеют некоторое представление о языке SQL?
Если это не так у Вас есть три выбора

  • - все равно читать эту статью дальше - поскольку ничего особо сложного в ней нет
  • - перейти первой части и прочитать ее и все документы, на которые там имеются ссылки, а потом вернуться сюда
  • - нажать Alt+F4

Я рад, что Вы остались здесь - тогда продолжим. :-)

Для изучения я приготовил небольшой примерчик - это примитивная база данных Access 2000, состоящая из двух таблиц, такой вот структуры:

Первая таблица - воображаемые телефонные абоненты - в ней четыре поля - автоинкрементный код абонента, его имя, телефонный номер, и код выхода на междугороднюю линию - по умолчанию этому полю присваивается значение 8.
Вторая таблица служит для хранения информации о воображаемых междугородних звонках абонентов - здесь хранятся дата и время звонка, его продолжительность, код набранного города и набранный номер.
Таблица абонентов связанна с таблицей звонков по полю AbonentID отношением один ко многим.
В этой части мы будем работать только с первой из этих двух таблиц - про вторую пока можно не вспоминать.

Примечание 1:
гуру SQL Server несомненно признали в этой картинке диаграмму из Enterprise Manager - я воспользовался им просто для наглядности, на самом деле база Access - хотя перенести ее на SQL Server не составит никакого труда.

Примечание 2:
nvarchar - тип который не поддерживает BDE - одна из причин перехода на ADO.
 

Update Criteria 

Как я уже, кажется, говорил ADO всегда работает по принципу клиент/сервер - даже если Вы открываете локальную таблицу на своем компьютере. Это хорошо конечно, но иногда может сбить с толку, если не разобраться что именно происходит «за кулисами».
А происходит там следующее. Начнем с простого случая - пусть программа открывает только одну таблицу - Abonents - и позволяет пользователю добавлять новых абонентов и вносить исправления.
Как только мы открываем таблицу Abonents, кроме самих данных, из базы извлекаются еще и метаданные. Метаданные - это «данные о данных» т.е. имена таблиц, полей и самое главное, информация о том, какие из полей являются ключевыми (в нашем случае ключевое поле - AbonentID).
Эта информация (полученные метаданные) - позволяют движку ADO (ADO cursor engine) преобразовывать действия пользователя в команды SQL, посылаемые серверу.
Предположим пользователь добавил абонента.
На сервер будет отправлена приблизительно такая команда:

INSERT INTO Abonents … список имен полей… список значений…

А теперь пользователь исправил фамилию одного из абонентов.
На сервер отправится что-то вроде:

Update Abonent Set Name = 'Сидоров' where  AbonentID = 5.

Теперь становится понятным, почему нужны поля с первичным ключем - иначе движок ADO не имел бы ни малейшего понятия - как ему найти на сервере строку, которую надо изменить.
А откуда cursor engine знает, что первичный ключ таблиц Abonents - это поле AbonentID? Правильно - он знает об этом благодаря метаданным, которые были получены при открытии таблицы.
А что если первичного ключa все-таки нет? Тогда cursor engine пытается найти строку, включив раздел Where все остальные, не ключевые поля, и если при этом будет изменена не одна строка а несколько (предположим в базе были однофамильцы с одинаковыми телефонами) - вы получите предупреждение, о том что изменилось несколько строк, а не одна, но, увы, будет уже поздновато:-)

Примечание 1:
Если кому интересно внешний вид этого сообщения имеется в этой статье http://www.delphikingdom.com/mastering/ado.htm - точнее это вторая картинка сверху.

Примечание 2:
OLE DB Provider for Oracle обновляет данные без использования первичного ключа - используя Row ID

Пока все нормально, но предположим Вы - не единственный пользователь базы данных, и кто-то еще успел изменить данные до Вас. Что произойдет?
Ведь тогда команда

Update Abonent Set Name = 'Сидоров' where  AbonentID = 5.

просто-напросто запишет Ваш вариант изменений поверх чужого. Хорошо если это просто фамилия - а если это сумма? Кто-то добавил к этой сумме 100 долларов, а вы, не зная об этом - вписали 10? А если это была сумма, которую Вам должны заплатить? Не пойдет! Тут на арену выходит первое из динамических свойств - Update Criteria.
Но прежде пара слов - о том это за динамические свойства такие. Они динамические, потому что их можно изменять после того, как набор данных ADO уже открыт, доступ к ним осуществляется по имени через свойство Properties, например вот так:

ADODataSet1.Properties['имя свойства'].Value:=< новое значение свойства>

Итак, первое свойство, о котором мы поговорим, называется Update Criteria - оно может принимать следующие значения

adCriteriaKey

В строку Where включается только значения полей, состовляющих первичный ключ

adCriteriaAllCols

В строку Where включается значения всех полей, которые были выбраны при открытии набора данных командой Select

adCriteriaUpdCols

В строку Where включается значения только тех полей, которые были изменены пользователем. Это значение принято по умолчанию

adCriteriaTimeStamp

Это значение можно использовать если у вас есть поле типа TimeStamp - в строку Where включается значение такого поля, в результате если Вы попытаетесь изменить строку в таблице позже того, как ее изменил кто-то другой - получите сообщение об ошибке.

 

Внимательный читатель конечно уже поймал меня на маленькой хитрости - так как значение свойства по умолчанию adCriteriaUpdCols - значит мне не удастся вписать 10 долларов если кто-то уже вписал 100 - я получу сообщение об ошибке.
Хорошо, а как Cursor Engine узнает - удалось ему исправить строку или нет? Очень просто - сервер возвращает ему в ответ на команду цифру - число строк, которые были затронуты командой (number of rows affected).

Если эта цифра не равна единице - система считает, что произошел конфликт, связанный с оптимистической блокировкой при конкурирующем обновлении и выдает сообщение об ошибке «Row cannot be located for updating и т.д.» (Строка для обновления не найдена)

Все вроде ясно - но тут есть одна тонкость. Разработчики баз данных SQL Server'а часто используют так называемые триггеры - специальные SQL команды, которые могут срабатывать при добавлении или изменении данных в таблице, с которой Вы работаете. В результате Ваша программа может получить значение number of rows affected вовсе не после Вашей команды, а после команды, запущенной триггером. И если Ваша команда сработала и успешно изменила запись, но триггер выполнил какую-то другую команду, которая не изменила ни одной записи к Вам придет значение number of rows affected равное нулю и выскочит сообщение об ошибке которой на самом деле не было. Что же делать, как говаривал Достоевский? (вопрос «Кто виноват» опустим) Я могу предложить такой вариант выхода - во всех триггерах первой строчкой всегда пишите команду SET NOCOUNT ON - это заставит SQL Server не считать число строк, затронутых командами триггера - и вы, таким образом, будите получать правильное значение rows affected.

Update Resync

Следующее свойство, о котором я хотел рассказать, называется Update Resync. Итак, после того как мы добавляем или изменяем строку, изменения записываются на сервер. Но бывают ситуации, когда сервер сам добавляет кое-что к вновь введенным данным. Самый распространенный случай - в строке есть поле типа Identity (автоинкрементное) - значение которого устанавливается сервером. Другие случаи - поля со значениями по умолчанию (если такому полю ничего не присвоить - его значение будет установлено сервером), поля типа TimeStamp (при каждом изменении строки в это поле сервер записывает время изменения с точностью до миллисекунд). Кроме того, запись, которую Вы добавили или обновили, может быть изменена сработавшим триггером.
Посмотрите на нашу табличку Abonents. Как видите у нее есть поле со значение по умолчанию (поле Code принимает значение по умолчанию 8).
Теперь вообразите такой сценарий:

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

Стоп. Посмотрим, что тут произойдет. Так как значение свойства Update Criteria по умолчанию равно adCriteriaUpdCols на сервер будет отправлена команда вида:

Update Abonents Set Code = '9' where AbonentID = 15 AND Code IS NULL 

Все верно - передается ключевое поле и исходное значение того поля которое надо изменить, но это исходное значение должно быть равно 8! Наша программа не знала о том, что сервер вписал значение по умолчанию - в результате мы получим сообщение об ошибке «Не удается найти строку для обновления. Некоторые значения могли быть изменены со времени последнего чтения». Некоторые Delphi программисты предлагают обновлять весь набор данных после каждого изменения - по-моему это не очень элегантный способ - ведь нужно обновить только одну строку! Но как?
Для этого то и предназначено свойство Update Resync. Вот список его возможных значений:

 

adResyncNone

Никаких обновлений данных на стороне клиента после добавления или изменения не производится

adResyncAutoIncrement

После добавления новых строк считывается значение автоинкрементного поля (новое Identity) Это значение Update Resync принято по умолчанию

adResyncUpdates

После изменения строки - измененная строка тут же считывается с сервера

adResyncInserts

После добавления строки новая строка сразу же считывается с сервера

adResyncConflicts

В случае ошибки, связанной с конфликтом при конкурирующим обновлении с сервера считывается значение строки, вызвавшей конфликт

adResyncAll

Комбинация из всех возможных значений свойства Update Resync

 

Итак по умолчанию принято значение adResyncAutoIncrement - это значит что после добавления каждой записи движок ADO выдает команду select @@IDENTITY - и обновляет значение соответствующего поля на клиенте, отображая значение ID вновь добавленной строки. Увы, данная команда не поддерживалась в Access до версии 2000 - именно с этим связанна проблема, описанная в статье - Дениса Иванова

Примечание:
вообще я от души не советую связываться с Access 97 - если есть выбор - лучше выбрать Access 2000 или Paradox. Чтоб не быть голословным - Access 97 не поддерживает блокировку на уровне строк - поэтому Access 2000 и Paradox однозначно лучше.

Вопрос на засыпку - какое значение нужно установить свойству Update Resync в нашем примере, чтобы при добавлении абонента сразу же считывалось значение по умолчанию поля Code?

Верный ответ - ADODataSet1.Properties['Update Resync'].Value:= adResyncAutoIncrement + adResyncInserts;

Другой верный ответ - или adResyncAll

ADODataSet1.Properties['Update Resync'].Value:= adResyncInserts + adResyncInserts;

Примечание:
устанавливайте adResyncInserts только в комбинации одновременно с adResyncAutoIncrement - иначе Вы перестанете получать значения Identity для вновь добавленных полей.

В результате сразу после добавления нового абонента cursor engine сперва считает значение AbonentId которое получила вновь добавленная строка, а зачем, считает с сервера строку, используя полученное значение AbonentId - таким образом мы сразу увидим все изменения которые сервер произвел над нашей новой строкой и переоткрывать набор данных не понадобится.

Я написал небольшую программку, позволяющую по играться со свойством Update Resync вот ее внешний вид:

 

 

Вы можете скачать ее и поэкспериментировать - добавьте нового абонента, не внося его код, а потом исправьте код во вновь добавленной записи. Вы получите сообщение об ошибке.
Попробуйте сделать то же самое, установив adResyncInserts - ошибка не должна появляться.

Несколько слов о свойстве adResyncConflicts. В случае если Вы пытаетесь изменить запись, которую уже изменили до Вас - и установлено Update Criteria равное adResyncConflicts с сервера считываются значения полей записи, вызвавшей конфликт и помещаются в свойство Underlying Values каждого элемента масcива Fields соответствующего Recordset.
Не совсем по-русски звучит? Ну тогда скажу на Delphi:

for  i:= 0 to ADODataSet1.Recordset.Fields.Count-1 do
begin
   ADODataSet1.Recordset.Fields[i].UnderlyingValue; 
 {это актуальные, хранящиеся в настоящий момент, а не в 
момент открытия набора данных значения каждого i-того поля, 
записи которая вызвала конфликт}
end;

Обратите внимание как в моей прораммке-примере отлавливается событие RecordChangeComplete - если на этом событии переменная Reason равна erUndoUpdate - это значит что нужно отменить изменения - что я и пытаюсь сделать - но сработает это только если Update Criteria равно adResyncConflicts.

На этом пока все.

JINX,
22 мая 2001г.
Специально для
Королевства Delphi

 

[ список статей ]

Рейтинг ресурсов УралWeb
Рейтинг@Mail.ru

Rambler's Top100

TBN 100x100