「古い瓶に新しい酒」: SOD MVVMフレームワークでWinformsに新風を

「古い瓶に新しい酒」: SOD MVVMフレームワークでWinformsに新風を

WinFormsにおけるMVVM技術の必要性。MVVMフレームワークの実装は実は難しくなく、鍵はモデル(Model)とビュー(View)の双方向バインディング、すなわちモデルの変更がビューの内容を変更し、ビューの変更もモデルを変更できることです。

最終更新 2021/11/23 21:13
用户1177503
読了目安 10 分
カテゴリ
Winform
テーマ
Winformコントロールライブラリ
タグ
.NET Winform MVVM Winformオープンソースプロジェクト オープンソース

1. 注目の MVVM フレームワーク

近年最もホットなテクノロジーの一つがフロントエンド技術であり、様々なフレームワーク、標準、デザインスタイルが次々と登場しています。多くのフロントエンドフレームワークの中で、MVCやMVVM機能を持つフレームワークが注目を集めています。例えば、GitHubで高い注目を集めるVue.jsは、中国人開発者の作品であり、そのデザインスタイルやドキュメントの親しみやすさから、中国人にとってはさらに優れています。そのため、私はこれを会社に推奨しました。その理由は、非常に優れたMVVM機能を持ち、データ指向であり、DOMの詳細に依存しないため、jQueryなどと比較してコード量が少なく、バックエンドプログラマーにとって扱いやすく、UIデザイナーとプログラマーの役割分担にも有利だからです。

以下は、Vue.jsがMVVM機能を実現するための原理図です:

前述したVue.jsのこれらの利点に見覚えはありませんか?そうです、これはかつてWPFで普及したMVVM技術です。WinForms技術と比較して、WPFはUIデザイナーにより強力なデザイン能力を提供し、より魅力的で美しいインターフェースを作成できます。ただし、Microsoftの多くの技術は常に時代を先取りしており、更新が非常に速いです。WPFがリリースされた頃、WinFormsはまだデスクトップ開発の主要分野を占めていました。その後、モバイル開発の時代が到来し、Webベースのフロントエンド技術が大きく発展し、WPFの人気を上回りました。しかし、WPFが導入したMVVMの考え方は、Webフロントエンドでさらに発展し、現在ではMVVMベースの様々なフロントエンドフレームワークが雨後の筍のように登場しています。

2. WinForms における MVVM の需要

Webフロントエンド技術の大発展により、クロスプラットフォームのHTML5ベースのモバイルフロントエンド開発技術が成熟し、様々なアプリケーションが従来のC/SからB/S、APPモードへと移行しています。そのため、WPFのようなC/Sモードのフロントエンド技術への注目度は徐々に低下しています。その結果、WPF上のMVVMは広く普及しているとは言えません。現在も多くのレガシーシステムや新しいC/SシステムはWinForms技術を用いて開発・保守されていますが、WinFormsには優れたMVVMフレームワークが存在しません。WinFormsのUI効果や全体的な開発品質、開発効率は効果的に向上しておらず、WPFのような異なる開発スタイルへの移行は技術的に困難です。したがって、WinForms上で動作するMVVMフレームワークがあれば、多くのバックエンド.NETプログラマーにとって福音となるでしょう。

筆者は常に第一線で活躍する.NET開発者、アーキテクトであり、Web、デスクトップ、バックエンド開発技術に幅広く関わってきました。開発者が自嘲的に「コーダー」と呼ぶ心情を深く理解しており、仕事に追われて恋人や家族と過ごす時間がないことを痛感しています。そのため、私は開発効率を向上させ、開発品質を改善する方法を常に研究・整理してきました。そして約10年の歳月をかけて、開発フレームワーク「SODフレームワーク」を完成させました。最近、Webフロントエンド開発技術を研究する中で、Vue.jsのMVVM思想に再び触れ、WinFormsにおけるMVVM技術の必要性を痛感しました。MVVMフレームワークの実装はそれほど難しくなく、鍵となるのはモデル(Model)とビュー(View)の双方向バインディング、すなわちモデルの変更がビューの内容を変更し、ビューの変更もモデルを変更できることです。

