AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Sprachen und Entwicklungsumgebungen Object-Pascal / Delphi-Language MVC + Observer Pattern Konzept / Was haltet Ihr davon

MVC + Observer Pattern Konzept / Was haltet Ihr davon

Ein Thema von FAM · begonnen am 5. Feb 2015 · letzter Beitrag vom 6. Feb 2015
Antwort Antwort
Seite 1 von 3  1 23   
FAM

Registriert seit: 22. Dez 2014
47 Beiträge
 
Delphi XE Enterprise
 
#1

MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 12:30
Delphi-Version: XE
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:
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.
CONTROLLER

Delphi-Quellcode:
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.
Model

Delphi-Quellcode:
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.
MODEL (Eltern - Klasse)
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.

Geändert von FAM ( 5. Feb 2015 um 12:35 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Sir Rufo
Sir Rufo

Registriert seit: 5. Jan 2005
Ort: Stadthagen
9.454 Beiträge
 
Delphi 10 Seattle Enterprise
 
#2

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 13:24
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:
TViewController = class
public
  property Firstname : TEdit;
  property Lastname : TEdit;
end;

TView = class( TForm )
  Edit1 : TEdit; // -> ViewController.Firstname
  Edit2 : TEdit; // -> ViewController.Lastname
end;
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.

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:
  • View
    Delphi-Quellcode:
    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, View.Frame.AcitivityView,
      FMX.Objects, System.Actions, FMX.ActnList, FMX.Layouts;

    type
      TMainView = class( TWorkspaceView )
        ActivityCurtain: TRectangle;
        ActivityView1: TActivityView;
        ToolBar1: TToolBar;
        SpeedButton1: TSpeedButton;
        ActionList1: TActionList;
        SomeActionAction: TAction;
        procedure SomeActionActionExecute(Sender: TObject);
        procedure SomeActionActionUpdate(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;

    { TMainView }

    procedure TMainView.AttachToViewModel( AViewModel: TViewModelBase );
    begin
      FMain := AViewModel as TMainViewModel;
      inherited;

    end;

    procedure TMainView.DetachFromViewModel( AViewModel: TViewModelBase );
    begin

      inherited;
      FMain := nil;
    end;

    procedure TMainView.SomeActionActionExecute(Sender: TObject);
    begin
      inherited;
      // Command im ViewModel ausführen
      FMain.Reference.SomeActionCommand.Execute;
    end;

    procedure TMainView.SomeActionActionUpdate(Sender: TObject);
    begin
      inherited;
      TAction( Sender ).Enabled := FMain.IsAssigned and FMain.Reference.SomeActionCommand.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' ) // reagieren bei Aktivität im ViewModel
          then
            begin
              ActivityCurtain.BringToFront;
              ActivityCurtain.Visible := Assigned( FMain.Reference.Activity );
              ActivityView1.Visible := Assigned( FMain.Reference.Activity );
              ActivityView1.BringToFront;
              // ViewModel der View zuweisen
              ActivityView1.SetViewModel( FMain.Reference.Activity );
            end;

        end

    end;

    end.
  • ViewModel
    Delphi-Quellcode:
    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
        FSomeActionCommand: ICommand;
        FActivity: AutoRef<TActivityViewModel>;
        procedure SetActivity( const Value: TActivityViewModel );
        function GetActivity: TActivityViewModel;
        function GetSomeActionCommand: ICommand;
      public
        constructor Create( );
        destructor Destroy; override;

        property SomeActionCommand: ICommand read GetSomeActionCommand;

        property Activity: TActivityViewModel read GetActivity;
      end;

    implementation

    { TMainViewModel }

    constructor TMainViewModel.Create;
    begin
      inherited Create;

    end;

    destructor TMainViewModel.Destroy;
    begin

      inherited;
    end;

    function TMainViewModel.GetActivity: TActivityViewModel;
    begin
      Result := FActivity;
    end;

    function TMainViewModel.GetSomeActionCommand: ICommand;
    begin
      if not Assigned( FSomeActionCommand )
      then
        FSomeActionCommand := TRelayCommand.Create(
            procedure
          begin

            // Aktivitätsanzeige setzen

            SetActivity( TActivityViewModel.Create );
            FActivity.Reference.Info := 'Performing SomeAction';

            // Task starten

            TTask.Run(
                procedure
              begin
                // Wir schlafen einfach mal ein wenig
                Sleep( 2000 );

                // Aktivitätsanzeige ausschalten
                TThread.Synchronize( nil,
                    procedure
                  begin
                    SetActivity( nil );
                  end );
              end );
          end,
          function: Boolean
          begin
            Result := not FActivity.IsAssigned;
          end );
      Result := FSomeActionCommand;
    end;

    procedure TMainViewModel.SetActivity( const Value: TActivityViewModel );
    begin
      if FActivity <> Value
      then
        begin
          FActivity := Value;
          OnPropertyChanged( 'Activity' );
        end;
    end;

    end.
  • DPR
    Delphi-Quellcode:
    program SimpleForm;

    uses
      de.itnets.References,
      de.itnets.Events,
      System.StartUpCopy,
      System.Messaging,
      FMX.Forms,
      MVVM.View.FMX.Form.Base in '..\..\View\FMX\MVVM.View.FMX.Form.Base.pas{ViewBaseForm},
      MVVM.View.FMX.Frame.Base in '..\..\View\FMX\MVVM.View.FMX.Frame.Base.pas{ViewBaseFrame: TFrame},
      ViewModel.WorkspaceViewModel in 'ViewModel\ViewModel.WorkspaceViewModel.pas',
      View.Form.WorkspaceView in 'View\View.Form.WorkspaceView.pas{WorkspaceView},
      View.Form.MainView in 'View\View.Form.MainView.pas{MainView},
      ViewModel.MainViewModel in 'ViewModel\ViewModel.MainViewModel.pas',
      ViewModel.ActivityViewModel in 'ViewModel\ViewModel.ActivityViewModel.pas',
      View.Frame.AcitivityView in 'View\View.Frame.AcitivityView.pas{ActivityView: TFrame};

    {$R *.res}

    var
      MainViewModel: AutoRef<TMainViewModel>;

    procedure Prepare;
    var
      LViewModelSet: Boolean;
    begin
      LViewModelSet := False;

      MainViewModel := TMainViewModel.Create;
      MainViewModel.Reference.RequestClose.AddProc(
          procedure( Sender: TObject; const e: TSimpleEventArgs )
        begin
          Application.MainForm.Close;
        end );

      TMessageManager.DefaultManager.SubscribeToMessage(
      {AMessageClass} TFormsCreatedMessage,
      {AListener} procedure( const Sender: TObject; const m: TMessage )
        begin
          if Assigned( MainView ) and not LViewModelSet
          then
            begin
              // ViewModel der View zuweisen
              MainView.SetViewModel( MainViewModel );
              LViewModelSet := True;
            end;
        end );

    end;

    begin
      ReportMemoryLeaksOnShutdown := True;
      Prepare;
      Application.Initialize;
      Application.CreateForm( TMainView, MainView );
      Application.Run;

    end.
Wie man sehr schön sieht wird das MainViewModel erstellt und der MainView zugewiesen.
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:
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.
und natürlich die ViewModelBase
Delphi-Quellcode:
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.
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)
Angehängte Dateien
Dateityp: zip SimpleForm.zip (2,07 MB, 63x aufgerufen)
Kaum macht man's richtig - schon funktioniert's
Zertifikat: Sir Rufo (Fingerprint: ‎ea 0a 4c 14 0d b6 3a a4 c1 c5 b9 dc 90 9d f0 e9 de 13 da 60)

Geändert von Sir Rufo ( 5. Feb 2015 um 13:43 Uhr)
  Mit Zitat antworten Zitat
FAM

Registriert seit: 22. Dez 2014
47 Beiträge
 
Delphi XE Enterprise
 
#3

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 14:00
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.

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?
  Mit Zitat antworten Zitat
Benutzerbild von Sir Rufo
Sir Rufo

Registriert seit: 5. Jan 2005
Ort: Stadthagen
9.454 Beiträge
 
Delphi 10 Seattle Enterprise
 
#4

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 14:09
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.

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?
Wenn die View den Controller erstellt, dann eben nicht.

Mein MainViewModel kann ich testen ohne irgendeine View im Spiel zu haben.
Delphi-Quellcode:
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;
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.

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.
Kaum macht man's richtig - schon funktioniert's
Zertifikat: Sir Rufo (Fingerprint: ‎ea 0a 4c 14 0d b6 3a a4 c1 c5 b9 dc 90 9d f0 e9 de 13 da 60)

Geändert von Sir Rufo ( 5. Feb 2015 um 14:11 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Sir Rufo
Sir Rufo

Registriert seit: 5. Jan 2005
Ort: Stadthagen
9.454 Beiträge
 
Delphi 10 Seattle Enterprise
 
#5

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 15:23
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:
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.
Schon haben wir eine Aktivität mit Fortschritt.

Dann mal in das MainViewModel und die Aktivitäten eingebaut (nur die Änderungen)
Delphi-Quellcode:
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.
Da wir TProgressActivityViewModel von TActivityViewModel 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.

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:
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.
Im Anhang nur die Exe
Angehängte Dateien
Dateityp: zip SimpleForm_exe.zip (2,04 MB, 16x aufgerufen)
Kaum macht man's richtig - schon funktioniert's
Zertifikat: Sir Rufo (Fingerprint: ‎ea 0a 4c 14 0d b6 3a a4 c1 c5 b9 dc 90 9d f0 e9 de 13 da 60)
  Mit Zitat antworten Zitat
Thomas_K

Registriert seit: 16. Apr 2006
71 Beiträge
 
Delphi XE8 Professional
 
#6

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 16:07
@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?
  Mit Zitat antworten Zitat
Benutzerbild von Sir Rufo
Sir Rufo

Registriert seit: 5. Jan 2005
Ort: Stadthagen
9.454 Beiträge
 
Delphi 10 Seattle Enterprise
 
#7

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 5. Feb 2015, 16:20
@Thomas_K
Das habe ich auch geschrieben
Zitat von Sir Rufo:
Im Anhang habe ich den gesamten restlichen Source (exclusive den Basis-Units) und die ausführbare Exe angehängt
Wenn ich die hätte veröffentlichen wollen, hätte ich das gemacht. Aktuell sitzt da zuviel Arbeit drin, als dass ich die einfach so herausgeben möchte. Ob ich die jemals herausgeben werde habe ich auch nicht entschieden (mir noch keinen Kopf drüber gemacht).

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.
Kaum macht man's richtig - schon funktioniert's
Zertifikat: Sir Rufo (Fingerprint: ‎ea 0a 4c 14 0d b6 3a a4 c1 c5 b9 dc 90 9d f0 e9 de 13 da 60)
  Mit Zitat antworten Zitat
BlackSeven

Registriert seit: 25. Sep 2004
79 Beiträge
 
Delphi XE7 Professional
 
#8

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 6. Feb 2015, 08:33
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.

https://bitbucket.org/sglienke/dsharp
  Mit Zitat antworten Zitat
FAM

Registriert seit: 22. Dez 2014
47 Beiträge
 
Delphi XE Enterprise
 
#9

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 6. Feb 2015, 09:08
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"?
  Mit Zitat antworten Zitat
mkinzler
(Moderator)

Registriert seit: 9. Dez 2005
Ort: Heilbronn
39.851 Beiträge
 
Delphi 11 Alexandria
 
#10

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon

  Alt 6. Feb 2015, 09:16
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"?
http://mitov.com/products/openwirelivebindings#overview
Markus Kinzler
  Mit Zitat antworten Zitat
Themen-Optionen Thema durchsuchen
Thema durchsuchen:

Erweiterte Suche
Ansicht

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 19:57 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz