2つのViewの変更が、同時に行われるときのModelの挙動を考察

具体的に言えば、以下の状況を実現させます。

■目的

  • GridView と GraihpcsView の 2つの View がある。
  • GridView の項目をチェックすると、GraphicsView のある点が反転する。
  • 逆に GraphicsView のある点を反転させると、GridView の項目のチェックが反転する。

という、2つのViewの相互伝播を実装します。

■制限

C# でやるのが良いのでしょうが、手元の課題が C++ なので、C++ で実装します。
いや、C# で書いてから C++ に書き直したほうが早いかな?これは、検討を進めながら。

■まずは Model を作る。

2 つの View は同じ Model を共有しているという前提を作ります。直接 View 同士が繋がっているというスタート地点でもよいのですが、それは昔の話。そのあたりは端折って、Model -> View という分離を念頭に入れます。

class Model 
{
public:
	int gridid;
	float x;
	float y;
	float z;
	bool selected;
};

ひとまず、POJO なデータクラスを作ります。x,y,z は三次元データの位置情報、gridid が識別子で、selected が選択状態ですね。
これをコレクションとして扱うので、

class Models : public vector<Model*>
{
};

な感じにします。今回は挿入/削除を考えないので vector でよいかと。

■GridView を作る

class GridView 
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->selected << endl;
		}
	}
	void onChecked( int row, bool selected ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			if ( row == 0 ) {
				Model *m = *it;
				m->selected = selected;
				break;
			}
			row --;
		}
	}
};

for 文で廻しているところがダサいですが、ひとまず表示用の print 関数と、項目をチェックした時の onChecked 関数を作っておきます。
onCheced 関数では、行番号を渡すようにします。

Models を内部に持つようになっていますが、これは後から分離させたいところです。

■GraphicsView を作る

class GraphicsView 
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->selected << endl;
		}
	}
	void onClick( int gridid ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			if ( m->gridid == gridid ) {
				m->selected = !m->selected;
				break;
			}
		}
	}
};

基本動作は GridView と同じですが、onClick で渡されるのは gridid にしておきます。

■相互伝播を実装する

さて、GridView::onChecked と GraphicsView::onClick が相互に連携してることが分かるので、これを素直に連携させます。

class GrahicsView;
class GridView;
static GraihpcsView *grahpicsView;
static GridView *gridView;

class GridView 
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->selected << endl;
		}
	}
	void onChecked( int row, bool selected ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			if ( row == 0 ) {
				Model *m = *it;
				m->selected = selected;
				graphicsView->onClick( m->gridid ); // NG
				break;
			}
			row --;
		}
	}
};

class GraphicsView 
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->selected << endl;
		}
	}
	void onClick( int gridid ) {
		if ( models == NULL ) return ;
		int row = 0;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			if ( m->gridid == gridid ) {
				m->selected = !m->selected;
				graphicsView->onClick( row, m->selected ); // NG
				break;
			}
			row ++;
		}
	}
};

一見、良さそうに見えますが、片方が呼び出されると常にもう片方が呼び出され、再び片方を呼び出すというメッセージの無限伝播に陥ってしまって駄目です。
また、C++ の場合この onClick と onChecked のコンパイルを通すのは結構困難です。クラスをプロトタイプ宣言にしないと駄目ですね。C#ならば、このままでもコンパイルは通りますが、やっぱり、無限にメッセージが伝播してしまいます。
無限に伝播しないようにフラグを付けることも考えられますが、バグの温床となるのでやめておきましょう。ここでは、Model の書き換えをする、そして Model の変更が通知される、という方式に代えていきます。

  • GrahpicsView -> Model の変更
  • GridView -> Model の変更
  • Model -> GraphicsView へ変更通知
  • Model -> GridView への変更通知

というメッセージの流れです。こうすることによって、

  1. GraphicsView::OnClick を呼び出す。
  2. Model が変更される。
  3. Model から GridView へ変更が通知される。
  4. Model から GraphicsView へ変更が通知される。

というフローになります。4番目は通知としては無駄(自分で Model を変更しているので)になりますが、GraphicsView と GridView への対称性を保つためにこのままにしておきます。

■Model の変更通知を実装する

Model クラスは、INotify インターフェースを継承するようにして、関数ポインタとして onChanged を登録させるようにします。

class INotify
{
public:
	void (*onChanged)(void *sender);
	void onProperyChanged( void *sender );
};

class Model : public INotify
{
public:
	int gridid;
	float x;
	float y;
	float z;
protected:
	bool selected;
public:
	bool getSelected() { return this->selected; }
	void setSelected( bool v ) {
		if ( this->selected != v ) {
			this->selected = v;
			if ( onChanged != NULL ) {
				onChanged( this );
			}
		}
	}		
public:
	Model( int id, float x, float y, float z ) {
		this->gridid = id;
		this->x = x;
		this->y = y;
		this->z = z;
		this->selected = false;
	}
};

こうやっておいて、下記のように onChanged に登録したいところなのですが、実際はうまくコンパイルが通りません。