3. SOD WinForms MVVM 実装の原理

このような変更を実現するには、バインドされる側にプロパティ変更通知機能が必要であり、バインドする側が変更されたときに、バインドされる側に通知して適切な処理を行わせる必要があります。.NETでは、この通知機能を提供するインターフェースが次です:

INotifyPropertyChanged

このインターフェースはSystem.dll内で定義されており、.NET 2.0の時点ですでにサポートされています。以下がその具体的な定義です:

namespace System.ComponentModel
{
    // 摘要:
    //     属性値が変更されたことをクライアントに通知します。
    public interface INotifyPropertyChanged
    {
        // 摘要:
        //     属性値が変更されたときに発生します。
        event PropertyChangedEventHandler PropertyChanged;
    }
}

SODフレームワークのエンティティクラスの基底クラスEntityBaseはこのインターフェースを実装しています:

public abstract class EntityBase : INotifyPropertyChanged, ICloneable, PWMIS.Common.IEntity
{
    /// <summary>
    /// プロパティ変更イベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// プロパティ変更イベントを発生させる
    /// </summary>
    /// <param name="propertyFieldName">プロパティ変更イベントオブジェクト</param>
    protected virtual void OnPropertyChanged(string propertyFieldName)
    {
        if (this.PropertyChanged != null)
        {
            string currPropName = EntityFieldsCache.Item(this.GetType()).GetPropertyName(propertyFieldName);
            this.PropertyChanged(this, new PropertyChangedEventArgs(currPropName));
        }
    }
    // その他のコードは省略… …
}

そのため、SODフレームワークのエンティティクラスは、MVVMのModelとしてビューにバインドされるオブジェクトとして直接使用できます。したがって、WinForms形式のビュー要素がどのようにバインド操作を実装するかを解決するだけで、WinFormsアプリケーションはMVVM機能を実現できます。WinFormsでは、コントロールは基本的にバインディング機能を既に実装しており、それがコントロールのDataBindingsです。これにバインドを追加するだけです。例えば、次の例です:

this.textbox1.DataBindings.Add("Text", userEntity, "Name");

これにより、テキストボックスに入力された内容が変わると、エンティティクラスオブジェクト userEntity.Name プロパティの値も変わります。userEntity がSODエンティティクラスの場合、userEntity.Name が変わると、テキストボックスの Text プロパティも同期的に変わります。

SODフレームワークのデータコントロール(WinForms、WebForms)はIDataControlインターフェースを実装しており、いくつかの重要なプロパティ LinkObject、LinkProperty を定義しています:

/// <summary>
/// データマッピングコントロールインターフェース
/// </summary>
public interface IDataControl
{

    /// <summary>
    /// データベースのデータ項目に関連付けられたデータ
    /// </summary>
    string LinkProperty
    {
        get;
        set;
    }

