Autor Beitrag
Stevie
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 85

Windows 7
Delphi XE Professional
BeitragVerfasst: Do 28.04.11 23:55 
Wer schon mit .Net zu tun hatte, wird sie kennen und schätzen gelernt haben: Data Bindings.
Man kann dort nahezu alles aneinander binden. Sei es, um Listen von Objekten in Trees, Lists oder Grids darzustellen oder Eigenschaften von Objekten in Text-, Check- oder Comboboxen editierbar zu machen.
Oder sogar die Sichtbarkeit oder das Aussehen einzelner Controls zu beinflussen.

Wie das genau funktioniert, kann man in der MSDN nachlesen.

Ja und? In Delphi gibts DataSet, DataSource und diverse datensensitive Controls, um das zu bewerkstelligen!
Richtig, die Sache hat nur einen Haken: Man kann nicht einfach ein 08/15 Objekt daran hängen. Sicher, es gibt TSnapObjectDataset oder spezielle Controls, mit denen man das machen kann. Hat aber den entscheidenden Nachteil, dass man immer entweder besondere Controls oder Komponenten haben muss oder seine bindbaren Objekte von einer Basisklasse ableiten muss.

Ich hab mir mal einige Dinge von der .Net Implementierung abgeschaut und das ganze in Delphi mit folgenden Zielsetzung nachgebaut:
  • Standard Komponenten und Controls müssen unterstützt werden (z.B. TEdit)
  • wenig bis keine extra Implementierung für bindbare Objekte
  • minimaler Sourcecode, um Objekte aneinander zu binden


Ok, fangen wir an. Was brauchen wir?

Ein Binding braucht immer eine Source und ein Target (beides ein Objekt) und die Namen der zu bindenden Eigenschaften.
Für unser erstes Beispiel bauen wir uns eine einfache Klasse, die 2 Eigenschaften hat:

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
type
  TSettings = class
  private
    FCaption: string;
    FColor: TColor;
    procedure SetCaption(const Value: string);
    procedure SetColor(const Value: TColor);
  public
    property Caption: string read FCaption write SetCaption;
    property Color: TColor read FColor write SetColor;
  end;

implementation

procedure TSettings.SetCaption(const Value: string);
begin
  FCaption := Value;
end;

procedure TSettings.SetColor(const Value: TColor);
begin
  FColor := Value;
end;


Wir haben hier schonmal explizite Setter Methoden - die brauchen wir später noch. Aber für das erste Beispiel würde auch in direkter Zugriff auf die Feldvariable genügen.

So, was machen wir nun mit unserem TSettings Objekt? Wir erzeugen 2 Bindings, die auf den Titel und die Farbe unserer MainForm gehen.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
procedure TMainForm.FormCreate(Sender: TObject);
begin
  FSettings := TSettings.Create;
  FSettings.Caption := 'Binding demo';
  FSettings.Color := clGray;
  TBinding.Create(FSettings, 'Caption', MainForm, 'Caption');
  TBinding.Create(FSettings, 'Color', MainForm, 'Color');
end;


Dazu genügt es, die Unit System.Bindings einzubinden. Auch um das Freigeben der Binding Objekte braucht man sich nicht kümmern solange mindestens eins der beiden verbundenen Objekte von TComponent abgeleitet ist (in unserem Fall ist es das MainForm).

Starten wir die Anwendung und sehen, was passiert. Titel und Farbe des MainForms ist entsprechend der Werte aus unserem TSettings Objekts gesetzt worden. Toll, oder?
Naja, da hätts auch das explizite Zuweisen der Werte getan... Klar, aber der eigentliche Clou der Bindings kommt erst noch!

Packen wir mal einen Button auf die Form und setzen dort die Eigenschaft unseres Settings Objekts:

ausblenden Delphi-Quelltext
1:
2:
3:
4:
procedure TMainForm.Button1Click(Sender: TObject);
begin
  FSettings.Caption := 'Current time: ' + DateTimeToStr(Now);
end;


Da tut sich erstmal nix, aber wir sind auch noch nicht fertig. Gehen wir zurück zu unserer TSettings Klasse und passen sie etwas an.
Wir leiten sie mal von einem Basis Objekt ab, welches ein Interface implementiert - und zwar INotifyPropertyChanged. Auch das ist wieder brav von .Net abgekupfert.
Aha, nun brauchen wir also unsere Setter Methoden für die Eigenschaften.

So sieht unserer modifizierte Klasse nun aus:

ausblenden Delphi-Quelltext
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:
type
  TSettings = class(TPropertyChangedBase)
  private
    FCaption: string;
    FColor: TColor;
    procedure SetCaption(const Value: string);
    procedure SetColor(const Value: TColor);
  public
    property Caption: string read FCaption write SetCaption;
    property Color: TColor read FColor write SetColor;
  end;

implementation

procedure TSettings.SetCaption(const Value: string);
begin
  FCaption := Value;
  DoPropertyChanged('Caption');
end;

procedure TSettings.SetColor(const Value: TColor);
begin
  FColor := Value;
  DoPropertyChanged('Color');
end;


Tadaa, und auf Knopfdruck wird nun auch der Titel unserer MainForm geändert. Naja, ganz putzig, aber so richtig rocken tut's noch nicht, oder?

Na gut, werfen wir mal ein TEdit auf die Form und binden es ebenfalls im Konstruktor des MainForms an die Caption Eigenschaft unseres TSettings Objekts:

ausblenden Delphi-Quelltext
1:
2:
...
TBinding.Create(FSettings, 'Caption', Edit3, 'Text');


