2009-12-31 5 views
12

Ho osservato qualcosa la scorsa settimana che non mi aspettavo e descriverò di seguito. Sono curioso di sapere perché questo accade. È qualcosa di interno alla classe TDataSet, un artefatto di TDBGrid o qualcos'altro?Spostare le colonne in un DBGrid sembra spostare i campi DataSet collegati

L'ordine dei campi in un ClientDataSet aperto è stato modificato. Nello specifico, ho creato un ClientDataSet nel codice chiamando CreateDatatSet dopo aver definito la sua struttura usando FieldDefs. Il primo campo nella struttura di ClientDataSet era un campo Data denominato StartOfWeek. Solo pochi istanti dopo, il codice che avevo anche scritto, che presupponeva che il campo StartOfWeek fosse nella posizione zero, ClientDataSet.Fields [0], non era riuscito, poiché il campo StartOfWeek non era più il primo campo in ClientDataSet.

Dopo alcune indagini, ho appreso che era possibile che ogni singolo campo nel ClientDataSet potesse, in un dato momento, apparire in una posizione diversa dalla struttura originale al momento della creazione del ClientDataSet. Non ero a conoscenza che ciò potesse accadere e una ricerca su Google non ha nemmeno accennato a questo effetto.

Quello che è successo non era magia. I campi non cambiano posizione da soli, né cambiano in base a tutto ciò che ho fatto nel mio codice. Ciò che faceva apparire fisicamente i campi per cambiare posizione in ClientDataSet era che l'utente aveva cambiato l'ordine delle Colonne in un DbGrid a cui era collegato il ClientDataSet (tramite un componente DataSource, ovviamente). Ho replicato questo effetto in Delphi 7, Delphi 2007 e Delphi 2010.

Ho creato un'applicazione Delphi molto semplice che dimostra questo effetto. Consiste in un unico modulo con un DBGrid, un DataSource, due ClientDataSet e due pulsanti. Il gestore di eventi OnCreate di questa forma è simile al seguente

procedure TForm1.FormCreate(Sender: TObject); 
begin 
    with ClientDataSet1.FieldDefs do 
    begin 
    Clear; 
    Add('StartOfWeek', ftDate); 
    Add('Label', ftString, 30); 
    Add('Count', ftInteger); 
    Add('Active', ftBoolean); 
    end; 
    ClientDataSet1.CreateDataSet; 
end; 

Button1, che viene etichettato Mostra ClientDataSet Struttura, contiene il seguente gestore di evento OnClick.

procedure TForm1.Button1Click(Sender: TObject); 
var 
    sl: TStringList; 
    i: Integer; 
begin 
    sl := TStringList.Create; 
    try 
    sl.Add('The Structure of ' + ClientDataSet1.Name); 
    sl.Add('- - - - - - - - - - - - - - - - - '); 
    for i := 0 to ClientDataSet1.FieldCount - 1 do 
     sl.Add(ClientDataSet1.Fields[i].FieldName); 
    ShowMessage(sl.Text); 
    finally 
    sl.Free; 
    end; 
end; 

Per dimostrare l'effetto del campo mobile, eseguire questa applicazione e fare clic sul pulsante denominato Mostra struttura ClientDataSet. Si dovrebbe vedere qualcosa simile a quello mostrato qui:

The Structure of ClientDataSet1 
- - - - - - - - - - - - - - - - - 
StartOfWeek 
Label 
Count 
Active 

Avanti, trascinare le colonne del DBGrid di ri-organizzare l'ordine di visualizzazione dei campi. Fare di nuovo clic sul pulsante Mostra struttura ClientDataSet. Questa volta si vedrà qualcosa di simile a quello mostrato qui:

The Structure of ClientDataSet1 
- - - - - - - - - - - - - - - - - 
Label 
StartOfWeek 
Active 
Count 