    /// <summary>
    /// データに関連付けられたテーブル名
    /// </summary>
    string LinkObject
    {
        get;
        set;
    }
    // その他のインターフェースメソッドの内容は省略… …

LinkObject を使用してバインドするエンティティクラスオブジェクトを指定し、LinkProperty を使用してバインドするオブジェクトのプロパティを指定できます。したがって、以下のコードを使用してWinFormsコントロールとSODエンティティクラスの双方向バインディングを実現できます:

public void BindDataControls(Control.ControlCollection controls)
{
    var dataControls = MyWinForm.GetIBControls(controls);
    foreach (IDataControl control in dataControls)
    {
        //control.LinkObject ここでは "DataContext"
        object dataSource = GetInstanceByMemberName(control.LinkObject);
        if (control is TextBox)
        {
            ((TextBox)control).DataBindings.Add("Text", dataSource, control.LinkProperty);
        }
        if (control is Label)
        {
            ((Label)control).DataBindings.Add("Text", dataSource, control.LinkProperty);
        }
        if (control is ListBox)
        {
            ((ListBox)control).DataBindings.Add("SelectedValue", dataSource, control.LinkProperty, false, DataSourceUpdateMode.OnPropertyChanged);
        }
    }
}

さらに、いくつかのコマンドをビューにバインドする必要があるかもしれません。これも比較的簡単に実装できます:

private Dictionary<object, CommandMethod> dictCommand;
public delegate void CommandMethod();

public void BindCommandControls(Control control,CommandMethod command)
{
    if (control is Button)
    {
        dictCommand.Add(control, command);
        ((Button)control).Click += (sender, e) => {
            dictCommand[sender]();
        };
    }
}

このプロセスを経て、フォームのロードイベントに以下の数行のコードを記述するだけで済みます:

SubmitedUsersViewModel DataContext{get;set;}

private void Form1_Load(object sender, EventArgs e)
{
    base.BindDataControls(this.Controls);
    base.BindCommandControls(this.button1, DataContext.SubmitCurrentUsers);
    base.BindCommandControls(this.button2, DataContext.UpdateUser);
    base.BindCommandControls(this.button3, DataContext.RemoveUser);
}

上記のコードでは、まず DataContext というビューモデルオブジェクトを定義し、BindDataControls メソッド内でビューコントロールにバインドするオブジェクトとして使用します。この DataContext の CurrentUser プロパティの Name プロパティがテキストボックスコントロールにバインドされているため、CurrentUser.Name は複合プロパティとしてバインドされます。ラベルコントロールやリストボックスコントロールについても同様のプロセスです。以下の図を参照してください:

このように、ビュー上で簡単なデータプロパティの設定と、わずかなコードビハインドのバインディングコードを記述するだけで、双方向バインディング機能を持つプログラムが完成します。

4. MVVM サンプルソリューション

4.1 ソリューション概要

SODフレームワークのMVVMサポートをデモンストレーションするために、簡単なソリューションを構築します。全体で3つのプロジェクトアセンブリに分かれており、それぞれMVVMの3つの主要部分に対応します:

  1. WinFormMvvm: WinFormサンプルプログラムのメインプログラム。ビュークラスが含まれるアセンブリ
  2. WinFormMvvm.Model: モデルクラスアセンブリ
  3. WinFormMvvm.ViewModel: ビューモデルアセンブリ

構築されたソリューションの図は以下の通りです:

注:このソリューションは SOD Ver 5.5.5.1019 を使用しています。これは現在のnuget上のSODのバージョンです。最新のSODフレームワークでは、WinFormMvvmプロジェクトの MvvmForm.cs ファイルがフレームワークに組み込まれています。

プログラムは App.config で今回のテストに使用するデータベースを指定しており、データベースの種類は Access です。デフォルトの接続文字列は Office 2007 以降のバージョンを必要とする場合があります。

以下が App.config の内容です:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name ="default" connectionString ="Provider=Microsoft.ACE.OLEDB.12.0;Jet OLEDB:Engine Type=6;Data Source=testdb.accdb" providerName="Access"/>
  </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="PWMIS.Core" publicKeyToken="17ba13a12b9fd814" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.5.5.1019" newVersion="5.5.5.1019" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

より低いバージョンのAccessデータベースが必要な場合、または他のデータベース(SqlServerなど)に変更したい場合は、以下の手順を参照してください:

  1. 次のリンクを開きます:

http://pwmis.codeplex.com/

  1. コンテンツの章「3,修改下 App.config 文件的连接配置」を参照してください;

