* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



ДОБАВЛЕНИЕ ВОЗМОЖНОСТЕЙ РЕДАКТИРОВАНИЯ
31 марта 2009

Разумеется, чаще возникают ситуации, когда необходимо обеспечить возможность редактирования данных, в т.ч. удаления записей и добавления новых. Определенные сложности при этом могут возникать лишь при добавлении новых записей, в то время как редактирование и удаление достаточно тривиальны.
Рассмотрим более сложный и более приближенный к реальности пример потомка TDataSet. Наш класс TMyDataSet будет предоставлять доступ для чтения/изменения/удаления/добавления записей в адресную книгу, представляющую собой список (обычный TList), содержащий указатели на записи, состоящие из двух строковых компонентов: имени и электронного адреса. Кроме того, TMyDataSet будет поддерживать работу с закладками.
Итак, какие действия предпринимает TDataSet при редактировании, добавлении и удалении записей?
Самая простая операция – удаление. Вызов общедоступного метода Delete приводит (если TDataSet не находится в состоянии вставки записи, в противном случае Delete просто вызывает Cancel) к вызову InternalDelete, на который возлагается обязанность удалить запись из набора. Работа завершается вызовом Resync, так что InternalDelete может поместить физический курсор на любую запись по своему усмотрению.
Что касается редактирования, то можно выделить два его вида: обычное редактирование записи (состояние dsEdit), которое я и буду в дальнейшем называть редактированием, и вставка записи (dsInsert).
Для начала редактирования вызывается метод Edit. Он, в свою очередь, вызывает метод InternalEdit. В большинстве ситуаций никаких действий в InternalEdit можно не предпринимать – TDataSet переведет объект в состояние dsEdit, особым образом обработает вычисляемые (calculated) поля, выдаст сообщение об изменении записи и т.д. Для начала вставки записи вызовается Insert или Append. Как в состоянии dsInsert, так и в dsEdit TDataSet позволяет вносить в запись изменения.
Выводится из этих состояний TDataSet также одинаковыми способами. Их два: вызов метода Post (подтверждение изменений – и вставки, если мы находимся в состоянии dsInsert) или метода Cancel (отмена изменений/вставки). Первый из них вызывает InternalPost, второй –InternalCancel. Отметим, что перед вызовом каждого из них осуществляется вызов UpdateCursorPos – а это означает, что физический курсор будет совпадать с логическим. Методы Post и Cancel могут быть вызваны не только напрямую сторонним кодом, но и из других методов TDataSet: так, при попытке, пока TDataSet находится в состоянии редактирования (dsEdit) или вставки (dsInsert), переместить логический курсор на другую запись, операция вставки или редактирования будет либо отменена, либо подтверждена (в зависимости от того, внесены ли в запись изменения). Логика TDataSet полностью возлагает реализацию добавления/удаления/редактирования записей на Internal-методы; в частности, именно они определяют, в какую позицию набора будет при вставке помещаться вставляемая запись, будет ли она добавлена сразу (а при вызове InternalCancel – удалена) или будет существовать только в виде буфера (а физически добавляться при вызове InternalPost).
Серьезное обсуждение операции вставки невозможно без знакомства с аппаратом закладок.
Закладка (bookmark) – это некоторая информация, идентифицирующая положение записи в наборе данных. Предпочтительно представлять закладку ANSI-строкой символов (т.е. последовательностью ненулевых символов, завершающейся нулевым), т.к. клиент может пытаться читать ее из свойства Bookmark методом GetBookmarkStr именно как ANSI-строку (в примере от Borland закладка представляется 4-байтовым адресом записи, что, безусловно, неправильно, т.к. адрес может содержать нулевые байты). Свойство Bookmark возвращает закладку, соответствующую текущей записи. Впоследствии с помощью этой закладки можно очень быстро сделать текущей соответствующую ей запись. Закладка всегда указывает на одну и ту же запись, вне независимости от изменения положения этой записи в наборе.
Если задача потомка TDataSet – обмен данными с каким-либо сервером БД, то, скорее всего, сервер уже снабжен поддержкой закладок, и можно просто перенаправлять все запросы ему.
Кроме собственно закладки, каждой записи соответствует поле так называемого флага закладок. Записи соответствует ровно один флаг закладки, как и ровно одна закладка. Флаг закладки предназначен для того, чтобы логика TDataSet могла отличать одни типы записей от других. Помимо нормального флага bfCurrent, определены следующие значения:
 bfInserted – “вставленная” запись (запись, подлежащая просмотру и редактированию, но еще не включенная в набор).
 bfBOF – запись, находящаяся перед первой записью (например, вставленная в пустой набор). Для установки физического курсора на запись с таким флагом закладки логика TDataSet вызывает InternalFirst вместо InternalSetToRecord;
 bdEOF – запись, находящаяся за последней записью. Вызывается InternalLast.
