Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   MVC + Observer Pattern Konzept / Was haltet Ihr davon (https://www.delphipraxis.net/183799-mvc-observer-pattern-konzept-haltet-ihr-davon.html)

FAM 5. Feb 2015 11:30

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:
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.

Sir Rufo 5. Feb 2015 12:24

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:
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)

FAM 5. Feb 2015 13:00

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

Zitat von Sir Rufo (Beitrag 1288897)
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?:shock:

Sir Rufo 5. Feb 2015 13:09

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

Zitat von FAM (Beitrag 1288902)
Zitat:

Zitat von Sir Rufo (Beitrag 1288897)
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?:shock:

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.

Sir Rufo 5. Feb 2015 14:23

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:
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
Delphi-Quellcode:
TProgressActivityViewModel
von
Delphi-Quellcode:
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

Thomas_K 5. Feb 2015 15:07

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?

Sir Rufo 5. Feb 2015 15:20

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
 
@Thomas_K
Das habe ich auch geschrieben
Zitat:

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.

BlackSeven 6. Feb 2015 07:33

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.

https://bitbucket.org/sglienke/dsharp

FAM 6. Feb 2015 08:08

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"?

mkinzler 6. Feb 2015 08:16

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

Zitat von FAM (Beitrag 1288952)
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

Sir Rufo 6. Feb 2015 08:28

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

Zitat von FAM (Beitrag 1288952)
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"?

Ja, ich habe einmal an jedem ViewModel einen PropertyChanged Event hängen und kann damit entweder den BindSourceAdapter refreshen oder eben manuell den Wert übergeben. Wenn manuell gebunden, dann brauche ich aber auch ein manuelles Rückschreiben in das ViewModel, was aber eigentlich idR mit einem Event geht, wo dann von allen die Werte zurückgeschrieben werden.

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).

FAM 6. Feb 2015 08:40

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
 
Liste der Anhänge anzeigen (Anzahl: 1)
Zitat:

Zitat von mkinzler (Beitrag 1288953)
http://mitov.com/products/openwirelivebindings#overview

danke für den Hinweis.

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?

FAM 6. Feb 2015 08:46

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

Zitat von Sir Rufo (Beitrag 1288956)
Ja, ich habe einmal an jedem ViewModel einen PropertyChanged Event hängen und kann damit entweder den BindSourceAdapter refreshen oder eben manuell den Wert übergeben.

das kommt dem schon nahe wie ich mir das denke.

Zitat:

Zitat von Sir Rufo (Beitrag 1288956)
Wenn manuell gebunden, dann brauche ich aber auch ein manuelles Rückschreiben in das ViewModel, was aber eigentlich idR mit einem Event geht, wo dann von allen die Werte zurückgeschrieben werden.

kannst du bitte mal einen source-snippet posten - wie du das genau implementierst (das rückschreiben)? Ich würde das (indirekt) über die write Methode in der jeweiligen Property vom eigentlichen Model machen, den aufruf dafür würde ich im view schreien

Sir Rufo 6. Feb 2015 08:53

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

FAM 6. Feb 2015 09:00

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

Zitat von Sir Rufo (Beitrag 1288964)
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.

Achso da ist in meiner Skizze ein fehler... der erste Kasten müsste das View enthalten, dann passt das denke ich

Zitat:

Zitat von Sir Rufo (Beitrag 1288964)
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

da bin ich jetzt raus - ich wüsste jetzt nicht wie ich für eine Visuelle Komponente einen Wrapper erstellen könnte? Code-Snippet ?!


welches Konzept ist für dich besser?

Sir Rufo 6. Feb 2015 09:15

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:
CheckLabel
ist nur für die visuelle Rückmeldung, dass es tatsächlich im ViewModel angekommen ist.
Im Anhang der Source (immer noch ohne mein Basis-Framework) und eine Exe zum Ausprobieren

Sir Rufo 6. Feb 2015 09:26

AW: MVC + Observer Pattern Konzept / Was haltet Ihr davon
 
So ein Edit-Wrapper könnte ungefähr so aussehen
Delphi-Quellcode:
TEditView = class(Component)
public
  procedure SetViewModel( AViewModel : TViewModelBae; const PropertyName : string; const AAccessor : TFunc<string>; const AMutuaotr : TProc<string> );
  propety Edit : TEdit;
end;
Denkbar ist allerdings auch ein Wrapper nach dieser Art
Delphi-Quellcode:
TEditView = class( TComponent )
public
  property Edit : TEdit;
  property PropertyName : string;
  property ViewModel : TViewModelBase;