Toll, nun steht der Titel des Forms auch im Edit drin... und wenn ich dort drin änder passiert nix!
Stimmt, es fehlt noch was. Als letzte Unit (genauer gesagt nach den Units, in denen die Controls drin sind, an die ich binden möchte) im uses des Interface Teils des Forms muss die Unit System.Bindings.Controls.
Dort befinden sich einige Ableitungen von Standard VCL Controls mit dem gleichen Namen, welche das INotifyPropertyChanged Interface implementieren. Und dadurch, dass sie als letzte (oder zumindest nach den entsprechenden Units steht) werden beim Erstellen des Forms Objekte von den abgeleiteten Klassen erstellt. Das heißt im Klartext, es müssen keine speziellen Controls oder Komponenten auf der Form verbaut werden, damit man das Data Binding nutzen kann.

Starten wir mal die Anwendung und tippern ein wenig im Edit rum. Wow, der Titel des Forms ändert sich!

Um es nochmal zu betonen:
  • keine speziellen Controls nötig (nur das Subclassing von Controls, die man Binding fähig machen möchte)
  • keine speziellen Events nötig
  • als read-once Source können direkte Ableitungen von TObject benutzt werden (lediglich für die Benachrichtigung von Eigenschaftsänderungen ist das Interface nötig)
  • minimaler Sourcecode, um Objekte, Komponenten und Controls zu verbinden.


Im angehangenden Beispiel sind noch einige weitere Bindings und Controls auf der Form, mit welchen man einige Eigenschaften verändern kann.
Das ganze wurde unter Delphi XE entwickelt. Delphi 2010 könnte auch funktionieren, aber da waren die Generics noch ein wenig verbuggt. Alle Versionen darunter sind leider nicht unterstützt. Das liegt am regen Gebrauch von Generics und neuer RTTI.

Außerdem findet ihr in den benötigten Units noch weitere Dinge wie zum Beispiel Multicast Events oder den NotificationHandler, der dafür zuständig ist, dass die TBinding Objekte ohne Zutun freigegeben werden.

In Planung und teilweise schon realisiert sind Bindings für Listen in dementsprechenden Controls (z.B. ListBox)

Außerdem werde ich in den nächsten Tagen hier noch etwas zu den Validations und ValueConvertern der Bindings schreiben, die jetzt bereits vorhanden sind und funktionieren.

Anregungen, Kritik und Fragen sind herzlich willkommen und sehr erwünscht.

... to be continued

P.S.: Crosspost bei Delphi Praxis
Einloggen, um Attachments anzusehen!
_________________
“Simplicity, carried to the extreme, becomes elegance.” Jon Franklin
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 18602
Erhaltene Danke: 1597

W10 x64 (Chrome, IE11)
Delphi 10.1 Ent, Oxygene, C# (VS 2015), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Fr 29.04.11 08:37 
Grundsätzlich ist die Idee schon interessant. in Delphi habe ich das eigentlich aber nicht wirklich vermisst. Aber das muss ich mir zu Hause einmal genauer anschauen, heute Morgen bin ich nur kurz dazu gekommen.

Ich wollte aber etwas zu den Multicastevents schreiben: Die hatte ich nämlich schon implementiert, allerdings ein wenig anders als du. ;-)
Ich habe das so umgesetzt, dass es ein Event-Objekt gibt, in dem das auslösende Element usw. drin steht. Zusätzlich kann man eigene Klassen davon ableiten, so dass man sehr einfach beliebige Daten an das Event mitgeben kann.

Deine Lösung mit Assembler hat den Nachteil, dass das mit XE2 und 64-Bit bzw. anderen Plattformen schon wieder nicht mehr funktionieren wird.
Stevie Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 85

Windows 7
Delphi XE Professional
BeitragVerfasst: Fr 29.04.11 08:55 
user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Ich wollte aber etwas zu den Multicastevents schreiben: Die hatte ich nämlich schon implementiert, allerdings ein wenig anders als du. ;-)
Ich habe das so umgesetzt, dass es ein Event-Objekt gibt, in dem das auslösende Element usw. drin steht. Zusätzlich kann man eigene Klassen davon ableiten, so dass man sehr einfach beliebige Daten an das Event mitgeben kann.

Deine Lösung mit Assembler hat den Nachteil, dass das mit XE2 und 64-Bit bzw. anderen Plattformen schon wieder nicht mehr funktionieren wird.

Es gibt bestimmt Duzende verschiedene Implementierungen von Multicastevents. Die Implementierung, die ich gewählt habe (und welche auch nicht komplett selber entwickelt ist, sondern auf Allen Bauers 3teiliger Blog Serie basiert), hat 2 entscheidende Vorteile, die ich zumindest bei anderen Implementierungen nicht gesehen hatte:
  • Multicasts können einfach an normale "herkömmliche" Events angehangen werden ohne extra einen Eventhandler zu schreiben.
  • Es muss sich im besten Fall nicht um ein Objekt gekümmert werden, welches erstellt und freigegeben werden muss

Außerdem sehe ich nicht, dass ich irgendwas ableiten müsste, um zusätzliche Parameter an das Event zu übergeben, dadurch, dass es generisch ist. Einfach ein neuen Eventtypen erstellen und ihn nutzen.

Bezüglich des ASM kann ich nur sagen: Schau mal in die RTTI und sonstige Sourcen von Delphi, wo mit proxy classes, method interception und sonstigem gearbeitet wird (vor allem die RTTI.pas und ObjAuto.pas). Da wird auch ASM genutzt, weils nunmal nicht anders geht. Und wenn dann mal XE2 und Co rauskommen, kann man sich immernoch die paar Zeilen Code vornehmen und ggf anpassen (ich bin kein ASM Experte, aber das meiste davon dürfte nichtmal betroffen sein, weils nix anders is, als Params in Register und Stack packen)

_________________
“Simplicity, carried to the extreme, becomes elegance.” Jon Franklin