  2. この節の下にあるリンク「2.2.3 扩展数据访问类配置」をクリックしてください。

4.2 MVVM の WinForm ビューの作成

これはシンプルなWinFormフォームで、3つのSOD「データコントロール」を含みます:ユーザーIDを表示するラベルコントロール、ユーザー名を表示するテキストボックスコントロール、既存のユーザーリストを表示するリストボックスコントロール、そしてリストにデータを追加、変更、削除するための3つのボタンです。

データコントロールについては、このフォームデザイナー画面で「ツールボックス」を開き、「全般」タブでコンテキストメニュー「アイテムの選択」を選択し、packages\PDF.NET.SOD.WinForm.Extensions.5.5.5.1020\lib ディレクトリを参照して Pwmis.Windows.dll を選択すると、SODのデータコントロールが表示され、フォームにドラッグ&ドロップできます。

これらの3つのボタンコントロールには直接クリックイベントを設定せず、コマンドバインディングの形式で行います。例えば、追加ボタンには次のようにコマンド(ビューモデルのメソッド)をバインドします:

base.BindCommandControls(this.button1, DataContext.SubmitCurrentUsers);

これにより、ユーザー追加ボタンのクリックイベントが DataContext の SubmitCurrentUsers メソッドにバインドされます。

データコントロールのバインドについては、以下の1行だけ必要です:

base.BindDataControls(this.Controls);

既に述べたように、このメソッドはメソッドの最初のパラメータ内のすべてのデータコントロールを走査し、LinkObject と LinkProperty プロパティを見つけて、データコントロールとビューモデルオブジェクトのバインドを実装します。ここでは DataContext オブジェクトの CurrentUser オブジェクトのプロパティにバインドされます。

プロパティブラウザでデータコントロールの LinkProperty プロパティの横にある「...」ボタンをクリックすると、以下の「データコントロールプロパティセレクタ」フォームが表示されます:

ここでバインドするオブジェクトは現在のフォームの DataContext オブジェクトであるため、メインプログラムアセンブリを参照して選択する必要があります。そうすると、プロパティ名の欄にこのオブジェクトのすべてのプロパティとサブプロパティが表示されます。DataContext オブジェクトがリストに表示されない場合は、Form フォームで DataContext オブジェクトが宣言されているかどうかを確認し、事前にアセンブリをコンパイルしてください。最後に「OK」をクリックすると、データコントロールのバインド情報が設定されます。

4.3 MVVM のモデルの作成

私たちのモデルは非常にシンプルで、新しいユーザーの作成、既存ユーザーの読み込み、追加、変更、削除の操作を担当します。これらの操作はデータベースに対して行われ、通常のCRUD操作です。サンプルなので多くのロジックはなく、コードを直接ご確認ください:

public class UserModel
{
    private static int index = 0;
    private LocalDbContext context;
    public UserModel()
    {
        context = new LocalDbContext();
    }

    public List<UserEntity> GetAllUsers()
    {
        var list= OQL.From<UserEntity>().ToList(context.CurrentDataBase);
        int max = list.Max(p => p.ID);
        index = ++max;
        return list;
    }
    public void UpdateUser(UserEntity user)
    {
        int count= context.Update<UserEntity>(user);
    }

    public void AddUsers(IList<UserEntity> users)
    {
        int count = context.AddList(users);
    }

    public void SubmitUser(UserEntity user)
    {
        int count = context.Add(user);
    }

    public void RemoveUser(UserEntity user)
    {
        int count = context.Remove(user);
    }

    public UserEntity CreateNewUser(string userName="NoName")
    {
        return new UserEntity()
        {
              ID= ++index,
              Name =userName
        };
    }
}

ユーザーモデルクラスはユーザーエンティティクラスを使用します。これも非常にシンプルで、IDプロパティとNameプロパティのみを持ちます。詳細は以下の通りです:

public class UserEntity:EntityBase
{
    public UserEntity()
    {
        TableName = "Tb_User";
        PrimaryKeys.Add("UserID");
    }
    public int ID {
        get { return getProperty<int>("UserID"); }
        set { setProperty("UserID", value); }
    }