Ciò che è notevole su questo esempio è che le colonne della DBGrid vengono spostati, ma v'è un effetto apparente sulla posizione dei campi nella ClientDataSet, tale che il campo che era nella posizione ClientDataSet.Field [0] in un punto non è necessariamente lì qualche istante dopo. E, sfortunatamente, questo non è chiaramente un problema di ClientDataSet. Ho eseguito lo stesso test con TTables basati su BDE e AdoTable basati su ADO e ho ottenuto lo stesso effetto.

Se non è necessario fare riferimento ai campi in ClientDataSet visualizzati in un controllo DBGrid, non è necessario preoccuparsi di questo effetto. Per il resto di voi, posso pensare a diverse soluzioni.

Il modo più semplice, ma non necessario, per evitare questo problema è impedire all'utente di riordinare i campi in un oggetto DBGrid. Questo può essere fatto rimuovendo il flag dgResizeColumn dalla proprietà Options di DBGrid. Sebbene questo approccio sia efficace, elimina un'opzione di visualizzazione potenzialmente preziosa, dal punto di vista dell'utente.Inoltre, la rimozione di questo flag non limita solo il riordino della colonna, ma impedisce il ridimensionamento della colonna. (Per informazioni su come limitare il riordino della colonna senza rimuovere l'opzione di ridimensionamento della colonna, vedere http://delphi.about.com/od/adptips2005/a/bltip0105_2.htm.)

La seconda soluzione è evitare di fare riferimento ai campi di un DataSet in base alla loro posizione letterale (poiché questa è l'essenza del problema). Nelle parole ordine, se è necessario fare riferimento al campo Conteggio, non utilizzare DataSet.Fields [2]. Se conosci il nome del campo, puoi utilizzare qualcosa come DataSet.FieldByName ('Count').

C'è tuttavia un grosso svantaggio nell'uso di FieldByName. In particolare, questo metodo identifica il campo iterando attraverso la proprietà Fields del DataSet, cercando una corrispondenza in base al nome del campo. Poiché esegue questa operazione ogni volta che si chiama FieldByName, questo è un metodo da evitare nelle situazioni in cui è necessario fare riferimento al campo più volte, ad esempio in un ciclo che consente di esplorare un DataSet di grandi dimensioni.

Se si ha bisogno di fare riferimento al campo più volte (e un gran numero di volte), considerare l'utilizzo di qualcosa come il seguente frammento di codice:

var 
    CountField: TIntegerField; 
    Sum: Integer; 
begin 
    Sum := 0; 
    CountField := TIntegerField(ClientDataSet1.FieldByName('Count')); 
    ClientDataSet1.DisableControls; //assuming we're attached to a DBGrid 
    try 
    ClientDataSet1.First; 
    while not ClientDataSet1.EOF do 
    begin 
     Sum := Sum + CountField.AsInteger; 
     ClientDataSet1.Next; 
    end; 
    finally 
    ClientDataSet1.EnableControls; 
    end; 

C'è una terza soluzione, ma questo è disponibile solo quando il DataSet è un ClientDataSet, come nel mio esempio originale. In tali situazioni, è possibile creare un clone del ClientDataSet originale e avrà la struttura originale. Di conseguenza, qualsiasi campo creato nella posizione zero sarà ancora in quella posizione, indipendentemente da ciò che un utente ha fatto a un DBGrid che visualizza i dati ClientDataSets.

Questo è dimostrato nel seguente codice, che è associato al gestore di eventi OnClick del pulsante denominato Show Cloned ClientDataSet Structure.

procedure TForm1.Button2Click(Sender: TObject); 
var 
    sl: TStringList; 
    i: Integer; 
    CloneClientDataSet: TClientDataSet; 
begin 
    CloneClientDataSet := TClientDataSet.Create(nil); 
    try 
    CloneClientDataSet.CloneCursor(ClientDataSet1, True); 
    sl := TStringList.Create; 
    try 
     sl.Add('The Structure of ' + CloneClientDataSet.Name); 
     sl.Add('- - - - - - - - - - - - - - - - - '); 
     for i := 0 to CloneClientDataSet.FieldCount - 1 do 
     sl.Add(CloneClientDataSet.Fields[i].FieldName); 
     ShowMessage(sl.Text); 
    finally 
     sl.Free; 
    end; 
    finally 
    CloneClientDataSet.Free; 
    end; 
end; 

Se si esegue questo progetto e fare clic sul pulsante etichettato Mostra Cloned ClientDataSet Struttura, avrai sempre la vera struttura del ClientDataSet, come mostrato qui

The Structure of ClientDataSet1 
- - - - - - - - - - - - - - - - - 
StartOfWeek 
Label 
Count 
Active 

Addendum:

E ' è importante notare che l'effettiva struttura dei dati sottostanti non è influenzata. In particolare, se, dopo aver modificato l'ordine delle colonne in un oggetto DBGrid, si chiama il metodo SaveToFile di ClientDataSet, la struttura salvata è la struttura originale (true internal). Inoltre, se si copia la proprietà Data di un ClientDataSet su un altro, il ClientDataSet di destinazione mostra anche la struttura vera (che è simile all'effetto osservato quando una sorgente ClientDataSet è clonata).