Для клиента (т.е. внешнего по отношению к потомку TDataSet кода) работа с закладками сводится к двум операциям: чтению закладки для текущей записи и перемещению к записи, для которой известна закладка. Этим действиям соответствуют два метода TDataSet:GetBookmarkData и InternalGotoBookmark. Есть, впрочем, и третий метод, SetBookmarkData, служащий для изменения значения закладки текущей записи. Замена закладки исключительно плохо документирована. Разбор исходного текста класса TDataSet показывает, что код TDataSet вызывает SetBookmarkData в одном-единственном случае – перед вызовом InternalInsert новой записи приписывается та же закладка, что и предыдущей активной (если только вставляемая запись не окажется первой и единственной в наборе).
Смысл этого дублирования мне (после просмотра примера от Borland) видится таким: так как значение физического курсора, соответствующее предыдущей активной записи, неизвестно, а добавленная в набор данных запись может до вызова Post быть “висячей” (а значит, также не иметь соответствующего ей значения физического курсора), то предлагается вместо перехода на запись использовать переход на закладку. При этом до вызова Post предыдущая активная запись и новая запись имеют одинаковые закладки. Никакой коллизии здесь нет – при попытке получить закладку вставленной, но не добавленной в набор записи (у нее флаг закладки равен bfInserted, bfEOF или bfBOF), возвращается пустая строка (т.к. метод BookmarkAvailable может вернуть True лишь для записей с флагом закладки bfCurrent). То есть, с точки зрения внешнего по отношению к TDataSet кода, новая запись закладки не имеет. Это, кстати, следует учитывать в реализации поиска записи по закладке: поиск нужно проводить лишь по записям с флагом закладки bfCurrent.
При обработке InternalPost закладку у вставляемой записи нужно изменить на такую, которая позволит однозначно эту запись идентифицировать – ведь теперь ее флаг закладки равен bfCurrent, и если оставить старое значение закладки, то в этом наборе будут присутствовать несколько записей с одинаковыми закладками, но разными значениями физического курсора.
Как только мы начинаем реализовывать операцию вставки, сразу же встает важный вопрос: в какую позицию набора помещать добавляемую запись?
Вообще говоря, в TDataSet существует два способа добавления записи. Это методы Insert и Append (оба в итоге вызывают InternalInsert) – их назначение описано в справочной службе. Для обоих методов жестко определено лишь одно требование: логический курсор должен переместиться на добавленную запись. О том, в какую именно позицию вставляется новая запись каждым из методов, можно прочитать в справочной системе Delphi (каждая конкретная БД поступает по-своему).
В принципе, можно сделать так, чтобы не только по Append, но и по Insert записи всегда добавлялись в конец набора. Со вставкой в конец проблем обычно не возникает, т.к. в этом случае приходится перемещать физический и логический курсоры, а значит, начальное значение физического курсора можно не принимать во внимание. Добавление в конец, однако, не всегда приемлемо, и вдобавок вызывает неприятные визуальный эффект (особенно если мы одновременно обозреваем несколько соседних записей, как в DBControlGrid или DBGrid): как только курсор уходит с записи, она “прыгает” из зоны видимости в конец набора.
Поэтому имеет смысл задаться целью вставить запись именно в то место TDataSet, на которое при вызове Insert указывает логический курсор. Везде в дальнейшем предполагается, что вставка новой записи осуществляется как раз согласно этому правилу.
Сама по себе вставка проблемой не является и в этом случае. Проблема – в том, как корректно обрабатывать возникающую “виртуальную” запись, реально не существующую в наборе? А именно, как правильно обрабатывать команды на перемещение курсора на эту запись, или между ней, предыдущей и следующей записями.
Перемещение физического курсора может произойти при вызове одного из следующих методов: InternalFirst, InternalLast, GetRecord,SetRecNo, InternalSetToRecord и InternalGotoBookmark. Пока TDataSet находится в состоянии dsInsert – то есть до отмены или подтверждения вставки записи – один из этих методов может быть вызван только при вызове методов UpdateCursorPos, Resync,SetBufferCount или при записи в свойство Bookmark. В других случаях TDataSet предварительно выходит из состояния редактирования. Например, попытка выполнить метод First приведет еще до вызова InternalFirst к вызову либо Cancel, либо Post.
С SetRecNo есть небольшая тонкость: непонятно, имеет ли вставленная запись номер до того, как она будет включена в набор. Я буду предполагать, что не имеет. В противном случае пришлось бы в GetRecNo и SetRecNo сравнивать номер записи с номером вставленной записи и перемещение курсора производить в зависимости от результата. А раз не имеет, то работа SetRecNo не зависит от виртуальных записей.
Ясно, что методы InternalSetToRecord и InternalGotoBookmark для любой записи в наборе данных, кроме вставленной, будут работать так же хорошо, как работали без этой виртуальной записи (т.к. целевая запись – единственная идентифицируемая переданными в метод аргументами запись, реально существующая в наборе). То есть опасность может представлять лишь попытка перейти на саму редактируемую запись, и именно этот случай мы должны распознавать. Но куда же в такой ситуации помещать курсор?
Вызов InternalGotoBookmark приведет, т.к. закладка вставленной записи дублирует закладку какой-то уже имеющейся, к переходу на эту уже имеющуюся запись. Естественно ожидать, что так же должна себя вести и InternalSetToRecord. Для этого есть и более существенные основания – они будут рассмотрены чуть позже при анализе методов GetNextRecord и GetPriorRecord. Есть несколько способов добиться такого поведения InternalSetToRecord, их мы тоже рассмотрим позже.
Для UpdateCursorPos принципиально, для чего (перед какой операцией) осуществляется позиционирование курсора. Этот метод вызывается при ошибках перед Resync, а также: в Refresh, Cancel, AddRecord, Edit, Post, Delete. Вызов Edit в состоянии dsInsert игнорируется, вызов Delete вырождается в обращение к Cancel, а вызов AddRecord приводит еще до UpdateCursorPos к выходу из режима редактирования. Вызов перед Resync, так же, как и внутри Refresh, производится для того, чтобы Resync прочитал записи именно вокруг текущей. Вызов в Post дает InternalPost знать, в какое место набора данных вставляется запись. Вызов в Cancel позволяет InternalCancelсделать, например, вывод о том, какую запись нужно сделать текущей после отмены вставки.
С учетом этого для UpdateCursorPos курсор вполне корректно устанавливать на запись, перед которой осуществляется вставка. Действительно, в InternalPost легко по значению курсора, указывающему на следующую запись, понять, в какое место в наборе нужно поместить вставленную запись (отодвинув последующие): именно на это место. InternalCancel можно не перекрывать. Тогда после “вычеркивания” вставленной записи текущей станет запись, следующая за ней, что выглядит вполне логично. По поводу Resync см. замечания далее.
Как обеспечить правильную работу SetBufferCount? Если взглянуть на код этого метода, видно, что опасны лишь два ситуации:
Для метода GetNextRecord, вызывающего SetCurrentRecord, опасен случай, когда самый последний буфер содержит вставленную запись. Ясно, что в качестве “следующей” нужно прочитать запись, находящуюся в наборе за этой записью.
Для метода GetPriorRecord, также обращающегося к SetCurrentRecord, опасен случай, когда вставленная запись находится в первом буфере. Нужно прочитать запись, находящуюся в наборе перед вставленной.
Понятно, что на какую бы реально существующую в наборе запись не устанавливал физический курсор метод InternalSetToRecord, она не может находиться одновременно и прямо перед записью, следующей за вставленной, и сразу за записью, за которой следует вставленная.
В GetNextRecord имеется следующий код:
if (State = dsInsert) and (FCurrentRecord = FActiveRecord) and
(GetBookmarkFlag(ActiveBuffer) = bfCurrent) then GetMode := gmCurrent;
Если бы вместо bfCurrent стояло fbInserted, эти строки можно было бы понять так: если TDataSet находится в состоянии редактирования, и имеет место случай (1), причем вставленная запись пока отсутствует в наборе, то читается не запись, следующая за той, на которую указывает физический курсор, а именно та, на которую физический курсор и указывает. Следовательно, здесь код TDataSet рассчитывает, что InternalSetToRecord при попытке установки физического курсора на вставленную запись будет устанавливать его на следующую за ней запись.
Однако в исходных текстах TDataSet написано именно bfCurrent, что очень меня удивляет. Возможно, я чего-то не понимаю, но не исключено, что это ошибка разработчиков из Borland. Последствия, к которым приводит эта недоработка, читатель легко может увидеть сам: нужно поместить на форму DBGrid с Align=alClient и DBNavigator с Align=alTop. Высота формы должна быть небольшой, чтобы в DBGrid была видна лишь одна запись БД. Если теперь пользователь добавит запись (щелчок по кнопке из DBNavigator) перед этой видимой записью, а затем увеличит высоту окна, то в DBGrid отобразятся не все записи, следующие за вставленной: не будет той записи, перед которой мы производим вставку. Конечно, необходимо для демонстрации связать с сеткой потомок TDataSet, позволяющий производить вставку в произвольное место набора – например, TADOTable.
Дело в том, что при увеличении высоты DBGrid, последний автоматически увеличит количество буферов в используемом TDataSet, чтобы иметь по одному буферу для каждой из одновременно отображаемых в сетке записей. Для этого будет вызван SetBufferCount, которыйзабудет прочитать одну из записей (ту, перед которой происходит вставка).
Кстати, подобный эффект можно увидеть, если между созданием потомка TDataSet и отображением DBGrid выполнить у TDataSet метод Insert – одна из записей не будет показана в DBGrid.
А вот если бы вместо проверки на bfCurrent выполнялась проверка на bfInserted, все было бы в порядке.
Из этого следует, что идеально правильной работы, возможно, удастся добиться лишь таким образом: записи следует добавлять в набор сразу же при вызове InternalInsert, но не устанавливать для них флаг закладки bfCurrent вместо bfInserted (иначеGetNextRecord будет работать с ошибками в силу все того же фрагмента кода).
Проведенная мною небольшая серия испытаний пока не выявила противопоказаний к такому решению. Однако есть некоторые сомнения – все-таки это вмешательство в обычную схему работы TDataSet. Желающие могут провести самостоятельные эксперименты. Я сделал так: код, добавляющий запись, перенес из InternalPost в InternalInsert, в internalPost действия в состоянии dsInsert сделал такими же, как и в dsEdit, а InternalCancel просто свел к вызову InternalDelete.
Далее в этой работе я все-таки буду придерживаться обычной схемы, несмотря на содержащиеся в ней ошибки. Лучше уж ошибаться вместе со всеми, чем в гордом одиночестве.
Метод Resync, вообще говоря, также работает неправильно, но относительно него в файлах справки Delphi есть предупреждение, гласящее, что Resync предназначен для внутреннего использования и не должен вызываться извне TDataSet напрямую. Сам же TDataSet никогда и не вызывает Resync в состоянии dsInsert. Остается надеяться, что потребители нашего потомка TDataSet учтут предупреждение.
Итак, как же реализовать InternalSetToRecord?
Сложность заключается в том, что к моменту вызова InternalInsert метод UpdateCursorPos не был вызван, а буфер активной ранее записи уже перезаписан вызовом InternalInitRecord, так что, даже если мы вызовем UpdateCursorPos, он попытается вызватьInternalSetToRecord для перемещения физического курсора на запись, соответствующую данному буферу – но буфер-то теперь содержит новую информацию.
Попробуем придумать, как можно реализовать InternalSetToRecord таким образом, чтобы он успешно определял, где именно находится эта новая запись.
Есть два пути. Начнем с простого. Будем хранить в буфере записи значение физического курсора, при котором он (курсор) указывает на эту запись.
В результате выполнения Post или Delete значения курсора, соответствующие записям, считанным в буферы, могут отличаться от значений, сохраненных в этих же буферах, но последующий вызов Resync (которым завершаются Post и Delete) прочитает эти записи заново, устранив расхождение.
Чтобы определить, какая запись была активной до вызова Insert, можно перекрыть метод DoBeforeInsert, который вызывается еще до изменения активного буфера, и в нем тем или иным образом запомнить значение физического курсора. Между вызовами DoBeforeInsert иInternalInsert не выполняется никаких действий, изменяющих положение физического курсора. В случае Append это не так, но тут нам может помочь другое. Если на момент вызова Insert набор не содержит записей, то после InitRecord для новой записи ставится флаг закладки bfBOF. Если вызван Append, ставится флаг fbEOF. В остальных случаях флаг остается таким, каким его сформировал InitRecord, то есть fbInserted. Поэтому при вызове InternalInsert можно поступить так: если флаг не равен bfInserted, то установить значение физического курсора, хранимое в буфере активной записи, так, как это по смыслу нужно для выполнения Append, в противном же случае – равным значению, выясненному в DoBeforeInsert. Конечно, это означает привязку к последовательности вызовов в логике TDataSet, и есть опасность несовместимости с грядущими новыми версиями VCL.
Второй вариант – его использует Borland в своих потомках TDataSet – заключается в сведении InternalSetToRecord к вызовуInternalGotoBookmark. Модификация этого решения используется в компоненте TRxMemoryData из RX Library.
Основания таковы. Если запись имеет флаг закладки bfCurrent, то у нее есть закладка; если флаг bfInserted, то, хотя BookmarkAvailable для нее вернет False, значение закладки все-таки установлено и совпадает с закладкой записи, “отодвигаемой” при вставке; если bfBOF или bfEOF, то нам вообще беспокоиться не о чем – TDataSet вызовет не InternalSetToRecord, а InternalFirst или InternalLast.
В таком подходе есть один недостаток: когда мы отодвигаем одну из записей набора, чтобы вставить новую, закладки записей не должны изменяться – иначе весь механизм закладок окажется лишенным смысла, т.к. клиент не сможет перейти по ранее сохраненной закладке на соответствовавшую ей в момент получения закладки запись. С другой стороны, значения физического курсора, соответствующие отодвигаемым записям, должны измениться. Следовательно, есть два пути. Можно в паре с каждой закладкой хранить информацию (скрытую от внешнего по отношению к нашему потомку TDataSet кода), позволяющую вычислить значение физического курсора. В таком случае при добавлении/удалении записей нужно изменять внутреннее преставление по меньшей мере нескольких, а то и всех, закладок (в зависимости от того, насколько эффективные алгоритмы использовать и какую информацию хранить).
С другой стороны, в случае, если операций вставки производится существенно больше, чем операций поиска по закладке (путем присваивания свойству Bookmark, т.е. не считая вызовов InternalSetToRecord), может оказаться выгоднее (и уж точно – проще) искать записи последовательным перебором, а InternalSetToRecord реализовать так, как описано ранее (DoBeforeInsert и InternalInsert).
Разумеется, дилемма отпадает, если вставку всех записей производить как добавление в конец набора – тогда можно просто принять в качестве закладки значение физического курсора.
Чуть не забыл! Insert и Append – не единственные методы для вставки записей. Если имеется буфер записи, в который уже записаны значения полей, то можно добавить в набор запись с такими значениями полей вызовом InsertRecord. Этот метод обращается кAddRecord, а тот, в свою очередь, к InternalInsertRecord. Такой способ добавления записи тоже нужно обрабатывать.


Теги: 1с программирование самоучитель, коды программирования Borland Delphi

Статьи по теме:

Использование шаблонов с Report Builder
Распределенная обработка данных
Суперклассинг
Свойство Bands для TQRSubDetail
Свойство ParentFont
Панель View
StoreDefs
Native-формат данных
Событие BeforePrint для TQuickRep
Событие AfterPrint для TQuickRep
Метод ApplySettings
Асинхронный режим, основанный на событиях
Расширение QuickReport
Translate
Свойство Expression для TQRGroup
| Borland Delphi | vitek |
 


Пн Вт Ср Чт Пт Сб Вс
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | ДОБАВЛЕНИЕ ВОЗМОЖНОСТЕЙ РЕДАКТИРОВАНИЯ. Регион сайта: Москва и Санкт-Петербург