いよいよブックマーク機能を実装しました。
今回は、ブックマーク機能を実装するにあたって利用したMVCという設計手法とXBELという規格の話が中心です。
まずはスクリーンショットをご覧下さい。一般的なウェブブラウザに搭載されているような階層構造を持ったブックマークがウィンドウ左側に表示されているのがわかると思います。
このブックマーク機能の仕組みですが、MVC(Model-View-Controller)という設計手法に則って設計されています。なぜこのような手法にしたかというと複数のウィンドウ間でブックマーク表示の同期を取らなければならないからです。
このツールは一般的なウェブブラウザ同様、複数のウィンドウを開いて作業を行えるようになっているため、あるウィンドウでブックマークの変更を行った場合、他に開いているウィンドウにもその内容が即座に反映させなくてはなりません。
MVCを使わない場合・・・
まず、標準のTreeViewだけで上記の要件を満たしたブックマーク機能を実装することを考えてみます。
TreeViewの表示内容を弄るにはNodesプロパティを使います。ここにTreeNodeオブジェクトを追加したり削除したりすることによりTreeViewの表示内容を弄ることができます。TreeNodeは名前の通りツリー構造になっていて一つ以上の子TreeNodeを追加することができ、これによりデータの階層的な表示を実現できます。つまり、TreeViewは表示に関する機能と、表示するデータを管理する機能の二つを両方持っているということになります。従ってここにブックマークのデータを入れてやればTreeViewを使ってブックマークデータの編集や表示が行えるというわけです。
ところが、複数のウィンドウが開かれていて、それぞれのウィンドウが同じブックマークの内容を表示したい場合はどうでしょう。当然それぞれのウィンドウがTreeViewを持つことになりますがこの場合、変更があったTreeViewの内容を全ての他のTreeViewにコピーして回らなければなりません。
また、あるウィンドウでブックマークの追加や削除が行われたことをどうやって他のウィンドウが知れば良いのでしょうか。また、他のウィンドウで行われたブックマークの変更を自分のウィンドウに反映させるにはどうすれば良いでしょうか。さらに、もっと多くのウィンドウが開かれたり閉じられたりをしていた場合のTreeViewの管理はどうなるでしょう。
おそらくはデリゲート等を用いてウィンドウ間の通知機構を実装する方法にたどりつきますが、考えただけで面倒です。
このような動作をより簡単に実現させるためには、まず表示部分とデータの部分を独立させなければなりません。その上でデータの変更を表示部分に通知するメカニズムと、表示部分に対して行われた操作によって、データの変更を要求するメカニズムを実装する必要があります。これがいわゆるModel View Controllerという設計手法です。
前述の通り、標準のTreeViewだけで上記の要件を満たしたブックマーク機能を実装するのは非常に面倒です。
なぜならWindows.FormsのTreeViewはView(TreeView)とModel(Nodes)が一体になっているため、データと表示部分の分離がされていないからです。
MVCを使うと・・・
そこで、TreeViewには単に表示に専念するだけのViewになってもらい、ブックマークのデータを保持するModelを独自に作ります。
これでViewとModelの分離は行えました。こうするともはやTreeViewのNodesプロパティはブックマークのデータではなく、単なる表示内容のデータになりますから、ブックマークの編集が行われる度にこのデータを他のTreeViewにコピーして回るという面倒なことはしなくても良くなります。
ここでは、ユーザーがブックマークアイテムをあるフォルダへドラッグして移動したケースを例に、MVCがどのように働くかを説明します。
ViewとModelの関係
Modelにはブックマークデータを管理するツリー構造の他に、ブックマークの追加や削除、移動、コピー、名前の変更、フォルダの作成などを行うメソッドを作っておきます。.NET Frameworkには(頻繁に利用されるデータ構造であるにも関わらず)データのツリー構造を管理するクラスが用意されていません。そこでまずはツリー構造を管理するクラスの自作から始めました。
対してViewはWindows標準のTreeViewを用いますが、データの表示に専念させ、データの管理は全く行わないようにします。また、ドラッグ&ドロップやクリック、アイテムの削除の操作は全てコントローラに通知されるようにします。
Controllerの働き
ControllerはユーザーがViewで行った操作に従って、Modelに適切なメッセージを送り、データの変更要求を行う役目を持っています。
上の例ではユーザーがブックマークをドラッグして移動しています。するとこの操作はControllerに通知され、Controllerはその通知に従い実際にモデルに対してデータの変更を要求します。ちなみにControllerはModelのデータを直接いじりません。データの操作を行うのはあくまでModelになります。
ModelからViewへの通知
さて、実際にModelが変更されるとModelからViewへ「Modelの内容が変更されたから表示を更新してください」といった通知が行われます。通知といっても実際にはメソッド呼び出しだったりイベント発生だったりするだけです。ViewはModelの内容を視覚的に認識できる形で画面に表示します。今回の例では、ツリー構造を持ったModelの内容を元に、TreeViewのアイテムの再構築を行っています。
この時大事なことは単一のModelに対してViewは複数存在することができる、ということです。つまり、画面に複数のウィンドウがあり、そこへそれぞれのViewが貼り付けられていたとしても、Modelから各Viewに対して適切に通知が行われれば、あるウィンドウで行ったModelの変更が即座に他のウィンドウのViewにも反映される、ということになります。
複数のViewが存在する場合でも
画面に複数のドキュメントウィンドウが開かれている等、複数のViewが存在する状況でも、単にModelはそれぞれのViewに対して同時にModel更新の通知を送ってやれば、各Viewの表示内容が一斉に更新されるわけです。今回ブックマーク機能の実装にあたりMVCを利用した最大の理由が「場合によっては画面に複数のViewが存在する」という理由です。
ドラッグ&ドロップの実装
今回一番時間がかかったのが、ブックマークのドラッグ&ドロップによる編集です。
TreeViewにはドラッグ&ドロップをサポートする機能が標準で搭載されていますが、若干機能不足です。
たとえば標準のドラッグ&ドロップではTreeNodeを他のTreeNodeの上にドラッグし、ドラッグされたTreeNodeの子とすることは簡単にできますが、あるTreeNodeを二つのTreeNodeの間にドラッグして任意の位置にアイテムの挿入を行う、といった機能はありませんから、こういった機能は自前で実装する必要があります。
また、ドラッグ&ドロップにも下記のようなルールを設ける必要があります。
- あるフォルダをそのフォルダ自身が内包しているフォルダには移動できないようにする。Windowsのファイルシステムを例にあげて説明すると、C:hogeというフォルダと、C:hogehageという二つのフォルダがあった場合、前者のフォルダを後者のフォルダの内部に移動させる操作はできない、といった具合です。
- あるフォルダをそのフォルダ自身の子とすることもできない。
- ブックマークの項目をフォルダへ入れて子とすることはできるが、ブックマークの項目をブックマークの項目の子とすることはできない。
- 同様にフォルダをブックマーク項目に入れて子とすることもできない。
これらの機能を一つ一つ検証しながら実装していたのですが、あまりにもややこしすぎて時間がかかってしまいました。
ブックマークの保存形式
ブックマークの保存は、ブックマーク記述言語「XBEL」形式で行います。XBELとはXML Bookmarks Exchange Languageの略で、XMLを利用したブックマークの相互利用のための規格です。XML形式ですので、ブックマークの保存の際にはブックマークのツリー構造を再帰的に辿ってXmlDocumentを構築した後、ファイルに出力しています。XBEL形式に関してはこのページを参考にしました。
この形式を利用する利点は、様々なブラウザのブックマークとの相互変換が可能である事です。自作のアプリとはいえ、独自のデータ形式を使ってしまっては後々困ることになりますから、オープンな規格はどんどん利用すべきだと思います。
さて、次回はWeb版、デスクトップアプリ版両方のDMMアフィリエイトリンク作成ツールに搭載されているキャッシュシステムについて書いてみようかと思います。