Wednesday, March 4, 2009

Patching TDataSetProvider

Observação: Se aplica ao Delphi 6 e 7.

Depois de gastar muitas horas implementando alguma "mágica" e novas funcionalidades em um descendente direto do TDataSetProvider que uso em meus sistemas, usando o BDS 2006, parti para implementar as mesmas funcionalidades no TDataSetProvider do Delphi 6 (a empresa na qual trabalho possui sistemas em Delphi 6 cuja migração imediata para um compilador superior é inviável). Simplesmente abri minha unit no Delphi 6 e fui compilar em um projeto vazio e POWWWW!!!
Problema: Não existe o método DoBeforeUpdateRecord no TBaseProvider (ancestral do TDataSetProvider).
Toda a nova funcionalidade estava baseada em um novo DoBeforeUpdateRecord do TDataSetProvider. O que eu queria fazer é relativamente simples: Fazer algo que o TDataSetProvider padrão não faz, antes do update de cada registro, mais precisamente nos inserts.

O evento DoBeforeUpdateRecord do Provider é chamado pela classe Resolver durante o processo de update (no BDS 2006 em diante). Sem o método DoBeforeUpdateRecord virtual, eu teria que arrumar outra alternativa.
Tentei o InternalApplyUpdates, sem chance! Não tem como fazer o que queria por lá. ApplyUpdates então? Sem chance de novo! O método é estático e mesmo que fosse dinâmico eu teria que desviar a chamada para o evento BeforeUpdateRecord original, uma coisa que não me agradou.....
Tentei durante um bom tempo e sempre esbarrava em métodos estáticos que deveriam ser dinâmicos, protegidos que deveriam ser públicos, propriedades que deviam ser públicas e eram privadas...
Resultado: Não é viável fazer no Delphi 6!!! E então?

Solução: Bem, não gosto de modificar o fonte da VCL, mas neste caso é bem justificável e imprescindível. De quebra ainda corrigiria um bug antigo (http://www.distribucon.com/midasbug/index.aspx).

As modificações são simples, retiradas da própria unit Provider.pas porém da versão BDS 2006. Não têm absolutamente nenhum impacto no funcionamento e abrem grandes possibilidades de customização dos DataSetProviders. O mesmo pode ser feito no Delphi 7, e após o resultado que obtive, eu aconselho.

Segue a lista de modificações que fiz. As linhas adicionadas ou modificadas estão marcadas em azul:


TBaseProvider = class(TCustomProvider)
protected
procedure DoBeforeUpdateRecord(SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean); virtual;
procedure DoAfterUpdateRecord(SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind); virtual;
end;

TUpdateTree = class(TObject)
public
procedure Clear;
function DoUpdates: Boolean;
procedure RefreshData(Options: TFetchOptions);
procedure InitErrorPacket(E: EUpdateError; Response: TResolverResponse);
procedure InitData(ASource: TDataSet);
procedure InitDelta(const ADelta: OleVariant); overload;
procedure InitDelta(ADelta: TPacketDataSet); overload;
property Data: Pointer read FData write FData;
property Delta: TPacketDataSet read FDeltaDS;
property DetailCount: Integer read GetDetailCount;
property Details[Index: Integer]: TUpdateTree read GetDetail;
property ErrorDS: TPacketDataSet read GetErrorDS;
property HasErrors: Boolean read GetHasErrors;
property Name: string read FName write FName;
property Parent: TUpdateTree read FParent;
property Source: TDataSet read FSourceDS;
property IsNested: Boolean read GetIsNested;
end;

TCustomResolver = class(TComponent)
public
property Provider: TBaseProvider read FProvider;
property UpdateTree: TUpdateTree read FUpdateTree;
end;

// Implementation

procedure TBaseProvider.DoBeforeUpdateRecord(SourceDS: TDataSet;
DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
begin
if Assigned(FBeforeUpdateRecord) then
FBeforeUpdateRecord(Self, SourceDS, DeltaDS, UpdateKind, Applied);
end;

procedure TBaseProvider.DoAfterUpdateRecord(SourceDS: TDataSet;
DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind);
begin
if Assigned(FAfterUpdateRecord) then
FAfterUpdateRecord(Self, SourceDS, DeltaDS, UpdateKind);
end;

procedure TDataSetProvider.SetDataSet(ADataSet: TDataSet);
begin
FDataSet := ADataSet;
if Assigned(FDataSet) then
FDataSet.FreeNotification(Self);
end;

function TCustomResolver.InternalUpdateRecord(Tree: TUpdateTree): Boolean;
var
RecNoSave: Integer;
Applied: Boolean;
UpdateKind: TUpdateKind;
E: Exception;
PrevErr, Err: EUpdateError;
begin
PrevErr := nil;
Err := nil;
Tree.Delta.UseCurValues := False;
while True do
try
UpdateKind := Tree.Delta.UpdateKind;
if ((UpdateKind = ukInsert) and (FPrevResponse in [rrMerge, rrApply])) or
((FPrevResponse = rrMerge) and Tree.Delta.HasMergeConflicts) then
DatabaseError(SInvalidResponse);
Applied := False;
RecNoSave := Tree.Delta.RecNo;
try
Provider.DoBeforeUpdateRecord(Tree.Source, Tree.Delta, UpdateKind, Applied); (* ACM patch *)
finally
if Tree.Delta.RecNo <> RecNoSave then
Tree.Delta.RecNo := RecNoSave;
end;
if not Applied then
case UpdateKind of
ukModify:
begin
if poDisableEdits in Provider.Options then
raise Exception.CreateRes(@SNoEditsAllowed);
DoUpdate(Tree);
end;
ukDelete:
begin
if poDisableDeletes in Provider.Options then
raise Exception.CreateRes(@SNoDeletesAllowed);
DoDelete(Tree);
end;
ukInsert:
begin
if poDisableInserts in Provider.Options then
raise Exception.CreateRes(@SNoInsertsAllowed);
DoInsert(Tree);
end;
end;
Provider.DoAfterUpdateRecord(Tree.Source, Tree.Delta, UpdateKind); (* ACM patch *)
if (poPropogateChanges in Provider.Options) and Tree.Delta.NewValuesModified then
LogUpdateRecord(Tree);
Break;
except
E := AcquireExceptionObject;
PrevErr.Free;
PrevErr := Err;
Err := IProviderSupport(Tree.Source).PSGetUpdateException(E, PrevErr);
if HandleUpdateError(Tree, Err, FMaxErrors, FErrorCount) then
begin
Tree.Delta.UseCurValues := True;
Continue;
end else
break;
end;
PrevErr.Free;
Err.Free;
FPrevResponse := rrSkip;
Result := FErrorCount <= FMaxErrors;
end;

Após a alteração no código fonte da VCL (você fez backup do original, certo?) basta salvá-lo, incluí-lo em um projeto e compilar o projeto.
A melhor forma de utilizar patches deste tipo para substituir o código original da DCU que geralmente é linkada ao executável é criar um diretório de patches para o seu Delphi, colocar lá os arquivos fontes modificados (neste caso Provider.pas) e incluir este caminho no LibraryPath do seu IDE.

Em um próximo post vou escrever sobre as modificações que fiz no TDataSetProvider, ou melhor, no descendente dele que uso.

No comments: