nakamurakko’s diary

仕事で覚えたこと、勉強したことを自分のメモ代わりに書いていこうかなと。

DelphiでMVVMライクな実装を試す

DelphiでMVVMを実装したくて、本を読んだり、ライブラリーをGitHubで公開してそうなものを探したりしたけど、「LiveBindingを使えばできるのでは?」と思って探してみた。

参考にしたサイト

自分なりに実装してみたので、手順を残す。

環境

  • Delphi 10.2
  • libeay32.dll (Web接続用)
  • ssleay32.dll (Web接続用)

構成

  • URLを指定し、コンテンツ情報を文字列で取得するアプリケーション。
  • ViewはTForm、ViewModelはTDateModuleで実装する。

f:id:nakamurakko:20200226092318p:plain

実装

データ用クラス

まず、アクセスしたURL、アクセスしたページを文字列として保持するクラスを作成する。

/// <summary>Web情報クラス。</summary>
TWebInfo = class
private
  FURL: string;
  FContent: string;
  procedure SetURL(const Value: string);
  procedure SetContent(const Value: string);
public
  /// <summary>URL。</summary>
  property URL: string read FURL write SetURL;
  /// <summary>コンテンツ文字列。</summary>
  property Content: string read FContent write SetContent;
end;

Modelクラス

指定したURLを使って、コンテンツを文字列として返すクラスメソッド(GetContent)を持つクラスを用意する。

/// <summary>データ通信用Model。</summary>
TDataCommunicationModel = class
public
  /// <summary>コンテンツを取得する。</summary>
  /// <param name="URL">URL文字列。</param>
  /// <returns>コンテンツの文字列を返す。</returns>
  class function GetContent(URL: string): string;
end;

クラスメソッドにして、インスタンス化しないで済むようにしている。 実装はIndyを使ってSSL通信で書いた内容とほぼ同じ。

class function TDataCommunicationModel.GetContent(URL: string): string;
var
  http: TIdHTTP;
  sslIoHandler: TIdSSLIOHandlerSocketOpenSSL;
begin
  Result := '';

  http := TIdHTTP.Create;
  sslIoHandler := TIdSSLIOHandlerSocketOpenSSL.Create(http);
  try
    sslIoHandler.SSLOptions.SSLVersions := [sslvTLSv1_1, sslvTLSv1_2];
    http.IOHandler := sslIoHandler;
    http.HandleRedirects := True;

    // URLを指定して取得。
    Result := http.Get(URL);
  finally
    FreeAndNil(http);
  end;
end;

ViewModelクラス

定義

TDataModuleを継承してViewModelを作成する。 バインディングさせるためにTPrototypeBindSourceを持たせる。

f:id:nakamurakko:20200226092353p:plain

TPrototypeBindSourceには、バインドさせるTWebInfoクラスが持っているプロパティ(URL、Content)と同じ名前のフィールドを定義する。

f:id:nakamurakko:20200226092802p:plain

データを保持するためにデータクラス(TWebInfo)の変数をprivate変数として宣言する。

/// <summary>サンプルViewModelクラス。</summary>
TMainViewModel = class(TDataModule)
  BindSource: TPrototypeBindSource;
private
  /// <summary>Web情報。</summary>
  WebInfo: TWebInfo;
public
end;

Adapter作成

データ用保持用の変数とTPrototypeBindSourceのフィールドを関連付けるために、TPrototypeBindSource.OnCreateAdapterイベントを実装する。

  • ①まず、データ保持用クラスのインスタンスを生成。
  • ②TObjectBindSourceAdapterのインスタンスを生成。引数にデータ保持用インスタンスを指定する。
  • ③AutoEdit、AutoPostをTrueにして、なるべく自動反映されるようにしておく。
procedure TMainViewModel.BindSourceCreateAdapter(Sender: TObject; var ABindSourceAdapter: TBindSourceAdapter);
begin
  // ①データ保持用インスタンスを生成。
  WebInfo := TWebInfo.Create;
  WebInfo.URL := 'https://www.nakamurakko.jp';
  WebInfo.Content := '';

  // ②TObjectBindSourceAdapterのインスタンスを生成。
  ABindSourceAdapter := TObjectBindSourceAdapter<TWebInfo>.Create(Self, WebInfo, False);

  // ③AutoEdit、AutoPostをTrueに設定。
  ABindSourceAdapter.AutoEdit := True;
  ABindSourceAdapter.AutoPost := True;
end;

これで、TPrototypeBindSourceがデータ保持用クラスと画面のコントロールの橋渡しをしてくれるようになる。

取得処理の実装

View側から呼び出すWeb情報取得処理をViewModelに実装する。

procedure TMainViewModel.GetContent;
begin
  BindSource.Refresh;

  // Modelのクラスメソッドを使用してコンテンツを文字列で取得。
  WebInfo.Content := TDataCommunicationModel.GetContent(WebInfo.URL);

  BindSource.Refresh;
end;

BindSource.Refreshを呼び出しているのは、次の理由で行っている。

  • 最初に呼び出しているのは、Viewの変更状態を反映させるため。
  • 最後に呼び出しているのは、Viewに変更状態を通知させるため。

AutoPost、AutoEditをTrueに設定しても反映されない場合は試してみるといいはず。

Viewクラス

定義

TFormを継承してViewクラスを作成して、ViewModelを変数に宣言する。

/// <summary>サンプルフォーム。(Viewクラス)</summary>
TMainForm = class(TForm)
private
  ViewModel: TMainViewModel;
end;

コンストラクターでViewModelのインスタンスを生成しておく。

constructor TMainForm.Create(AOwner: TComponent);
begin
  inherited;

  ViewModel := TMainViewModel.Create(Self);
end;

デザイナーを開き、URLを入力するTEdit(①)、取得処理を呼び出すTButton(②)、取得したコンテンツ文字列を表示するTMemo(③)を用意する。

f:id:nakamurakko:20200226093055p:plain

ViewModelのメソッドを呼び出す

貼り付けたボタンのクリックイベントでViewModel.GetContentメソッドの呼び出す。

procedure TMainForm.GetContentButtonClick(Sender: TObject);
begin
  ViewModel.GetContent;
end;

ViewとViewModelのバインド

LiveBindingデザイナを開いて、URL(①)、コンテンツ(②)をバインドする。

f:id:nakamurakko:20200226092427p:plain

実行してみる。

起動してみるとこんな感じ。 TPrototypeBindSource.OnCreateAdapterイベントで設定したURLが初期表示されている。

f:id:nakamurakko:20200226092450p:plain

ボタンをクリックしてみるとこんな感じで反映される。

  1. ボタンクリック
  2. ViewModelのメソッドが呼ばれる
  3. Modelのメソッドが呼ばれる
  4. ViewModelのデータ用変数に取得結果を反映
  5. バインドされたTMemoの表示が変わる

f:id:nakamurakko:20200226092510p:plain

以上、TWebBrowser.Navigateメソッドで出来るWebアクセスを、MVVMライクに出来ないかと実装してみた。

課題

  • TPrototypeBindSourceを仲介しない方法があるかどうか。(間に1ステップはさむのが手間)
  • クラスのプロパティをバインド出来たけど、深い階層のバインドや複雑なバインドが出来るのか調査が必要。(サンプルソースを見ると、コレクションのバインドは多分出来そう。)

追記

ソースをGitHubにアップしました。https://github.com/nakamurakko/MVVMLikeDelphiSample