Einzelnen Beitrag anzeigen

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