class GridView : public INotify
{
public:
	Models *models;
public:
	GridView() {
		this->onChanged = &GridView::onProperyChanged;	// compile error
	}

	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->getSelected() << endl;
		}
	}
	void onChecked( int row, bool selected ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			if ( row == 0 ) {
				Model *m = *it;
				// m->selected = selected;
				m->setSelected( selected );
				break;
			}
			row --;
		}
	}
	void onProperyChanged( void *sender )
	{
		auto model = (Model*)sender;
		cout << "GridView::onChanged: " << model->gridid << endl;	
	}
};

alice018.cpp(56) : error C2440: ‘=’ : ‘void (__thiscall GridView::* )(void *)’ から ‘void (__cdecl *)(void *)’ に変換できません。この変換が可能なコンテキストはありません。

のようなコンパイルエラーがでます。親クラス名が入ってしまうので柔軟性が失われてしまったのです。まぁ、厳密な型をチェックする分にはこちらのほうが良いのではないでしょうか。
また、この onChanged イベントは、ひとつの View に対してしか通知ができません。今回の場合のように、GrapihcView, GridView へ同時に通知するということができません。

__event と __hook を使って mvvm を実装してみる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3559

なので、今回は素直に __event/__hook を使ってしまいます。

■Model の実装をやり直す

class INotify
{
public:
	__event void onChanged(void *sender);
};
class ITarget
{
public:
	void onPropertyChanged( void *sender );
};

class Model : public INotify
{
public:
	int gridid;
	float x;
	float y;
	float z;
protected:
	bool selected;
public:
	bool getSelected() { return this->selected; }
	void setSelected( bool v ) {
		if ( this->selected != v ) {
			this->selected = v;
			onChanged( this );
		}
	}		
public:
	Model( int id, float x, float y, float z ) {
		this->gridid = id;
		this->x = x;
		this->y = y;
		this->z = z;
		this->selected = false;
	}
};

Model クラスには、__event キーワードを付けて、onChanged イベント登録の準備をしておきます。selected プロパティが変更になったときに、onChanged を呼び出します。
この時点で、View を判断させてもよいのですが、依存関係といしては Model <- View が望ましいので、あえてこういう形にしておきます。

■Model からの通知を View で受ける。

Model で INotify::onChanged を呼び出したときには、ITagert::onPropertyChanged を呼び出すようにします。
GridView, GraphicsView のそれぞれに、onPropertyChanged を実装しておきます。

class GridView : public ITarget
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->getSelected() << endl;
		}
	}
	void onChecked( int row, bool selected ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			if ( row == 0 ) {
				Model *m = *it;
				// m->selected = selected;
				m->setSelected( selected );
				break;
			}
			row --;
		}
	}
	void onPropertyChanged( void *sender )
	{
		auto model = (Model*)sender;
		cout << "GridView::onChanged: " << model->gridid << endl;	
	}
};

class GraphicsView : public ITarget
{
public:
	Models *models;
public:
	void print() {
		if ( models == NULL ) return;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			cout << m->gridid << ":" << m->getSelected() << endl;
		}
	}
	void onClick( int gridid ) {
		if ( models == NULL ) return ;
		for ( auto it = models->begin(); it != models->end(); ++it ) {
			Model *m = *it;
			if ( m->gridid == gridid ) {
				// m->selected = !m->selected;
				m->setSelected( !m->getSelected() );
				break;
			}
		}
	}
	void onPropertyChanged( void *sender )
	{
		auto model = (Model*)sender;
		cout << "GraphicsView::onChanged: " << model->gridid << endl;	
	}
};

■バインドする

__hook キーワードを使って、Model::onChanged と、GridView::onPropertyChanged をバインドしていきます。
Model の通知先は、2 の View になるので、__hook を 2回定義します。

int main()
{
	models = new Models();
	graphicsView = new GraphicsView();
	gridView = new GridView();
	graphicsView->models = models;
	gridView->models = models;

	for ( int i=0; i<10; i++ ) {
		auto model = new Model(i,0.0,0.0,0.0);
		__hook( &Model::onChanged, model, &GridView::onPropertyChanged, gridView );
		__hook( &Model::onChanged, model, &GraphicsView::onPropertyChanged, graphicsView );
		models->push_back( model );
	}
	graphicsView->onClick( 2 );
	gridView->print();

	gridView->onChecked( 8, true );
	gridView->print();
}

ここでは、Model の selected プロパティから直接変更通知を受けるために、すべての model に対して onChanged を登録します。
ただ、これだと面倒なので、vector::onChanged に登録して、複数の model を一括に扱うようにしたいですよね。

■実行結果

実行結果はこんな感じです。
最初は、GraphicsView -> Model -> GridView へのデータ伝播。二番目のものは、GridView -> Model -> GraphicsView での伝播ですね。

D:\work\blog\src\alice>alice018
GraphicsView::onChanged: 2
GridView::onChanged: 2
0:0
1:0
2:1
3:0
4:0
5:0
6:0
7:0
8:0
9:0
GraphicsView::onChanged: 8
GridView::onChanged: 8
0:0
1:0
2:1
3:0
4:0
5:0
6:0
7:0
8:1
9:0

あえて、ViewModel, Controller を使わずに書いていますが、このバインドの部分は ViewModel としても良いはずです。
Model::onChanged イベントに View の型が入ってこないので、どんな View が来ても(あるいは View がなくても) Model を変える必要がありません。
そういう意味ではそれなりに独立性が高いかなと。

カテゴリー: C++ パーマリンク