Analogamente, le modifiche agli ordini colonna di DBGrid associati ad altri Dataset testati, inclusi TTable e AdoTable, non influiscono in realtà sulla struttura delle tabelle sottostanti. Ad esempio, un TTable che visualizza i dati dalla tabella di esempio sample.db di Paradox fornita con Delphi in realtà non cambia la struttura di quella tabella (né te la aspetti che lo aspetti).

Ciò che possiamo concludere da queste osservazioni è che la struttura interna del DataSet stesso rimane intatta. Di conseguenza, devo supporre che ci sia una rappresentazione secondaria della struttura del DataSet da qualche parte. E, deve essere associato al DataSet (che sembrerebbe essere eccessivo, poiché non tutti gli usi di un DataSet hanno bisogno di questo), associato a DBGrid (che ha più senso dal momento che il DBGrid utilizza questa funzione, ma che non è supportato dall'osservazione che il riordino di TField sembra persistere con il DataSet stesso) o è qualcos'altro.

Un'altra alternativa è che l'effetto è associato a TGridDataLink, che è la classe che fornisce ai controlli multirow-aware (come i DBGrid) la loro consapevolezza dei dati. Tuttavia, sono propenso a respingere anche questa spiegazione, poiché questa classe è associata alla griglia, e non al DataSet, ancora una volta poiché l'effetto sembra persistere con le classi DataSet stesse.

Che mi riporta alla domanda originale. Questo effetto è qualcosa di interno alla classe TDataSet, un artefatto di TDBGrid o qualcos'altro?

Permettetemi anche di sottolineare qualcosa qui che ho aggiunto a uno dei commenti seguenti. Più di ogni altra cosa, il mio post è progettato per rendere gli sviluppatori consapevoli del fatto che quando utilizzano DBGrids i cui ordini di colonne possono essere modificati, anche l'ordine dei TField potrebbe cambiare. Questo artefatto può introdurre bug intermittenti e seri che possono essere molto difficili da identificare e risolvere. E, no, non penso che questo sia un insetto Delphi. Sospetto che tutto funzioni come è stato progettato per funzionare. È solo che molti di noi non erano consapevoli che questo comportamento stava accadendo. Ora sappiamo.

+3

Molto informativo, ma c'è una domanda qui da qualche parte? –

+0

Grazie a @Cary, non ne avevo idea e sto usando DataSet.Field [x] molto spesso. Penso che dovresti segnalarlo sul sito di Embarcadero come un bug. – Wodzu

+0

C'è una domanda, che appare nella seconda frase: "È qualcosa di interno alla classe TDataSet, un artefatto di TDBGrid o qualcos'altro?"Trascorro un po 'di tempo (circa un'ora) alla ricerca sia della sorgente TCustomGrid che di TDataSet, ma non ho visto dove stia accadendo Ancora più importante, ed è per questo che il mio post è così lungo, volevo almeno fare Gli sviluppatori di Delphi sono consapevoli di questo comportamento interessante: per chiunque utilizzi un controllo DBGrid o un'altra griglia simile che produce queste modifiche nell'ordine TField, potrebbe essere una fonte di un errore intermittente e di difficile individuazione. –

risposta

3

Apparentemente il comportamento è di progettazione. In realtà non è correlato al dbgrid. È semplicemente un effetto collaterale di una colonna che imposta un indice di campo. Ad esempio questa affermazione,

ClientDataSet1.Fields [0] .Index: = 1;

causerà l'uscita del pulsante "Mostra struttura ClientDataSet" per modificare di conseguenza, sia che vi sia una griglia o meno. La documentazione per gli stati di TField.Index;

"Modificare l'ordine della posizione di un campo nel set di dati modificando il valore dell'indice. La modifica del valore dell'indice influisce sull'ordine in cui i campi vengono visualizzati nelle griglie di dati, ma non sulla posizione dei campi nelle tabelle del database fisico. "

Si dovrebbe concludere anche il contrario e modificare l'ordine dei campi in una griglia dovrebbe causare la modifica degli indici di campo.


Il codice che causa questo è in TColumn.SetIndex. TCustomDBGrid.ColumnMoved imposta un nuovo indice per la colonna spostata e TColumn.SetIndex imposta il nuovo indice per il campo di quella colonna.

procedure TColumn.SetIndex(Value: Integer); 
[...] 
     if (Col <> nil) then 
     begin 
      Fld := Col.Field; 
      if Assigned(Fld) then 
      Field.Index := Fld.Index; 
     end; 
[...] 
1

Cary Penso di aver trovato una soluzione per questo problema. Invece di utilizzare i campi wrapper VCL, è necessario utilizzare una proprietà Fields interna dell'oggetto COM Recordset.

Ecco come dovrebbe fare riferimento:

qry.Recordset.Fields.Item[0].Value 

Questi campi non sono influenzati dal comportamento che avete descritto in precedenza. Quindi possiamo ancora fare riferimento ai campi dal loro indice.

Provalo e dimmi quale è stato il risultato. Ha funzionato per me.

Edit:

Naturalmente funziona solo per i componenti ADO, non per il TClientDataSet ...

Edit2:

Cary Non so se questa è la risposta alla tua domanda, comunque ho spinto gente nei forum di embarcadero e Wayne Niddery mi ha dato una risposta abbastanza dettagliata su tutto questo movimento Fields.

Per farla breve: Se si definiscono le colonne in TDBGrid in modo esplicito, gli indici di campo non si spostano! Hai un po 'più senso ora, vero?

Leggi discussione completa: https://forums.embarcadero.com/post!reply.jspa?messageID=197287

+0

La soluzione è buona, in quanto è ancora possibile fare affidamento su specifici TField in base alla loro posizione fisica nota nella struttura del DataSet sottostante. La limitazione è, come hai sottolineato, che funziona solo per ADO DataSet. Tuttavia, mi hai inviato di nuovo alla ricerca di un membro corrispondente della classe TFields. Questa classe ha un metodo FieldByNumber, che è invariante rispetto all'ordine delle colonne in una griglia associata. FieldByNumber è come la proprietà dell'oggetto. Ho aggiunto una risposta alla domanda che spiega FieldByNumber in modo più dettagliato di quanto non sia disponibile qui. –

+0

Leggendo tra le righe, possiamo concludere che il DBGrid è la fonte di questo effetto, interagendo in qualche modo con i TField del DataSet. Accetterò la tua risposta sulla base di questa conclusione. Ma penso che possiamo ancora scavare più a fondo. Voglio sapere di più sul meccanismo preciso che è responsabile, in quanto potrebbe essere la fonte di altri effetti collaterali poco noti. Grazie, Dimitrij –

1

Wodzu inviato una soluzione al problema campo riordinato che era specifico per ADO DataSet, ma lui mi ha portato a una soluzione che è simile, ed è disponibile per tutti i set di dati (sia che si tratti è implementato correttamente in tutti i DataSets è un altro problema). Nota che né questa risposta, né quella di Wodzu, sono in realtà una risposta alla domanda originale. Invece, è una soluzione al problema osservato, mentre la domanda si riferisce a dove questo manufatto ha origine.

La soluzione che la soluzione Wodzu mi ha portato era FieldByNumber, ed è un metodo della proprietà Fields. Ci sono due aspetti interessanti nell'uso di FieldByNumber. Innanzitutto, devi qualificare il suo riferimento con la proprietà Fields del tuo DataSet. In secondo luogo, a differenza dell'array Fields, che utilizza un indicizzatore basato su zero, FieldByNumber è un metodo che accetta un parametro a un punto per indicare la posizione del TField a cui si desidera fare riferimento.

Quanto segue è una versione aggiornata del gestore di eventi Button1 che ho inserito nella mia domanda originale. Questa versione utilizza FieldByNumber.

procedure TForm1.Button1Click(Sender: TObject); 
var 
    sl: TStringList; 
    i: Integer; 
begin 
    sl := TStringList.Create; 
    try 
    sl.Add('The Structure of ' + ClientDataSet1.Name + 
     ' using FieldByNumber'); 
    sl.Add('- - - - - - - - - - - - - - - - - '); 
    for i := 0 to ClientDataSet1.FieldCount - 1 do 
     sl.Add(ClientDataSet1.Fields.FieldByNumber(i + 1).FieldName); 
    ShowMessage(sl.Text); 
    finally 
    sl.Free; 
    end; 
end; 

Per il progetto di esempio, questo codice produce il seguente output, indipendentemente dall'orientamento delle colonne nella DBGrid associato:

The Structure of ClientDataSet1 using FieldByNumber 
- - - - - - - - - - - - - - - - - 
StartOfWeek 
Label 
Count 
Active 

Per ripetere, notare che il riferimento al TField sottostante necessaria FieldByNumber da qualificare con un riferimento a Fields. Inoltre, il parametro per questo metodo deve essere compreso nell'intervallo da 1 a DataSet.FieldCount. Di conseguenza, per fare riferimento al primo campo nel DataSet, è possibile utilizzare il seguente codice:

ClientDataSet1.Fields.FieldByNumber(1) 

Come la matrice Fields, FieldByNumber restituisce un riferimento TField. Di conseguenza, se si desidera fare riferimento a un metodo specifico per una determinata classe TField, è necessario eseguire il cast del valore restituito nella classe appropriata. Ad esempio, per salvare il contenuto di un TBlobField a un file, potrebbe essere necessario fare qualcosa come il seguente codice:

TBlobField(MyDataSet.Fields.FieldByNumber(6)).SaveToFile('c:\mypic.jpg'); 

Si noti che non sto suggerendo che si dovrebbe fare riferimento tfields in un DataSet utilizzando letterali interi. Personalmente, l'uso di una variabile TField che viene inizializzata tramite una chiamata una tantum a FieldByName è più leggibile ed è immune ai cambiamenti nell'ordine fisico della struttura di una tabella (sebbene non sia immune alle modifiche nei nomi dei campi!).

Tuttavia, se si dispone di DataSet associati a DBGrid di cui è possibile riordinare le colonne e si fa riferimento ai campi di questi DataSet utilizzando valori letterali integer come indicizzatori dell'array Fields, è possibile prendere in considerazione la conversione del codice per utilizzare il DataSet. Metodo Fields.FieldByName.

+2

Questo vince "Domanda Delphi più lunga ancora" su Stack Overflow. :-) –

+0

@Cary, per favore guarda la mia risposta aggiornata. C'è un'altra soluzione per questo "problema". – Wodzu