    public string Name
    {
        get { return getProperty<string>("UserName"); }
        set { setProperty("UserName", value); }
    }

}

このユーザーエンティティクラスは非常にシンプルですが、SODエンティティクラスはすべて「プロパティ変更通知」インターフェースを実装しているため、ビューにモデルバインディングの要素として直接提供できます(前述の通り)。

次に、このユーザーエンティティクラスを操作するデータコンテキストです。ユーザーモデルクラスはその使用方法を示していますが、その定義は非常にシンプルです:

class LocalDbContext : DbContext
{
    public LocalDbContext()
        : base("default")
    {
        //local は接続文字列の名前
    }

    protected override bool CheckAllTableExists()
    {
        //ユーザーテーブルを作成
        CheckTableExists<UserEntity>();
        return true;
    }
}

以上で、シンプルなMVVMモデルクラスの全定義が完了しました。

4.4 MVVM のビューモデルの作成

ビューモデルはビューの抽象化であり、主要なビュー処理ロジックをカプセル化します。MVPのPresenterとは異なり、ビューモデルは抽象的なリストコントロールなどの詳細なビュー要素の抽象化を含まず、ビューで使用される可能性のあるデータをカプセル化し、バックエンドのMVVMモデルオブジェクトの呼び出しを含む場合があります。

この例では、ユーザービューモデルの機能も非常にシンプルで、ビューが必要とするユーザーリストを提供し、ビューの追加、変更、削除のコマンドに応答します。詳細なコードは以下の通りです:

public class SubmitedUsersViewModel
{

    private UserModel model = new UserModel();
    public BindingList<UserEntity> Users { get; private set; }
    public UserEntity CurrentUser { get; private set; }


    UserEntity _selectUser;
    /// <summary>
    /// 現在選択されているユーザー。設定すると、現在のユーザーが設定されます。
    /// </summary>
    public UserEntity SelectedUser {
        get { return _selectUser; }
        set {
            _selectUser = value;
            this.CurrentUser.ID = value.ID;
            this.CurrentUser.Name = value.Name;
        }

    }

    int _selectedUserID;
    public int SelectedUserID
    {
        get { return _selectedUserID; }
        set {
            _selectedUserID = value;
            var obj = this.Users.FirstOrDefault(p=>p.ID==value);
            if (obj != null)
            {
                this.CurrentUser.ID = obj.ID;
                this.CurrentUser.Name = obj.Name;
                _selectUser = this.CurrentUser;
            }

        }
    }

    public SubmitedUsersViewModel()
    {
        var data = model.GetAllUsers();
        Users = new BindingList<UserEntity>(data);
        CurrentUser = new UserEntity();

    }

    public void UpdateUser()
    {
        var obj = this.Users.FirstOrDefault(p => p.ID == this.CurrentUser.ID);
        if (obj != null)
        {
            obj.Name = this.CurrentUser.Name;
            //更新後は ResetBindings メソッドを呼び出す必要があります。そうしないと、コントロール上のデータが1行失われます。
            this.Users.ResetBindings();

            model.UpdateUser(obj);
        }

    }
    public void UpdateUser(int id,string name)
    {
        var obj = this.Users.FirstOrDefault(p => p.ID == id);
        if (obj != null)
        {
            obj.Name = name;
            //更新後は ResetBindings メソッドを呼び出す必要があります。
            this.Users.ResetBindings();

            model.UpdateUser(obj);
        }
    }

    public void SubmitUsers(UserEntity user)
    {
        //UserEntity newUser = new UserEntity();
        //newUser.ID = user.ID;
        //newUser.Name = user.Name;
        //Users.Add(newUser);
        if (!Users.Contains(user))
        {
            Users.Add(user);
            model.SubmitUser(user);
        }
    }
    public void SubmitCurrentUsers()
    {
        UserEntity newUser = model.CreateNewUser(CurrentUser.Name);
        SubmitUsers(newUser);
        CurrentUser.ID = newUser.ID;
    }

