![]() |
Delphi-Version: XE
MVC + Observer Pattern Konzept / Was haltet Ihr davon
Hallo Zusammen,
ich habe mir mal zum Thema MVC + Observer-Pattern einen konzeptionellen Entwurf überlegt ... Grundüberlegung war folgende: Im View werden unterschiedliche Events ausgelöst, über den Controller werden diese dann ausgewertet und die entsprechende Funktion werden dafür aufgerufen. Die Funktionen werden zuvor am jeweiligen Controller registriert. Verwendungsmöglichkeit: einfaches EventDispatching (Komponentenunabhängig), z.B. GUI-Update Unabhänig davon sollen Datenmodelle mit "integrierten Observer Pattern" implenetiert werden können. Verwendungsmöglichkeit: Bei Datenänderung im Model werden entsprechende EventHandlers ausgelöst. Ich finde das charmante daran ist die Event-Trennung (Kapselung) - Controller gesteuerte Events (MVC) + auto. Model-gesteuerte Events (ObserverPattern) Was haltet Ihr davon? - möchte nur der "Betriebsblindheit" mal vorbeugen ;) VIEW
Delphi-Quellcode:
CONTROLLER
unit main;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, event.fam.types, model.stockpile, controller, StdCtrls, ExtCtrls; type TForm1 = class(TForm) LabelEventHandlerDebug: TLabel; RadioGroup1: TRadioGroup; RadioButton1: TRadioButton; RadioButton2: TRadioButton; Button1: TButton; procedure FormCreate(Sender: TObject); procedure SetApplicationBackground; procedure Button1Click(Sender: TObject); procedure RadioButton1Click(Sender: TObject); procedure EventHandlerViewLabelUpdate; procedure EventHandlerModelStockpileUpdate; procedure RadioButton2Click(Sender: TObject); private { Private-Deklarationen } controller: TController; FAMEvents: TEvents; StockpileModel: TStockpileModel; // Stockpile model public { Public-Deklarationen } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var data: TArray<Double>; begin StockpileModel.SetData(data); end; procedure TForm1.FormCreate(Sender: TObject); begin { Create eventtype } FAMEvents := TEvents.Create(); { Create controller instances } controller := TController.Create(); { Register Event-Handler } controller.OnUpdateUI := EventHandlerViewLabelUpdate; controller.OnApplicationBackground := SetApplicationBackground; { Create object instances } StockpileModel := TStockpileModel.Create(); { Register Event-Handler for StockpileModel } StockpileModel.registerOn(EventHandlerModelStockpileUpdate); end; procedure TForm1.RadioButton1Click(Sender: TObject); begin controller.DispatchEvent(FAMEvents.Name.OnUpdateUI); end; procedure TForm1.RadioButton2Click(Sender: TObject); begin controller.DispatchEvent(FAMEvents.Name.OnApplicationBackground); end; procedure TForm1.SetApplicationBackground; begin self.Color := clBlue; end; procedure TForm1.EventHandlerModelStockpileUpdate; begin ShowMessage('EventHandlerModelStockpileUpdate'); end; procedure TForm1.EventHandlerViewLabelUpdate; begin LabelEventHandlerDebug.caption := ('TListener has been OnUpdateUI.'); end; end.
Delphi-Quellcode:
Model
unit controller;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, event.fam.types; type { Define a procedural type } TFAMEvent = procedure of object; TController = class private FUpdateUI: TFAMEvent; FApplicationBackground: TFAMEvent; public FAMEvents: TEvents; Constructor Create; procedure dispatchEvent(const FAMEventName: String); property OnUpdateUI: TFAMEvent read FUpdateUI write FUpdateUI; property OnApplicationBackground: TFAMEvent read FApplicationBackground write FApplicationBackground; end; implementation { TController } constructor TController.Create; begin { Create eventtype } FAMEvents := TEvents.Create(); end; procedure TController.dispatchEvent(const FAMEventName: String); begin if ((FAMEventName = FAMEvents.Name.OnUpdateUI) and Assigned(FUpdateUI)) then FUpdateUI(); if ((FAMEventName = FAMEvents.Name.OnApplicationBackground) and Assigned(OnApplicationBackground)) then OnApplicationBackground(); end; end.
Delphi-Quellcode:
MODEL (Eltern - Klasse)
unit model.stockpile;
interface uses model; type { Stockpile Model } TStockpileModel = class(TModel) private data: TArray<Double>; public function GetData: TArray<Double>; procedure SetData(data: TArray<Double>); end; implementation { TStockpileModel } function TStockpileModel.GetData: TArray<Double>; begin result := self.data; end; procedure TStockpileModel.SetData(data: TArray<Double>); begin self.data := data; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen // wurde mit registerOn an das Model regestriert notify; end; end.
Delphi-Quellcode:
unit model;
interface type TEvent = procedure of object; TModel = class(tObject) protected // interne Liste OnChange: array of TEvent; // Aufruf aller Routinen der Liste procedure notify; public // neuer 'Event-Handler' in Liste procedure registerOn(routine: TEvent); // 'Event-Handler' aus Liste entfernen procedure registerOff(routine: TEvent); end; implementation // registriert neue routinen an den controller procedure TModel.registerOn(routine: TEvent); var n: integer; begin n := Length(OnChange); SetLength(OnChange, n + 1); OnChange[n] := routine; end; // de-registriert routinen vom controller procedure TModel.registerOff(routine: TEvent); var i, j: integer; begin i := Low(OnChange); while i <= High(OnChange) do // High liefert -1 bei leerem Array begin if @OnChange[i] = @routine // mit '@' nur Adressen vergleichen then begin for j := i to High(OnChange) - 1 do OnChange[j] := OnChange[j + 1]; SetLength(OnChange, Length(OnChange) - 1); end else i := i + 1; end; end; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen procedure TModel.notify; var i: integer; begin for i := Low(OnChange) to High(OnChange) do OnChange[i]; end; end. Events-Types
Delphi-Quellcode:
unit event.fam.types;
interface type Events = record onUpdateUI: string; onApplicationBackground: string; end; TEvents = class private FEvents: Events; public Constructor Create; property Name: Events read FEvents; end; implementation { EventTypes } constructor TEvents.Create; var _FEvents: Events; begin _FEvents.onUpdateUI := 'onUpdateUI'; _FEvents.onUpdateUI := 'onApplicationBackground'; FEvents := _FEvents; Finalize(_FEvents); FillChar(_FEvents, SizeOf(_FEvents), 0); end; end. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Liste der Anhänge anzeigen (Anzahl: 1)
Das ist irgendwie kein MVC, sondern irgendwas ... Nur weil man da etwas Model-View-Controller benennt, wird es noch kein MVC.
Schau dir mal an wie bei apple mit MVC gearbeitet wird, dann bekommt man eine ungefähre Vorstellung. Da hat der Controller jedes Control auf der View und auch da wird das erst benannt.
Delphi-Quellcode:
Problematisch ist und bleibt aber das vernünftige Umsetzen. Um es richtig zu machen müsste der Controller die Controls erzeugen und die View müsste sich nur noch merken an welcher Stelle diese Controls dargestellt werden sollen. Wenn das gesamte Framework dafür vorbereitet ist, dann ist alles ganz einfach. Wenn nicht, dann fängt man quasi bei Adam und Eva an.
TViewController = class
public property Firstname : TEdit; property Lastname : TEdit; end; TView = class( TForm ) Edit1 : TEdit; // -> ViewController.Firstname Edit2 : TEdit; // -> ViewController.Lastname end; Da ist das MVVM schon "wesentlich einfacher" umzusetzen und kommt deinem Entwurf auch wesentlich näher. Grundlegend bei den Entwürfen ist aber, dass die Views nicht die ViewModels/Controller erzeugen, denn sonst geht der gesamte Vorteil der Testbarkeit sofort flöten und die Views übernehmen auf einmal wieder die Kontrolle. Ich kann dir mal ein kleines Beispiel zeigen, wie ich das mit MVVM mache:
Genauso wie das ActivityViewModel der ActivityView zugewiesen wird. Im Anhang habe ich den gesamten restlichen Source (exclusive den Basis-Units) und die ausführbare Exe angehängt Zum besseren Verständnis hier einmal die BaseView
Delphi-Quellcode:
und natürlich die ViewModelBase
unit MVVM.View.FMX.Form.Base;
interface uses de.itnets.Events, de.itnets.References, MVVM.Core, MVVM.ViewModel.ViewModelBase, System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs; type TViewBaseForm = class( TForm, IView ) private FViewModel: WeakRef<TViewModelBase>; function GetViewModel: TViewModelBase; protected procedure ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); virtual; procedure AttachToViewModel( AViewModel: TViewModelBase ); virtual; procedure DetachFromViewModel( AViewModel: TViewModelBase ); virtual; public procedure SetViewModel( AViewModel: TViewModelBase ); virtual; procedure BeforeDestruction; override; function Equals( Obj: TObject ): Boolean; override; end; var ViewBaseForm: TViewBaseForm; implementation {$R *.fmx} { TForm1 } procedure TViewBaseForm.AttachToViewModel( AViewModel: TViewModelBase ); begin AViewModel.PropertyChanged.Add( Self.ViewModelPropertyChanged ); end; procedure TViewBaseForm.BeforeDestruction; begin SetViewModel( nil ); inherited; end; procedure TViewBaseForm.DetachFromViewModel( AViewModel: TViewModelBase ); begin AViewModel.PropertyChanged.Remove( Self.ViewModelPropertyChanged ); end; function TViewBaseForm.Equals( Obj: TObject ): Boolean; begin Result := ( Self = Obj ) or Assigned( Obj ) and {} ( ( Obj is TViewBaseForm ) and ( Self.FViewModel = ( Obj as TViewBaseForm ).FViewModel ) ) {} or {} ( Self.FViewModel.IsAssigned and Self.FViewModel.Reference.Equals( Obj ) ); end; function TViewBaseForm.GetViewModel: TViewModelBase; begin Result := FViewModel; end; procedure TViewBaseForm.SetViewModel( AViewModel: TViewModelBase ); begin if FViewModel <> AViewModel then begin if FViewModel.IsAssigned then DetachFromViewModel( FViewModel ); FViewModel := AViewModel; if FViewModel.IsAssigned and not( csDestroying in Self.ComponentState ) then begin AttachToViewModel( FViewModel ); PropertyChangedEvent.Call( ViewModelPropertyChanged, FViewModel, TPropertyChangedArgs.Create( ) ); end; end; end; procedure TViewBaseForm.ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); begin // Nothing to do here end; end.
Delphi-Quellcode:
PS Ein für mich sehr wichtiger Punkt ist die Unterstützung von allen Plattformen mit so wenig Anpassungen wie möglich. Dieser Code läuft ohne Änderungen exakt gleich auf Windows, OSX und Android (iOS nicht getestet, aber da befürchte ich eigentlich keine großen Überraschungen)
unit MVVM.ViewModel.ViewModelBase;
interface uses de.itnets.Events, de.itnets.References; type TViewModelBase = class private FPropertyChanged: PropertyChangedEvent; FDisplayName: string; function GetPropertyChanged: IPropertyChangedEvent; protected procedure SetDisplayName( const Value: string ); procedure OnPropertyChanged( const PropertyName: string = '' ); overload; procedure OnPropertyChanged( const PropertyNames: TArray<string> ); overload; public property DisplayName: string read FDisplayName; property PropertyChanged: IPropertyChangedEvent read GetPropertyChanged; end; TViewModelClass = class of TViewModelBase; implementation { TViewModel } function TViewModelBase.GetPropertyChanged: IPropertyChangedEvent; begin Result := FPropertyChanged; end; procedure TViewModelBase.OnPropertyChanged( const PropertyNames: TArray<string> ); var LPropertyName: string; begin for LPropertyName in PropertyNames do begin OnPropertyChanged( LPropertyName ); end; end; procedure TViewModelBase.OnPropertyChanged( const PropertyName: string ); begin FPropertyChanged.Invoke( Self, TPropertyChangedArgs.Create( PropertyName ) ); end; procedure TViewModelBase.SetDisplayName( const Value: string ); begin if FDisplayName <> Value then begin FDisplayName := Value; OnPropertyChanged( 'DisplayName' ); end; end; end. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
danke für deine Antwort. Was die Testbarkeit angeht, verstehe ich dich nicht ganz. Warum sollte diese flöten gehen...? ich habe doch mit diesem Konzept die BusinessLogik sauber von der GUI (View) getrennt. oder nicht?:shock: |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
Mein MainViewModel kann ich testen ohne irgendeine View im Spiel zu haben.
Delphi-Quellcode:
Ich kann auch die gesamte Anwendung durchlaufen lassen ohne ein einziges View zu erzeugen, denn die View wird erzeugt, wenn es ein ViewModel gibt und nicht umgekehrt, das ViewModel wird erzeugt, wenn es eine View gibt.
procedure Test;
var LVM : TMainViewModel; begin LVM := TMainViewModel.Create; try Assert( LVM.SomeAction.CanExecute ); Assert( not Assigned( LVM.Activity ) ); LVM.SomeAction.Execute; Assert( not LVM.SomeAction.CanExecute ); Assert( Assigned( LVM.Activity ) ); while not LVM.CanClose do Sleep(10); Assert( LVM.SomeAction.CanExecute ); Assert( not Assigned( LVM.Activity ) ); finally LVM.Free; end; end; Du willst aber eine Aktion per Event an die View geben, die dann eine neue View mit Controller erstellt und dann soll dieser Controller irgendwie eingebunden werden. Schwups ist die Abhängigkeit von der View wieder da und wir haben nichts gewonnen, nur mehr Schreibarbeit. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Liste der Anhänge anzeigen (Anzahl: 1)
Richtig spannend wird es, wenn man das erweitern möchte:
z.B. benötigen wir auch eine Aktivitätsanzeige mit einem Fortschritt. Nichts leichter als das
Delphi-Quellcode:
Schon haben wir eine Aktivität mit Fortschritt.
unit ViewModel.ActivityViewModel;
interface uses MVVM.ViewModel.ViewModelBase; type TActivityViewModel = class( TViewModelBase ) private FInfo: string; procedure SetInfo( const Value: string ); public property Info: string read FInfo write SetInfo; end; TProgressActivityViewModel = class( TActivityViewModel ) private FProgress: Single; procedure SetProgress( const Value: Single ); public property Progress: Single read FProgress write SetProgress; end; implementation { TActivityViewModel } procedure TActivityViewModel.SetInfo( const Value: string ); begin if FInfo <> Value then begin FInfo := Value; OnPropertyChanged( 'Info' ); end; end; { TProgressActivityViewModel } procedure TProgressActivityViewModel.SetProgress( const Value: Single ); begin if FProgress <> Value then begin FProgress := Value; OnPropertyChanged( 'Progress' ); end; end; end. Dann mal in das MainViewModel und die Aktivitäten eingebaut (nur die Änderungen)
Delphi-Quellcode:
Da wir
unit ViewModel.MainViewModel;
interface uses System.SysUtils, System.Classes, System.Threading, de.itnets.Commands, de.itnets.References, MVVM.ViewModel.ViewModelBase, ViewModel.WorkspaceViewModel, ViewModel.ActivityViewModel; type TMainViewModel = class( TWorkspaceViewModel ) private FSomeProgressActionCommand: ICommand; FSomeRandomActionCommand: ICommand; function GetSomeProgressActionCommand: ICommand; function GetSomeRandomActionCommand: ICommand; public property SomeProgressActionCommand: ICommand read GetSomeProgressActionCommand; property SomeRandomActionCommand: ICommand read GetSomeRandomActionCommand; end; implementation { TMainViewModel } function TMainViewModel.GetSomeProgressActionCommand: ICommand; begin if not Assigned( FSomeProgressActionCommand ) then FSomeProgressActionCommand := TRelayCommand.Create( procedure var LActivity: AutoRef<TProgressActivityViewModel>; begin SetCanClose( False ); // Aktivitätsanzeige setzen LActivity := TProgressActivityViewModel.Create; LActivity.Reference.Info := 'Performing SomeProgressAction'; SetActivity( LActivity ); // Task starten TTask.Run( procedure var LIdx: Integer; begin LActivity.Reference.Progress := 0; for LIdx := 1 to 10 do begin Sleep( 200 ); LActivity.Reference.Progress := LIdx * 10; end; // Aktivitätsanzeige ausschalten TThread.Synchronize( nil, procedure begin SetActivity( nil ); SetCanClose( True ); end ); end ); end, function: Boolean begin Result := not FActivity.IsAssigned; end ); Result := FSomeProgressActionCommand; end; function TMainViewModel.GetSomeRandomActionCommand: ICommand; begin if not Assigned( FSomeRandomActionCommand ) then FSomeRandomActionCommand := TRelayCommand.Create( procedure var LActivity: AutoRef<TActivityViewModel>; LProgressActivity: AutoRef<TProgressActivityViewModel>; begin SetCanClose( False ); // Aktivitätsanzeige setzen LActivity := TActivityViewModel.Create; LProgressActivity := TProgressActivityViewModel.Create; SetActivity( LActivity ); // Task starten TTask.Run( procedure var LIdx: Integer; begin TThread.Queue( nil, procedure begin LActivity.Reference.Info := 'Init data...'; SetActivity( LActivity ); end ); Sleep( 1000 ); TThread.Queue( nil, procedure begin LProgressActivity.Reference.Info := 'Reading data...'; LProgressActivity.Reference.Progress := 0; SetActivity( LProgressActivity ); end ); for LIdx := 1 to 20 do begin Sleep( 150 ); TThread.Queue( nil, procedure begin LProgressActivity.Reference.Progress := LIdx * 5; end ); end; TThread.Queue( nil, procedure begin LActivity.Reference.Info := 'Cleanup system...'; SetActivity( LActivity ); end ); Sleep( 1000 ); // Aktivitätsanzeige ausschalten TThread.Synchronize( nil, procedure begin SetActivity( nil ); SetCanClose( True ); end ); end ); end, function: Boolean begin Result := not FActivity.IsAssigned; end ); Result := FSomeRandomActionCommand; end; end.
Delphi-Quellcode:
von
TProgressActivityViewModel
Delphi-Quellcode:
abgleitet haben brauchen wir die View nicht ändern um lauffähig zu bleiben. Nur der Progress wird eben nicht angezeigt, aber die Funktionalität bleibt gewahrt.
TActivityViewModel
Ok, dann bauen wir uns eine entsprechende View (Frame mit einer ProgressBar und einem TextFeld) und bringen das auf die MainView. Damit das dann auch genutzt wird benötigen wir diese Änderungen an der View (nur die geänderten Teile)
Delphi-Quellcode:
Im Anhang nur die Exe
unit View.Form.MainView;
interface uses de.itnets.Events, de.itnets.References, MVVM.ViewModel.ViewModelBase, ViewModel.MainViewModel, System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Graphics, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.StdCtrls, View.Form.WorkspaceView, MVVM.View.FMX.Frame.Base, FMX.Objects, System.Actions, FMX.ActnList, FMX.Layouts, View.Frame.AcitivityView, View.Frame.ProgressAcitivityView; type TMainView = class( TWorkspaceView ) ActivityCurtain: TRectangle; ActivityView1: TActivityView; ToolBar1: TToolBar; SpeedButton1: TSpeedButton; ActionList1: TActionList; SomeActionAction: TAction; CloseAction: TAction; SpeedButton2: TSpeedButton; SomeProgressActionAction: TAction; SpeedButton3: TSpeedButton; ProgressActivityView1: TProgressActivityView; SomeRandomActionAction: TAction; SpeedButton4: TSpeedButton; procedure SomeActionActionExecute( Sender: TObject ); procedure SomeActionActionUpdate( Sender: TObject ); procedure CloseActionExecute( Sender: TObject ); procedure CloseActionUpdate( Sender: TObject ); procedure SomeProgressActionActionExecute( Sender: TObject ); procedure SomeProgressActionActionUpdate( Sender: TObject ); procedure SomeRandomActionActionExecute( Sender: TObject ); procedure SomeRandomActionActionUpdate( Sender: TObject ); private FMain: WeakRef<TMainViewModel>; protected procedure AttachToViewModel( AViewModel: TViewModelBase ); override; procedure DetachFromViewModel( AViewModel: TViewModelBase ); override; procedure ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); override; public end; var MainView: TMainView; implementation {$R *.fmx} uses System.StrUtils, ViewModel.ActivityViewModel; { TMainView } procedure TMainView.SomeProgressActionActionExecute( Sender: TObject ); begin inherited; FMain.Reference.SomeProgressActionCommand.Execute; end; procedure TMainView.SomeProgressActionActionUpdate( Sender: TObject ); begin inherited; TAction( Sender ).Enabled := FMain.IsAssigned and FMain.Reference.SomeProgressActionCommand.CanExecute; end; procedure TMainView.SomeRandomActionActionExecute( Sender: TObject ); begin inherited; FMain.Reference.SomeRandomActionCommand.Execute; end; procedure TMainView.SomeRandomActionActionUpdate( Sender: TObject ); begin inherited; TAction( Sender ).Enabled := FMain.IsAssigned and FMain.Reference.SomeRandomActionCommand.CanExecute; end; procedure TMainView.ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); begin inherited; if FMain.IsAssigned then begin if e.Matches( ['Active', 'DisplayName'] ) then begin Caption := FMain.Reference.DisplayName + ' (' + IfThen( FMain.Reference.Active, 'Active', 'Inactive' ) + ')'; end; if e.Match( 'Activity' ) then begin ActivityCurtain.BringToFront; ActivityCurtain.Visible := Assigned( FMain.Reference.Activity ); if FMain.Reference.Activity is TProgressActivityViewModel then begin ActivityView1.Visible := False; ActivityView1.SetViewModel( nil ); ProgressActivityView1.Visible := True; ProgressActivityView1.BringToFront; ProgressActivityView1.SetViewModel( FMain.Reference.Activity ); end else if FMain.Reference.Activity is TActivityViewModel then begin ProgressActivityView1.Visible := False; ProgressActivityView1.SetViewModel( nil ); ActivityView1.Visible := Assigned( FMain.Reference.Activity ); ActivityView1.BringToFront; ActivityView1.SetViewModel( FMain.Reference.Activity ); end else begin ActivityView1.Visible := False; ActivityView1.SetViewModel( nil ); ProgressActivityView1.Visible := False; ProgressActivityView1.SetViewModel( nil ); end; end; end end; end. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
@Sir Rufo,
Ich habe das SimpleForm Beispiel versucht mit XE7 zu öffnen, doch dabei beschwert sich die IDE, das sie nicht alle Dateien finden kann, MVVM.View.FMX.Form.Base.pas, MVVM.View.FMX.Frame.Base.pas ausserdem kann meine Die folgende Units nicht auflösen de.itnets.Events, de.itnets.References, MVVM.Core, MVVM.ViewModel.ViewModelBase, de.itnets.Commands, … Könntest du die restlichen Dateien die zum Kompilieren notwendig sind ebenfalls veröffentlichen? |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
@Thomas_K
Das habe ich auch geschrieben Zitat:
Das Beispiel-Projekt habe ich auch nur veröffentlicht um einen direkten Vergleich zwischen dem vorgestellten Konzept und MVVM zu ermöglichen, zu zeigen, dass MVVM mit Delphi durchaus zuverlässig funktioniert und wie MVVM grundsätzlich funktioniert. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
MVVM ist in DSharp meiner Erfahrung nach am besten implementiert.
Ich hoffe, dass Stefan dieses Jahr etwas mehr Zeit findet, den MVVM-Branch (Caliburn Micro) in DSharp weiterzuentwickeln. ![]() |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Habe das ganze mit dem MVVP Konzept implementiert, das Problem ist jetzt allerdings meine Delphi Version (XE), dort gibt es noch keine Live-Bindings. Kennt jemand ein Workaround um das "zu simulieren"?
|
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
|
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
Für Listen habe ich eine Observable Collection mit einem CollectionChanged Event. Dieser teilt mir genau mit, was sich in der Liste geändert hat. Damit kann ich dann jede andere Liste (Listendarstellung) mit möglichst wenig Stress (für mich und die Darstellung) synchron halten. Der Event liefert mir die Art der Änderung, den jeweiligen Index, wo sich etwas geändert hat und die jeweiligen Items (jeweils für OldItems und NewItems). |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Liste der Anhänge anzeigen (Anzahl: 1)
Zitat:
Einen anderen Gedanken den ich noch hätte wäre folgender (skizze siehe Screen) Anhang 42527 Im prinzip erstelle ich mir ein ScopeModel und immer wenn sich das ViewModel verändert (propertyChanged) rufe ich im Scope-Model die an diesen registrierten Methoden zur GUI (-Elemete) aktualisierung auf... eine Testbarkeit des ViewModels ist somit immer noch komplett unabhängig vom zustand des Views möglich! was haltet ihr davon? |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
Zitat:
|
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Aber du solltest den Scope mit dem ViewModel verbinden und nicht mit dem Model. Das ViewModel kapselt ja genau dieses Model und übersetzt, transferiert oder reichert das um weitere Informationen an.
Eine andere Möglichkeit ist das erstellen von WrapperKomponenten für die jeweiligen Controls. Dann hast du z.B. einen EditWrapper (nenn ihn meinetwegen Presenter) und der bekommt das ViewModel, den Namen der Eigenschaft, sowie einen Accessor (zum Lesen) und Mutuator (zum Schreiben). Klatsch ein Edit daran und schon tauscht das fröhlich aus. Genau sowas benutze ich um einen TreeView an ein Tree_ViewModel zu binden |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
Zitat:
welches Konzept ist für dich besser? |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Liste der Anhänge anzeigen (Anzahl: 1)
Also hier erst mal das manuelle Binding in der View
Delphi-Quellcode:
unit View.MainView;
interface uses de.itnets.Events, de.itnets.References, MVVM.ViewModel.ViewModelBase, ViewModel.MainViewModel, System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Graphics, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.StdCtrls, MVVM.View.FMX.Form.Base, FMX.Controls.Presentation, FMX.Edit; type TMainView = class( TViewBaseForm ) BarEdit: TEdit; { OnChangeTracking -> ControlChanged } FooEdit: TEdit; { OnChangeTracking -> ControlChanged } CheckLabel: TLabel; private FMain: WeakRef<TMainViewModel>; protected procedure AttachToViewModel( AViewModel: TViewModelBase ); override; procedure DetachFromViewModel( AViewModel: TViewModelBase ); override; procedure ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); override; public published procedure ControlChanged( Sender: TObject ); end; var MainView: TMainView; implementation {$R *.fmx} { TMainView } procedure TMainView.AttachToViewModel( AViewModel: TViewModelBase ); begin FMain := AViewModel as TMainViewModel; inherited; end; procedure TMainView.ControlChanged( Sender: TObject ); begin if FMain.IsAssigned then begin FMain.Reference.Bar := BarEdit.Text; FMain.Reference.Foo := FooEdit.Text; end; end; procedure TMainView.DetachFromViewModel( AViewModel: TViewModelBase ); begin inherited; FMain := nil; end; procedure TMainView.ViewModelPropertyChanged( Sender: TObject; const e: TPropertyChangedArgs ); begin inherited; if FMain.IsAssigned then begin if e.Match( 'Bar' ) then BarEdit.Text := FMain.Reference.Bar; if e.Match( 'Foo' ) then FooEdit.Text := FMain.Reference.Foo; if e.Matches( ['Bar', 'Foo'] ) then CheckLabel.Text := Format( 'Bar: "%s", Foo: "%s"', [FMain.Reference.Bar, FMain.Reference.Foo] ); end; end; end.
Delphi-Quellcode:
ist nur für die visuelle Rückmeldung, dass es tatsächlich im ViewModel angekommen ist.
CheckLabel
Im Anhang der Source (immer noch ohne mein Basis-Framework) und eine Exe zum Ausprobieren |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
So ein Edit-Wrapper könnte ungefähr so aussehen
Delphi-Quellcode:
Denkbar ist allerdings auch ein Wrapper nach dieser Art
TEditView = class(Component)
public procedure SetViewModel( AViewModel : TViewModelBae; const PropertyName : string; const AAccessor : TFunc<string>; const AMutuaotr : TProc<string> ); propety Edit : TEdit; end;
Delphi-Quellcode:
Jetzt müsstest du allerdings per RTTI von dem PropertyNamen auf die Property zugreifen, was aber auch machbar ist. Das zusammen mit einem ValueConverter, der dafür sorgt, dass du auch von Boolean zu String und wieder zurück wechseln kannst macht die Sache richtig rund. Das geht dann schon in die Richtung LiveBinding
TEditView = class( TComponent )
public property Edit : TEdit; property PropertyName : string; property ViewModel : TViewModelBase; end; |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
Delphi-Quellcode:
oder übersehe ich da irgendwo was?
ViewModelPropertyChanged
|
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
In Beitrag #2 habe ich am Ende die
Delphi-Quellcode:
und die
TViewModelBase
Delphi-Quellcode:
noch gezeigt, wo man das sehen kann.
TViewBaseForm
Bei der Verwendung dieser Pattern (egal ob MVC, MVP, MVVM, MVVP, ...) ist das A&O OOP und Vererben, bis der Arzt kommt. Dann wird vieles einfacher und schneller und die speziellen Teile sind nicht überfrachtet, weil ja der Basisteil schon im Vorfahr erledigt wird. Oder ich kann einfach erweitern, schon benutzen und trotzdem läuft noch alles (siehe die Erweiterung des Activity-ViewModels auf ProgressActivity-ViewModel). |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zitat:
@Sir: eine Frage noch :) wie kann ich es erreichen das ich bei notify einen parameter mit übergeben kann? nach dem Motto
Delphi-Quellcode:
?
notify(para1);
Delphi-Quellcode:
procedure TStockpileModel.SetData(data: TArray<Double>);
begin self.data := data; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen // wurde mit registerOn an das Model regestriert notify; end; BasisModel Als erstes erstes erstelle ich mir mal eine Basis Model, mit PropertyChanged-Handling
Delphi-Quellcode:
davon abgeleitete Klasse müssen bei Setter-Methoden jetzt die Funktion
TModel.notify
Delphi-Quellcode:
aufrufen
notify
Delphi-Quellcode:
Eigentliches Model
unit model;
interface type TEvent = procedure of object; TModel = class protected // interne Liste OnChange: array of TEvent; // Aufruf aller Routinen der Liste procedure notify; destructor destroy; override; public // neuer 'Event-Handler' in Liste procedure registerOn(routine: TEvent); // 'Event-Handler' aus Liste entfernen procedure registerOff(routine: TEvent); end; implementation // registriert neue routinen an den controller procedure TModel.registerOn(routine: TEvent); var n: integer; begin n := Length(OnChange); SetLength(OnChange, n + 1); OnChange[n] := routine; end; // de-registriert routinen vom controller procedure TModel.registerOff(routine: TEvent); var i, j: integer; begin i := Low(OnChange); while i <= High(OnChange) do // High liefert -1 bei leerem Array begin if @OnChange[i] = @routine // mit '@' nur Adressen vergleichen then begin for j := i to High(OnChange) - 1 do OnChange[j] := OnChange[j + 1]; SetLength(OnChange, Length(OnChange) - 1); end else i := i + 1; end; end; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen destructor TModel.destroy; begin // inherited; end; procedure TModel.notify; var i: integer; begin for i := Low(OnChange) to High(OnChange) do OnChange[i]; end; end.
Delphi-Quellcode:
hier könnte die
TViewModel.SetBar(const Value: String);
Delphi-Quellcode:
Methoden-Aufruf weggelasen wird
notify;
Delphi-Quellcode:
Dann das ViewModel
unit model.stockpile;
interface uses model; type { Stockpile Model } TStockpileModel = class(TModel) private data: TArray<Double>; cdsBioLife: String; function GetBioLife: String; public function GetData: TArray<Double>; procedure SetData(data: TArray<Double>); property BioLife: String read GetBioLife; destructor destroy; override; end; implementation { TStockpileModel } destructor TStockpileModel.destroy; begin inherited; end; function TStockpileModel.GetBioLife: String; begin Result := cdsBioLife; end; function TStockpileModel.GetData: TArray<Double>; begin Result := self.data; end; procedure TStockpileModel.SetData(data: TArray<Double>); begin self.data := data; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen // wurde mit registerOn an das Model regestriert notify; end; end.
Delphi-Quellcode:
Main
unit ViewModel;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, jpeg, StdCtrls, pngimage, JvPanel, JvExExtCtrls, JvExtComponent, Series, TeeShape, TeEngine, TeeProcs, Chart, Math, TeeGDIPlus, ComCtrls, JvExComCtrls, JvComCtrls, model, model.stockpile, controller, dOPCIntf, dOPCComn, dOPCDA, dOPC; implementation type TViewModel = class(TModel) private FModel: TStockpileModel; FBar: String; FFoo: string; function GetBioLifeCDS: String; function GetFishPictures: TBitmap; procedure SetBar(const Value: String); procedure SetFoo(const Value: string); public constructor Create; destructor Destroy; override; property BioLifeCDS: String read GetBioLifeCDS; property FishPictures: TBitmap read GetFishPictures; property Foo: string read FFoo write SetFoo; property Bar: String read FBar write SetBar; end; { TViewModel } constructor TViewModel.Create; begin inherited Create; FModel := TStockpileModel.Create; end; destructor TViewModel.Destroy; begin FModel.Free; inherited; end; procedure TViewModel.SetBar(const Value: String); begin if FBar <> Value then begin FBar := Value; notify; end; end; procedure TViewModel.SetFoo(const Value: string); begin if FFoo <> Value then begin FFoo := Value; notify; // OnPropertyChanged('Foo'); end; end; function TViewModel.GetBioLifeCDS: String; begin Result := FModel.BioLife; end; function TViewModel.GetFishPictures: TBitmap; begin try Result := TBitmap.Create; Result.LoadFromFile (ExpandFileName(IncludeTrailingPathDelimiter(ExtractFileDir(ParamStr(0)) + '') + '../../Assets/Images/sidebar-icon-error.jpg')); finally end; end; end.
Delphi-Quellcode:
....
// Model FViewModel: TStockpileModel; // Model wurde geändert procedure ViewModelPropertyChanged(Sender: TObject); implementation FViewModel := TViewModel.Create; // Wenn model geändert wird -> ViewModelPropertyChanged ausführen FViewModel.registerOn(ViewModelPropertyChanged); // Daten aus dem Model holen und GUI updaten procedure TForm1.ViewModelPropertyChanged(Sender: TObject); begin EditContentbarStartMarker.Text := FViewModel.Foo; end; // Daten ins Model zurückschreiben procedure TForm1.EditContentbarEndMarkerChange(Sender: TObject); begin FViewModel.Foo := TEdit(Sender).Text; end; |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Zum Event-Parameter:
Ich habe mir einen generischen MultiCast-Event geschrieben und davon einen speziellen PropertyChanged-Event erzeugt. Dieser Event hängt an jedem ViewModel (im Model hat der nichts verloren ;)) Siehe die Methode
Delphi-Quellcode:
OnPropertyChanged
Delphi-Quellcode:
Da diese Methode
unit MVVM.ViewModel.ViewModelBase;
interface uses de.itnets.Events, de.itnets.References; type TViewModelBase = class private FPropertyChanged: PropertyChangedEvent; FDisplayName: string; function GetPropertyChanged: IPropertyChangedEvent; protected procedure SetDisplayName( const Value: string ); procedure OnPropertyChanged( const PropertyName: string = '' ); overload; procedure OnPropertyChanged( const PropertyNames: TArray<string> ); overload; public property DisplayName: string read FDisplayName; property PropertyChanged: IPropertyChangedEvent read GetPropertyChanged; end; TViewModelClass = class of TViewModelBase; implementation { TViewModel } function TViewModelBase.GetPropertyChanged: IPropertyChangedEvent; begin Result := FPropertyChanged; end; procedure TViewModelBase.OnPropertyChanged( const PropertyNames: TArray<string> ); var LPropertyName: string; begin for LPropertyName in PropertyNames do begin OnPropertyChanged( LPropertyName ); end; end; procedure TViewModelBase.OnPropertyChanged( const PropertyName: string ); begin // Hier wird jetzt der Event durch die Gegend geschickt FPropertyChanged.Invoke( Self, TPropertyChangedArgs.Create( PropertyName ) ); end; procedure TViewModelBase.SetDisplayName( const Value: string ); begin if FDisplayName <> Value then begin FDisplayName := Value; OnPropertyChanged( 'DisplayName' ); end; end; end.
Delphi-Quellcode:
ist, kann ich die von allen abgeleiteten ViewModels aufrufen.
protected
Warum kein Notify im Model? Wenn sich im Model etwas ändert, dann ist das durch das ViewModel passiert ... ja, äh dann soll bitte auch das ViewModel die Nachricht schicken. Bei mir sind alle Models immutable (unveränderlich). Wenn ich etwas ändere, dann erzeuge ich mir eine neue Model-Instanz mit den aktuellen Werten. Nach dem Speichern sende ich dann diese neue Instanz in die Runde und jeder der sich dafür interessiert, kann dann diese neue Instanz übernehmen. Exakt zu diesem Zweck gibt es die virtuelle Methode ![]()
Delphi-Quellcode:
Das kann ich aber auch überschreiben
function Object.Equals( Obj : TObject ) : Boolean;
begin Result := Self = Obj; end;
Delphi-Quellcode:
TPerson = class
private FID : Integer; FName : string; public constructor Create( const ID : Integer; const Name : string ); function SameEntityAs( Other : TPerson ): Boolean; function Equals( Obj : TObject ) : Boolean; override; property ID : Integer read FID; property Name : string read FName; end; function TPerson.Equals( Obj : TObject ) : Boolean; begin Result := ( Self = Obj ) or Assigned( Obj ) and ( Self.ClassType = Obj.ClassType ) and SameEntityAs( Obj as TPerson ); end; function TPerson.SameEntityAs( Other : TPerson ): Boolean; begin Result := (Self = Other ) or Assigned( Other ) and (Self.FID = Other.FID ); end;
Delphi-Quellcode:
Und noch etwas weitergedacht, kann ich sogar das ViewModel vergleichen und zwar mit einem anderen
procedure Test;
var LPerson1, LPerson2 : TPerson; begin LPerson1 := TPerson.Create( 1, 'Foo' ); LPerson2 := TPerson.Create( 2, 'Foo' ); if LPerson1.Equals( LPerson2 ) then WriteLn('Gleiche Person ist gemeint'); LPerson2 := TPerson.Create( 1, 'Bar' ); if LPerson1.Equals( LPerson2 ) then WriteLn('Gleiche Person ist gemeint'); end;
Delphi-Quellcode:
und einem
TPersonViewModel
Delphi-Quellcode:
:
TPerson
Delphi-Quellcode:
Das auch noch auf die View übertragen und ich kann quasi alles mit jedem vergleichen.
TPersonViewModel = class
private FPerson : TPerson; public function Equals( Obj : TObject ) : Boolean; end; function TPersonViewModel.Equals( Obj : TObject ) : Boolean; begin Result := (Self = Obj ) or Assigned( Obj ) and ( ( ( Self.ClassType = Obj.ClassType ) and ( Self.FPerson.Equals( ( Obj as TPersonViewModel ).FPerson ) ) ) or ( Self.FPerson.Equals( Obj ) ) ); end; |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
hmm irgendwie klappt das doch nicht so wie ich mir das dachte....
wenn ich aus der Main heraus das Model ändere (über die ModelView) dann wird
Delphi-Quellcode:
ausgeführt, wenn ich aber aus dem ModelView das Model ändere passiert nichts... eigentlich logisch und denn wieder auch nicht ;)
TForm1.ViewModelStockpilePropertyChanged
jemand eine idee? ich vermute ich muss noch ein Event vom ModelView in Richtung View dispatchen?! Main
Delphi-Quellcode:
...
// Create reference of ViewModelStockpile (Scope to Model.Stockpile) FViewModelStockpile := TViewModelStockpile.Create; // Register PropertyChanged-Methode if changing the // Datamodel in ViewModelStockpile FViewModelStockpile.registerOn(ViewModelStockpilePropertyChanged); ... procedure TForm1.ViewModelStockpilePropertyChanged; begin EditContentbarStartMarker.Text := FViewModelStockpile.Foo; ShowMessage('Model wurde geändert'); end; ... view.model.stockpile
Delphi-Quellcode:
unit view.model.stockpile;
interface uses model, model.stockpile; type { View Model for Stockpile } TViewModelStockpile = class(TModel) private FModel: TStockpileModel; data: TArray<Double>; cdsBioLife: String; FBar: String; FFoo: string; function GetBioLife: String; procedure SetBar(const Value: String); procedure SetFoo(const Value: string); public constructor Create; function GetData: TArray<Double>; procedure SetData(data: TArray<Double>); property BioLife: String read GetBioLife; destructor destroy; override; property Foo: string read FFoo write SetFoo; property Bar: String read FBar write SetBar; end; implementation { TViewModelStockpile } constructor TViewModelStockpile.Create; begin inherited Create; FModel := TStockpileModel.Create; Foo := 'test'; end; destructor TViewModelStockpile.destroy; begin FModel.Free; inherited; end; procedure TViewModelStockpile.SetBar(const Value: String); begin if FBar <> Value then begin FBar := Value; notify; end; end; procedure TViewModelStockpile.SetFoo(const Value: string); begin if FFoo <> Value then begin FFoo := Value; ShowMessage('notify'); notify; // OnPropertyChanged('Foo'); end; end; function TViewModelStockpile.GetBioLife: String; begin Result := cdsBioLife; end; function TViewModelStockpile.GetData: TArray<Double>; begin Result := self.data; end; procedure TViewModelStockpile.SetData(data: TArray<Double>); begin self.data := data; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen // wurde mit registerOn an das Model regestriert notify; end; end. Model(Base)
Delphi-Quellcode:
unit model;
interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, jpeg, StdCtrls, pngimage, JvPanel, JvExExtCtrls, JvExtComponent, Series, TeeShape, TeEngine, TeeProcs, Chart, Math, TeeGDIPlus, ComCtrls, JvExComCtrls, JvComCtrls, dOPCIntf, dOPCComn, dOPCDA, dOPC; type TEvent = procedure of object; TModel = class protected // interne Liste OnChange: array of TEvent; // Aufruf aller Routinen der Liste procedure notify; destructor destroy; override; public // neuer 'Event-Handler' in Liste procedure registerOn(routine: TEvent); // 'Event-Handler' aus Liste entfernen procedure registerOff(routine: TEvent); end; implementation // registriert neue routinen an den controller procedure TModel.registerOn(routine: TEvent); var n: integer; begin n := Length(OnChange); SetLength(OnChange, n + 1); OnChange[n] := routine; end; // de-registriert routinen vom controller procedure TModel.registerOff(routine: TEvent); var i, j: integer; begin i := Low(OnChange); while i <= High(OnChange) do // High liefert -1 bei leerem Array begin if @OnChange[i] = @routine // mit '@' nur Adressen vergleichen then begin for j := i to High(OnChange) - 1 do OnChange[j] := OnChange[j + 1]; SetLength(OnChange, Length(OnChange) - 1); end else i := i + 1; end; end; // alle Ereignis-Behandlungs-Routinen der Liste aufrufen destructor TModel.destroy; begin // inherited; end; procedure TModel.notify; var i: integer; begin for i := Low(OnChange) to High(OnChange) do OnChange[i]; end; end. |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
fehler gefunden ...
Delphi-Quellcode:
Main
constructor TViewModelStockpile.Create(const EventHandler: TEventHandler);
begin inherited Create; FEventHandler := EventHandler; FModel := TStockpileModel.Create; //Foo := 'TViewModelStockpile.Create => Value1'; <<<<<<<<<<<< FEHLER! end;
Delphi-Quellcode:
läuft im Kreis ...
procedure TForm1.ViewModelStockpilePropertyChanged;
begin EditContentbarStartMarker.Text := FViewModelStockpile.Foo; ShowMessage('Model wurde geändert'); end; |
AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
Tja, was da nicht so richtig tut gute Frage.
Einen Fehler habe ich aber schon entdeckt:
Delphi-Quellcode:
Du vergleichst hier
procedure TEventHandler.setPropertyChanged(const Value: TEvent);
begin if @FPropertyChanged <> @Value then begin ShowMessage('setPropertyChanged'); FPropertyChanged := Value; end; end;
Delphi-Quellcode:
die Adresse der Methode. Das reicht aber nicht aus, denn bei unterschiedlichen Instanzen der gleichen Klasse ist diese Adresse gleich!
if @FPropertyChanged <> @Value then
Du musst das nach ![]()
Delphi-Quellcode:
und
TMethod.Code
Delphi-Quellcode:
vergleichen ;)
TMethod.Data
Meine Erfahrung mit MVVM haben mir allerdings auch gezeigt, dass die normalen Delphi-(SingleCast)-Events da nicht ausreichen und ein MultiCast-Event zwingend erforderlich ist. Ein und dasselbe ViewModel kann an mehreren Views hängen (Liste und Detail) und da wäre es doof, wenn nur eine View aktualisiert wird. |
Alle Zeitangaben in WEZ +1. Es ist jetzt 22:01 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz