Сообщений 1 Оценка 150 Оценить |
Структура DB-Aware компонентов Класс TDataLink Выбор способа организации канала данных. Разработка компонентов, отображающих набор данных Разработка компонентов, модифицирующих набор данных |
В этот раз мы бы хотели рассказать о процессе разработки компонентов для работы с наборами данным. Данная статья совсем не случайно является завершающей в нашем цикле, ведь для построения DB-Aware компонента, как вы сами убедитесь чуть позже, необходимы навыки создания как визуальных контролов, так и невизуальных компонентов, диалогов и т.д. Рэй Конопка назвал процесс создания таких компонент преобразованием. Почему? Об этом ниже.
Компоненты, работающие с данными, часто называют DB-Aware-компонентами (далее DB-A). Их отличительной особенностью является возможность подключения к какой-либо части базы данных, скажем, к таблице или отдельному полю. Создание компонентов для работы с данными не является сложной задачей. В отличие от других категорий компонентов, DB-A не имеют единой иерархии классов. Процесс создания таких компонентов может быть определен как преобразование какого-либо элемента управления в элемент управления, работающий с данными.
Для осуществления этого преобразования необходимо всего лишь включить в имеющийся класс компонента дополнительное поле и написать несколько методов, обеспечивающих связь между компонентом и набором данных.
Другими словами, чтобы создать DB-A, нужно подобрать наиболее соответствующий нашим требованиям компонент, после чего описать поведенческую модель связки компонент-данные. Необходимо заметить, что никаких особенных требований к имеющемуся компоненту не предъявляется, это может быть графический или оконный элемент управления, невизуальный компонент, диалог и так далее.
В зависимости от возможностей DB-A можно условно разделить на две категории:
Естественно, что первый тип компонентов устроен проще, чем второй.
В общем случае схема построения компонентов, работающих с данными, может быть представлена следующим образом (рисунок 1):
Рисунок 1
Перейдем к рассмотрению реализации DataLink. Для нее существует соответствующий класс TDataLink, ему посвящен следующий раздел.
Класс TDataLink является базовым классом для иерархии классов связи с БД. Класс обеспечивает коммуникационный канал для чтения/записи в БД.
Данный класс описан в модуле DB, а его интерфейсная часть выглядит следующим образом (private-секция опущена):
TDataLink = class(TPersistent) protected procedure ActiveChanged; virtual; procedure CheckBrowseMode; virtual; procedure DataEvent(Event: TDataEvent; Info: Longint); virtual; procedure DataSetChanged; virtual; procedure DataSetScrolled(Distance: Integer); virtual; procedure EditingChanged; virtual; procedure FocusControl(Field: TFieldRef); virtual; function GetActiveRecord: Integer; virtual; function GetBOF: Boolean; virtual; function GetBufferCount: Integer; virtual; function GetEOF: Boolean; virtual; function GetRecordCount: Integer; virtual; procedure LayoutChanged; virtual; function MoveBy(Distance: Integer): Integer; virtual; procedure RecordChanged(Field: TField); virtual; procedure SetActiveRecord(Value: Integer); virtual; procedure SetBufferCount(Value: Integer); virtual; procedure UpdateData; virtual; property VisualControl: Boolean read FVisualControl write FVisualControl; public constructor Create; destructor Destroy; override; function Edit: Boolean; function ExecuteAction(Action: TBasicAction): Boolean; dynamic; function UpdateAction(Action: TBasicAction): Boolean; dynamic; procedure UpdateRecord; property Active: Boolean read FActive; property ActiveRecord: Integer read GetActiveRecord write SetActiveRecord; property BOF: Boolean read GetBOF; property BufferCount: Integer read FBufferCount write SetBufferCount; property DataSet: TDataSet read GetDataSet; property DataSource: TDataSource read FDataSource write SetDataSource; property DataSourceFixed: Boolean read FDataSourceFixed write FDataSourceFixed; property Editing: Boolean read FEditing; property Eof: Boolean read GetEOF; property ReadOnly: Boolean read FReadOnly write SetReadOnly; property RecordCount: Integer read GetRecordCount; end; |
Теперь подробно остановимся на ключевых свойствах:
Свойства | Описание |
---|---|
property Active | Показывает, активен ли набор данных для данного TDataLink, если Active равно False, то набор не может ни читать, ни записывать данные в базу данных. |
property ActiveRecord | Номер текущей записи в наборе. Для ненаправленных наборов данных всегда равен 0 |
property BOF | Флаг активности первой записи в наборе данных |
property BufferCount | Количество кэшированных записей, для ненаправленных наборов - всегда равен 0. |
property DataSet | Набор данных, которым управляет данный TDataLink. |
property DataSource | Определяет объект TDataSource, который используется владельцем TDataLink для связи с базой данныхTDataLink также отвечает за обработку всех событий поступающих с набора данных. |
property DataSourceFixed | В некоторых случаях необходимо запретить смену набора данных (например, во время проведения некоторых операций). По умолчанию DataSourceFixed равно False |
property Editing | Набор находится в состоянии редактирования |
property Eof | Активна последняя запись в наборе |
property ReadOnly | Установка режима «Только для чтения» для набора данных |
property RecordCount | Количество записей в наборе |
Основные методы TDataLink:
Методы | Описание |
---|---|
procedure ActiveChanged; virtual; | Вызывается тогда, когда набор данных изменил статус активности. Для TDataLink данный метод имеет пустую реализацию (как и большинство нижеследующих). Если компонент не должен обслуживать данное событие, просто не перекрывайте данный метод в потомке TDataLink. Это относится практически ко всем событиям, описанным далее |
procedure CheckBrowseMode; virtual; | Срабатывает ДО возникновения события, которое может изменить набор данных. |
procedure DataEvent(Event: TDataEvent; Info: Longint); virtual; | Вызывается при возникновении многих событий набора данных. type TDataEvent = (deFieldChange, deRecordChange, deDataSetChange, deDataSetScroll, deLayoutChange, deUpdateRecord, deUpdateState, deCheckBrowseMode, dePropertyChange, deFieldListChange, deFocusControl, deParentScroll, deConnectChange);procedure DataEvent(Event: TDataEvent; Info: Longint); virtual;DataSetChanged вызывается автоматически при возникновении различных событий. Event – содержит тип произошедшего события, а Info предоставляет дополнительную информацию о нем.DataSetChanged обрабатывает данные события путем вызова соответствующих методов, как показано ниже: |
procedure DataSetChanged; | DataSetChanged – отвечает за изменения содержимого набора данных. Все события, которые изменяют содержимое набора данных (активизация режима редактирования набора данных, вставка или удаление записей) форсируют вызов данного метода. События, специфичные представлению данных в наборе в контексте data-aware компонента (скроллинг, смена расположения информации) также ведут к вызову данного метода. Тем не менее функциональность этого метода сводится к тому, чтобы просто вызватьметод RecordChanged. Классы-наследники могут перекрыть данный метод с целью обеспечения дополнительной функциональности. |
procedure DataSetScrolled(Distance: Integer); | Реагирует на изменения положения активной записи в наборе (скроллинг ) |
procedure EditingChanged; virtual; | Реакция на смену режима редактирования |
procedure FocusControl(Field: TFieldRef); virtual; | Форсирует установку фокуса на элемент управления, связанный с TDataLink. Естественно, в базовом классе метод не имеет реализации. |
procedure LayoutChanged; virtual; | Вызывает метод DataSetChanged. LayoutChanged призван обеспечить механизм для вызова метода, который обеспечит представление данных после изменения набора данных data-aware компонентом.Хорошим примером выступает изменения порядка следования столбцов в TCustomDBGrid. |
function MoveBy(Distance: Integer): Integer; virtual; | The MoveBy изменяет активную запись связанного набора данных в соответствии с параметром Distance. Если он больше 0, MoveBy активизирует соответствующую запись после текущей, иначе – до текущей. MoveBy возвращает количество записей, на которые произошло перемещение (данное значение может быть меньше Distance).MoveBy вызывает метод MoveBy связанного набора данных. |
procedure RecordChanged(Field: TField); virtual; | Вызывается при изменении содержимого текущей записи. Срабатывает после того, как выполнена операция Post набора данных.Параметр Field показывает какое именно поле изменило значение. Если Field=nil – любое количество полей или записей могли изменить значения. |
Основной особенностью данного класса является то, что создавать его экземпляр Вам не придется. Подтверждением этого служит тот факт, что почти все методы TDataLink вынесены в секцию Protected.
При построении собственных компонентов используются потомки класса, например, TFieldLink. Ниже приведена классовая иерархия потомков TDataLink (рисунок 2):
Рисунок 2
Кратко опишем назначение классов-наследников TDataLink.
Класс | Описание |
---|---|
TFieldDataLink | Связь с полем таблицы БД. |
TListSourceLinkTDataSourceLink | Связь для организации Lookup полей |
TNavDataLink | Связь для навигатора под набору данных (TDBNavigator) |
TDetailDataLinkTMasterDataLink | Связь для управления наборами данных, связанными отношениями главный – подчиненный. |
Как было сказано выше, существует множество готовых каналов связи для различных целей. Таким образом, для организации связи вашего компонента с набором данных всегда можно взять готовый класс. В этом случае при создании компонента необходимо назначить динамически обработчики событий выбранного канала данных.
Давайте рассмотрим, как реализована эта концепция в одном из наиболее часто используемых классов – TDEdit, «усеченный» вариант описания которого приведен ниже:
TDBEdit = class(TCustomMaskEdit) private FDataLink: TFieldDataLink; FCanvas: TControlCanvas; // вырезано для экономии места procedure ActiveChange(Sender: TObject); procedure DataChange(Sender: TObject); procedure EditingChange(Sender: TObject); procedure SetDataField(const Value: string); procedure SetDataSource(Value: TDataSource); procedure SetFocused(Value: Boolean); procedure SetReadOnly(Value: Boolean); procedure UpdateData(Sender: TObject); procedure WMCut(var Message: TMessage); message WM_CUT; procedure WMPaste(var Message: TMessage); message WM_PASTE; procedure WMUndo(var Message: TMessage); message WM_UNDO; procedure CMEnter(var Message: TCMEnter); message CM_ENTER; procedure CMExit(var Message: TCMExit); message CM_EXIT; procedure WMPaint(var Message: TWMPaint); message WM_PAINT; procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK; protected procedure Change; override; function EditCanModify: Boolean; override; procedure KeyDown(var Key: Word; Shift: TShiftState); override; procedure KeyPress(var Key: Char); override; procedure Loaded; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure Reset; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; function UseRightToLeftAlignment: Boolean; override; property Field: TField read GetField; // вырезано для экономии места published // вырезано для экономии места property DataField: string read GetDataField write SetDataField; property DataSource: TDataSource read GetDataSource write SetDataSource; property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False; end; |
Как было сказано выше, канал связи создается в конструкторе. Методы обслуживания событий набора данных реализованы в виде обработчиков событий.
constructor TDBEdit.Create(AOwner: TComponent); begin inherited Create(AOwner); inherited ReadOnly := True; ControlStyle := ControlStyle + [csReplicatable]; FDataLink := TFieldDataLink.Create; FDataLink.Control := Self; FDataLink.OnDataChange := DataChange; FDataLink.OnEditingChange := EditingChange; FDataLink.OnUpdateData := UpdateData; FDataLink.OnActiveChange := ActiveChange; end; |
Как видно из описания конструктора, в компоненте активно используется динамическое назначение событий, что в принципе не является хорошим тоном в программировании компонентов. Однако в этом случае поле FDataLink является приватным и переназначить события пользователю не удастся.
Список событий TFieldDataLink приведен в таблице:
Обработчик | Описание |
---|---|
OnDataChange | Изменения данных вызванные одной из следующих причин:- Переход к новой записи или столбцу- Переход в режим редактирования набора данных- Изменения свойств DataSource или DataField -Восстановление старых значений вызовом Cancel |
OnEditingChange | Вход или выход в/из режима редактирования набора данных |
OnUpdateData | Запись изменений в БД |
OnActiveChange | Изменение свойства Active подключенного набора данных |
В силу своей простоты такой способ организации DB-AWARE-компонентов является оптимальным при создании достаточно простых решений, однако он имеет несколько недостатков, среди которых:
Вторым распространенным подходом является написание своего канала связи для компонентов. Это намного более трудоемкая процедура, но она того стоит. Дальнейшие примеры этой главы демонстрируют разработку DB-Aware-компонентов именно таким способом.
Компоненты этого типа рода являются наиболее простыми для понимания, однако их разработка требует четкого понимания деталей работы коммуникационного канала между набором данных и компонентом.
В библиотеке стандартных компонентов Delphi есть интересный контрол – DBRadioGroup, в качестве значений для радиокнопок он использует список типа TStrings, который можно инициализировать как на этапе проектирования, так и на этапе разработки. Однако достаточно часто (например, при разработке систем тестирования) необходимо этот список инициализировать из БД, что данный компонент делать не в состоянии.
Мы напишем RadioGroup-компонент, который сможет заполнять список радиокнопок согласно данным из поля таблицы.
Рисунок 3
В данном случае удобнее всего начать разработку с наследника класса TDataLink, поэтому рассмотрим его объявление:
type TmmDBListRadioGroup = class; TmmRGListDataLink = class(TFieldDataLink) private FRadioGroup: TmmDBListRadioGroup; protected procedure ActiveChanged; override; procedure RecordChanged(Field: TField); override; function ValidListSource: Boolean; function ValidListField: Boolean; public constructor Create(AOwner: TmmDBListRadioGroup); reintroduce; virtual; end; |
В первую очередь обратим внимание на упреждающее объявление класса TmmDBListRadioGroup. Согласно структуре DB-Aware компонента, оба класса, как класс связи с данными, так и сам компонент, должны «знать» друг о друге.
Далее следует обратить внимание на конструктор. Директива reintroduce говорит о том, что унаследованный от базового класса конструктор (без параметров) нужно заменить на новый, при этом в качестве параметра новому конструктору передается ссылка на управляемый компонент.
Теперь рассмотрим реализацию класса связи с данными:
procedure TmmRGListDataLink.ActiveChanged; begin inherited; FRadioGroup.RefreshButtons; end; constructor TmmRGListDataLink.Create(AOwner: TmmDBListRadioGroup); begin inherited Create; FRadioGroup := AOwner; end; procedure TmmRGListDataLink.RecordChanged(Field: TField); begin inherited; if Assigned(Field) and (Field = DataSet.FindField(FieldName)) then FRadioGroup.RefreshButtons; end; function TmmRGListDataLink.ValidListField: Boolean; begin Result := ValidListSource and Assigned(DataSet.FindField(FieldName)) end; function TmmRGListDataLink.ValidListSource: Boolean; begin Result := Assigned(DataSource) and Assigned(DataSet) end; |
Наш компонент должен работать в двух режимах – обновление списка радиокнопок и непосредственная работа пользователя с компонентом. Именно поэтому нужно четко определить, когда мы будем обновлять список кнопок. В нашем примере мы делаем это только при открытии/закрытии набора данных, а также при модификации поля, значения из которого берутся в качестве значений радиокнопок.
Метод RecordChanged требует достаточно аккуратного обращения – он вызывается очень часто (при перемещении от одной записи к другой, смене состояния набора данных и т.д.). В некотором смысле данный метод напоминает контроллер обслуживания события движения мышкой, так как оба должны обслуживать огромное число событий и, естественно, должны работать очень быстро.
Теперь наступило время рассмотреть компонент, который будет отображать данные:
mmDBListRadioGroup = class(TCustomRadioGroup) private FListLink: TmmRGListDataLink; FListField: string; procedure SetListField(const Value: string); procedure SetListSource(const Value: TDataSource); function GetListSource: TDataSource; protected procedure RefreshButtons; virtual; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; property Items; published property ListSource: TDataSource read GetListSource write SetListSource; property ListField: string read FListField write SetListField; // published props property Align; property Anchors; property BiDiMode; property Caption; property Color; property Columns; property Ctl3D; property DragCursor; property DragKind; property DragMode; property Enabled; property Font; property ItemIndex; property Constraints; property ParentBiDiMode; property ParentColor; property ParentCtl3D; property ParentFont; property ParentShowHint; property PopupMenu; property ShowHint; property TabOrder; property TabStop; property Visible; property OnClick; property OnContextPopup; property OnDragDrop; property OnDragOver; property OnEndDock; property OnEndDrag; property OnEnter; property OnExit; property OnStartDock; property OnStartDrag; end; constructor TmmDBListRadioGroup.Create(AOwner: TComponent); begin inherited; FListLink := TmmRGListDataLink.Create(Self); end; destructor TmmDBListRadioGroup.Destroy; begin FListLink.Free; inherited; end; function TmmDBListRadioGroup.GetListSource: TDataSource; begin Result := FListLink.DataSource; end; procedure TmmDBListRadioGroup.RefreshButtons; var VOldPlace: TBookmark; begin if FListLink.ValidListSource and FListLink.DataSet.Active then with FListLink.DataSet do begin DisableControls; FListLink.FieldName := FListField; if FListLink.ValidListField then begin VOldPlace := FListLink.DataSet.GetBookmark; Items.Clear; First; while not Eof do begin Items.Add(FieldValues[ListField]); Next; end; FListLink.DataSet.GotoBookmark(VOldPlace); end; EnableControls; end else begin Enabled := False; Items.Clear; end; end; procedure TmmDBListRadioGroup.SetListField(const Value: string); begin if FListField <> Value then begin FListField := Value; RefreshButtons; end; end; procedure TmmDBListRadioGroup.SetListSource(const Value: TDataSource); begin FListLink.DataSource := Value; RefreshButtons; end; |
Замечания к коду:
Замечания по поводу правил хорошего тона:
Ранее мы рассмотрели компонент, который может отображать данные из набора, теперь попробуем создать еще один компонент, который сможет модифицировать набор данных.
Когда набор данных не находится в режиме редактирования, то компонент не может быть активным, однако он обязан синхронизировать положение выбранной кнопки со значением поля данных набора:
Рисунок 4
Когда же набор данных переходит в режим редактирования или добавления новой записи компонент принимает следующий вид и позволяет автоматически занести выбранное значение в поле данных:
Рисунок 5
Для этого нам необходимо ввести еще один канал данных, ориентированный уже на модификацию данных.
В качестве базового класса для компонента возьмем уже разработанный в предыдущем примере класс, который обеспечивает базовую функциональность отображения данных.
Давайте рассмотрим реализацию канала связи для модификации данных :
TmmDBLookUpRadioGroup = class; TmmRGWriteDataLink = class(TFieldDataLink) private FRadioGroup: TmmDBLookUpRadioGroup; protected procedure DataSetScrolled(Distance: Integer); override; procedure EditingChanged; override; function EditState: Boolean; function ValidDataSource: Boolean; function ValidDataField: Boolean; public constructor Create(AOwner: TmmDBLookUpRadioGroup); reintroduce; virtual; end; { TmmRGWriteDataLink } constructor TmmRGWriteDataLink.Create(AOwner: TmmDBLookUpRadioGroup); begin inherited Create; FRadioGroup := AOwner; EditingChanged; end; procedure TmmRGWriteDataLink.DataSetScrolled(Distance: Integer); begin inherited; if ValidDataField then with FRadioGroup do ItemIndex := Items.IndexOf(DataSet.FieldByName(FieldName).AsString); end; procedure TmmRGWriteDataLink.EditingChanged; begin inherited; FRadioGroup.Enabled:=EditState; end; function TmmRGWriteDataLink.EditState: Boolean; begin Result := ValidDataField and (DataSet.State in [DsEdit, dsInsert]) end; function TmmRGWriteDataLink.ValidDataField: Boolean; begin Result := ValidDataSource and Assigned(DataSet.FindField(FieldName)); end; function TmmRGWriteDataLink.ValidDataSource: Boolean; begin Result := Assigned(DataSource) and Assigned(DataSet); end; |
Особого внимания заслуживают следующие методы:
procedure TmmRGWriteDataLink.DataSetScrolled (Distance: Integer) – в режиме просмотра набора данных списка значений обеспечивает автоматическую синхронизацию выбранной кнопки со значением ключевого поля.
procedure TmmRGWriteDataLink.EditingChanged – реагирует на активизацию / деактивацию режимов набора данных (вставка, редактирование)ипереводит компонент в режим модификации набора данных.
Теперь можно перейти к рассмотрению самого компонента:
TmmDBLookUpRadioGroup = class(TmmDBListRadioGroup) private FDataLink: TmmRGWriteDataLink; procedure SetDataField(const Value: string); procedure SetDataSource(const Value: TDataSource); function GetDataField: string; function GetDataSource: TDataSource; protected procedure Click; override; public constructor Create(AOwner: TComponent); override; published property DataSource: TDataSource read GetDataSource write SetDataSource; property DataField: string read GetDataField write SetDataField; end; TmmDBLookUpRadioGroup = class(TmmDBListRadioGroup) private FDataLink: TmmRGWriteDataLink; procedure SetDataField(const Value: string); procedure SetDataSource(const Value: TDataSource); function GetDataField: string; function GetDataSource: TDataSource; protected procedure Click; override; public constructor Create(AOwner: TComponent); override; published property DataSource: TDataSource read GetDataSource write SetDataSource; property DataField: string read GetDataField write SetDataField; end; { TmmDBLookUpRadioGroup } procedure TmmDBLookUpRadioGroup.Click; begin inherited; if FDataLink.EditState then with FDataLink do Field.Value := Items[ItemIndex]; end; constructor TmmDBLookUpRadioGroup.Create(Aowner: TComponent); begin inherited; FDataLink := TmmRGWriteDataLink.Create(Self); Enabled:=False; end; function TmmDBLookUpRadioGroup.GetDataField: string; begin Result := FDataLink.FieldName; end; function TmmDBLookUpRadioGroup.GetDataSource: TDataSource; begin Result := FDataLink.DataSource; end; procedure TmmDBLookUpRadioGroup.SetDataField(const Value: string); begin FDataLink.FieldName := Value; end; procedure TmmDBLookUpRadioGroup.SetDataSource(const Value: TDataSource); begin FDataLink.DataSource := Value; end; |
В данном случае возникла всего одна сложность – TCustomRadioGroup на первый взгляд не имеет метода, который был бы контроллером события пользовательского ввода.
Решить эту проблему помогло изучение исходных кодов TCustomRadioGroup. Обратите особое внимание на реализацию метода Click, который собственно и выполняет необходимые нам действия.
Данная статья завершает курс о разработке пользовательских компонентов Delphi. Мы надеемся, что предоставленный нами материал оказался для вас полезным. Материал для этого курса взят из нашей книги «Создание компонентов в среде Delphi. Руководство разработчика», которая выйдет примерно в начале осени.
Сообщений 1 Оценка 150 Оценить |