    public void RemoveUser()
    {
        if (SelectedUser == null)
        {

            return;
        }
        var obj = this.Users.FirstOrDefault(p => p.ID == SelectedUser.ID);
        if (obj != null)
        {
            this.Users.Remove(obj);
            //更新後は ResetBindings メソッドを呼び出す必要があります。
            this.Users.ResetBindings();

            model.RemoveUser(obj);
        }
    }
}

4.5 NuGet パッケージ参照の追加

ソリューション全体で PDF.NET Core パッケージを追加する必要がありますが、WinFormsメインプログラムでは、さらに2つの関連パッケージ(SOD WinForm拡張とSOD Access拡張)を追加する必要があります。以下は、ソリューションにインストールされている全パッケージの図です:

4.6 ソリューションの実行

上記のプロセスを経て、ビュー要素を追加し、ビュー要素のデータバインディングを設定し、モデルとビューモデルオブジェクトを作成しました。シンプルなMVVMサンプルプログラムが完成しました。以下は実行結果の図です:

5. MVVM パターンのまとめ

このサンプルを実行することで、MVVMの特徴を体験できたと思いますが、適切に表現するのは難しいかもしれません。そこで、数人のWPF専門家と交流した後、彼らがまとめたMVVMのいくつかの核となる特徴(セールスポイント)を以下に示します:

  1. ビューロジック(ビューモデル)とビュー(ビュー要素、スタイル)の結合を解除する。
  2. ビューとビューモデルまたはモデルの双方向データバインディング。データ駆動型であり、ビュー駆動型ではない。
  3. ビューとビューモデルの分離により、インターフェース機能をすべてコード化し、TDD(テスト駆動開発)の可能性を提供する。

6. SOD WinForms MVVM サポート

SODフレームワークバージョン5.6.0.1111でリリースされたこの「独身の日」バージョン以降、直接のWinForms MVVMサポートを利用できます。それ以前のバージョンでは、このサンプルプログラムと同様に少し追加の作業が必要ですが、既存のSOD対応ソリューションに影響を与えることはありません。

このサンプルソリューションは、フレームワークのオープンソースサイト http://pwmis.codeplex.com で直接ダウンロード可能になります。ソースコードはすべてコミット済みであり、以下のアドレスから詳細なコード説明を確認できます:

http://pwmis.codeplex.com/SourceControl/latest#SOD/Example/WinFormMvvm/WinFormMvvm/Readme.txt

詳細情報を得るため、またはコミュニティQQグループに参加するため、またはこのフレームワークを支援するためには、フレームワークの公式サイトにアクセスしてください:

http://www.pwmis.com/sqlmap

SODフレームワークを選択していただきありがとうございます。開発の大きな助けとなることを信じています。

SOD開発チーム

深藍醫生(ディープブルードクター)

2016.11.13

------------PS---------------

SOD開発チームの @広州-銀古 さんに感謝します。彼はSODフレームワークのnugetパッケージを最新バージョンに迅速に更新してくれました。前述のnugetパッケージの問題はもうありません。

最後に、SODライブラリのリポジトリアドレスとスクリーンショット情報を添付します:

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2024/02/29

Winformでもこんなデータ表示ができる

winform開発の過程で、データ表示機能が必要になることがよくあります。これまではgridcontrolコントロールを使用していましたが、今日は例を通して、winform blazor hybridでant design blazorのtableコンポーネントを使ってデータ表示を行う方法を紹介します。

続きを読む
同じカテゴリ / 同じタグ 2024/02/29

Winformの画面も綺麗にできる?

先日、winformでblazor hybridを使用することを紹介しました。また、blazorのUIを組み合わせることでwinformプログラムのデザインをより美しくできると言いました。今回はwinform blazor hybridで描画する例を挙げて説明します。参考になれば幸いです。

続きを読む