end;
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

FAM 6. Feb 2015 09:32

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

Zitat von Sir Rufo (Beitrag 1288970)
Also hier erst mal das manuelle Binding in der View
Delphi-Quellcode:
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.

ok cool, eine frage hätte ich da noch, sorgt dein Framework für den Aufruf der
Delphi-Quellcode:
ViewModelPropertyChanged
oder übersehe ich da irgendwo was?

Sir Rufo 6. Feb 2015 09:42

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

Zitat von FAM (Beitrag 1288975)
ok cool, eine frage hätte ich da noch, sorgt dein Framework für den Aufruf der
Delphi-Quellcode:
ViewModelPropertyChanged
oder übersehe ich da irgendwo was?

Ja, und da das bei jeder View erfolgen muss und ich jede View von einer Basis-View ableite, erfolgt das Anhängen an den PropertyChanged Event auch eben in dieser Basis View -> weniger Arbeit für mich ;)

In Beitrag #2 habe ich am Ende die
Delphi-Quellcode:
TViewModelBase
und die
Delphi-Quellcode:
TViewBaseForm
noch gezeigt, wo man das sehen kann.

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).

FAM 6. Feb 2015 10:01

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

Zitat von Sir Rufo (Beitrag 1288978)
..., erfolgt das Anhängen an den PropertyChanged Event auch eben in dieser Basis View -> weniger Arbeit für mich ;)

da ich jetzt nicht den luxus eines Frameworks habe mache ich das jetzt "per Hand"

@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:
TModel.notify
davon abgeleitete Klasse müssen bei Setter-Methoden jetzt die Funktion
Delphi-Quellcode:
notify
aufrufen

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

Delphi-Quellcode:
TViewModel.SetBar(const Value: String);
hier könnte die
Delphi-Quellcode:
  notify;
Methoden-Aufruf weggelasen wird

Delphi-Quellcode:
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.
Dann das ViewModel

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

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;

Sir Rufo 6. Feb 2015 10:39

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:
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.
Da diese Methode
Delphi-Quellcode:
protected
ist, kann ich die von allen abgeleiteten ViewModels aufrufen.

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-Referenz durchsuchenTObject.Equals. Damit kann ich auf Gleichheit prüfen. Die Standard-Prüfung ist
Delphi-Quellcode:
function Object.Equals( Obj : TObject ) : Boolean;
begin
  Result := Self = Obj;
end;
Das kann ich aber auch überschreiben
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:
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;
Und noch etwas weitergedacht, kann ich sogar das ViewModel vergleichen und zwar mit einem anderen
Delphi-Quellcode:
TPersonViewModel
und einem
Delphi-Quellcode:
TPerson
:
Delphi-Quellcode:
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;
Das auch noch auf die View übertragen und ich kann quasi alles mit jedem vergleichen.

FAM 6. Feb 2015 11:28

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:
TForm1.ViewModelStockpilePropertyChanged
ausgeführt, wenn ich aber aus dem ModelView das Model ändere passiert nichts... eigentlich logisch und denn wieder auch nicht ;)

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.

FAM 6. Feb 2015 12:02

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

Delphi-Quellcode:
constructor TViewModelStockpile.Create(const EventHandler: TEventHandler);
begin
  inherited Create;
   FEventHandler := EventHandler;

  FModel := TStockpileModel.Create;
  //Foo := 'TViewModelStockpile.Create => Value1'; <<<<<<<<<<<< FEHLER!
end;
Main
Delphi-Quellcode:
procedure TForm1.ViewModelStockpilePropertyChanged;
begin
  EditContentbarStartMarker.Text := FViewModelStockpile.Foo;
   ShowMessage('Model wurde geändert');
end;
läuft im Kreis ...

Sir Rufo 6. Feb 2015 12:38

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:
procedure TEventHandler.setPropertyChanged(const Value: TEvent);
begin
  if @FPropertyChanged <> @Value then
  begin
  ShowMessage('setPropertyChanged');
    FPropertyChanged := Value;
  end;
end;
Du vergleichst hier
Delphi-Quellcode:
if @FPropertyChanged <> @Value then
die Adresse der Methode. Das reicht aber nicht aus, denn bei unterschiedlichen Instanzen der gleichen Klasse ist diese Adresse gleich!

Du musst das nach Delphi-Referenz durchsuchenTMethod casten und dann
Delphi-Quellcode:
TMethod.Code
und
Delphi-Quellcode:
TMethod.Data
vergleichen ;)

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 23:51 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