WPF で IDE ライクなビューを作る (ドッキングウィンドウ・AvalonDock)

以前 hatena の方でAvalonDock について書いてました。
http://lriki.hatenablog.com/entry/2014/12/23/215723
http://lriki.hatenablog.com/entry/2014/12/01/235751

が、実務とかで役立ちそうな技術情報は Qiita の方がいいかなと思ったので一部こっちにお引越しです。


さてさて、AvalonDock を MVVM で使い倒してこんなの作ります。

![20160613.png](https://qiita-image-store.s3.amazonaws.com/0/129567/77178758-d0b9-602f-cab6-7241488346cb.png)

プロジェクト一式はコチラです。
https://github.com/lriki/WPFSkeletonIDE/tree/GenericTheme1

前提条件
--------------------
- .NET 4.6.1
- Livet 1.3
- AvalonDock (Extended WPF Toolkit 2.9.0)


準備
--------------------
プロジェクトは Livet のテンプレートで作り、AvalonDock を NuGet から引っ張ってきています。

```
PM> Install-Package Extended.Wpf.Toolkit
```

ヘルパークラス
--------------------
以下、自分で用意したクラスです。

- LayoutItemTemplateSelector.cs
- LayoutItemContainerStyleSelector.cs
- LayoutInitializer.cs

AvalonDock のサンプルでは C# コードで ViewModel の型に対応するテンプレートやスタイル、初期レイアウトを決定していますが、一方これらのクラスは XAML 上で ViewModel の型との対応付けを完結させるためのユーティリティです。そのまま他のプロジェクトにコピペして使えると思います。

View
--------------------
![20160621_1.png](https://qiita-image-store.s3.amazonaws.com/0/129567/74261b1d-0593-60d3-fcb0-2ad4d8c06e88.png)

各ウィンドウの中身をユーザーコントロールとして作ります。

何のウィンドウの内容なのかを区別できるようにするため、適当に TextBlock を置いておきます。
![20160621_2.png](https://qiita-image-store.s3.amazonaws.com/0/129567/745c8460-4fd5-1b13-048a-4c75e367527d.png)


ViewModel
--------------------
View に対応する ViewModel を黙々と作ります。
![20160621_3.png](https://qiita-image-store.s3.amazonaws.com/0/129567/ce6009f5-24e7-cec2-8b49-ea2763273b63.png)

だいたい次のようなクラスがたくさんできます。

```csharp
/// <summary>
/// [出力] Pane の ViewModel
/// </summary>
public class OutputPaneViewModel : PaneViewModelBase
{
    #region Title Property
    public override string Title
    {
        get { return "出力"; }
    }
    #endregion

    #region ContentId Property
    public override string ContentId
    {
        get { return "OutputPaneViewModel"; }
    }
    #endregion
}
```

とりあえず今回の中身はウィンドウタイトルと「コンテンツID」と呼ばれる値のプロパティ2つです。
コンテンツID はレイアウトの保存・復元などで使用します。アプリケーション内で1つしかないAnchorableなどはクラスの完全修飾名を返しておけばOKです。ソースコードを編集するドキュメントウィンドウはインスタンスが複数作られるので、例えばそのウィンドウで編集中のソースファイル名を返したりします。

MainWindowViewModel は、Document の ViewModel コレクションと、AnchorablePane の ViewModel コレクションの2つをプロパティに持ちます。
また、コンストラクタで ViewModel を作ってます。

```csharp
public class MainWindowViewModel : ViewModel
{
    /// <summary>
    /// ドッキングドキュメントの ViewModel のリスト
    /// </summary>
    public ObservableCollection<ViewModel> DockingDocumentViewModels {...}

    /// <summary>
    /// ドッキングペインの ViewModel のリスト
    /// </summary>
    public ObservableCollection<ViewModel> DockingPaneViewModels {...}

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public MainWindowViewModel()
    {
        DockingDocumentViewModels = new ObservableCollection<ViewModel>();
        DockingDocumentViewModels.Add(new ViewModels.Documents.SourceFileDocumentViewModel());
        DockingDocumentViewModels.Add(new ViewModels.Documents.ProjectSettingDocumentViewModel());

        DockingPaneViewModels = new ObservableCollection<ViewModel>();
        DockingPaneViewModels.Add(new ViewModels.Panes.ErrorListPaneViewModel());
        DockingPaneViewModels.Add(new ViewModels.Panes.OutputPaneViewModel());
        DockingPaneViewModels.Add(new ViewModels.Panes.PropertyPaneViewModel());
        DockingPaneViewModels.Add(new ViewModels.Panes.SolutionExplorerPaneViewModel());
    }
}
```

MainWindow
--------------------
まずはこんな感じで名前空間を追加します。

```xml
xmlns:vDocuments="clr-namespace:WPFSkeletonIDE.Views.Documents"
xmlns:vPanes="clr-namespace:WPFSkeletonIDE.Views.Panes"
xmlns:vmDocuments="clr-namespace:WPFSkeletonIDE.ViewModels.Documents"
xmlns:vmPanes="clr-namespace:WPFSkeletonIDE.ViewModels.Panes"
xmlns:avalonDock="http://schemas.xceed.com/wpf/xaml/avalondock"
```

DockingManager 要素です。
MainWindow の Gird の下とかに配置します。

```xml
<!-- ======== ドッキングエリア ======== -->
<avalonDock:DockingManager 
    x:Name="_dockingManager" Grid.Row="1" 
    DocumentsSource="{Binding DockingDocumentViewModels}"
    AnchorablesSource="{Binding DockingPaneViewModels}">

    <!-- ======== LayoutItem コンテナ (ウィンドウやタブ) のスタイル ======== -->
    <avalonDock:DockingManager.LayoutItemContainerStyleSelector>
        <v:LayoutItemContainerStyleSelector>
            <!-- Document のスタイル -->
            <v:LayoutItemTypedStyle DataType="{x:Type vmDocuments:DocumentViewModelBase}">
                <Style TargetType="{x:Type avalonDock:LayoutItem}">
                    <Setter Property="Title" Value="{Binding Model.Title}" />
                </Style>
            </v:LayoutItemTypedStyle>
            <!-- Pane のスタイル -->
            <v:LayoutItemTypedStyle DataType="{x:Type vmPanes:PaneViewModelBase}">
                <Style TargetType="{x:Type avalonDock:LayoutAnchorableItem}">
                    <Setter Property="Title" Value="{Binding Model.Title}"/>
                </Style>
            </v:LayoutItemTypedStyle>
        </v:LayoutItemContainerStyleSelector>
    </avalonDock:DockingManager.LayoutItemContainerStyleSelector>

    <!-- ======== ウィンドウ内容のテンプレート ======== -->
    <avalonDock:DockingManager.LayoutItemTemplateSelector>
        <v:LayoutItemTemplateSelector>
            <!-- [プロジェクト設定] Document -->
            <DataTemplate DataType="{x:Type vmDocuments:ProjectSettingDocumentViewModel}">
                <vDocuments:ProjectSettingDocument />
            </DataTemplate>
            <!-- [ソースコード] Document -->
            <DataTemplate DataType="{x:Type vmDocuments:SourceFileDocumentViewModel}">
                <vDocuments:SourceFileDocument />
            </DataTemplate>
            <!-- [エラー一覧] Pane -->
            <DataTemplate DataType="{x:Type vmPanes:ErrorListPaneViewModel}">
                <vPanes:ErrorListPane />
            </DataTemplate>
            <!-- [出力] Pane -->
            <DataTemplate DataType="{x:Type vmPanes:OutputPaneViewModel}">
                <vPanes:OutputPane />
            </DataTemplate>
            <!-- [プロパティ] Pane -->
            <DataTemplate DataType="{x:Type vmPanes:PropertyPaneViewModel}">
                <vPanes:PropertyPane />
            </DataTemplate>
            <!-- [ソリューション エクスプローラ] Pane -->
            <DataTemplate DataType="{x:Type vmPanes:SolutionExplorerPaneViewModel}">
                <vPanes:SolutionExplorerPane />
            </DataTemplate>
        </v:LayoutItemTemplateSelector>
    </avalonDock:DockingManager.LayoutItemTemplateSelector>

    <!-- ======== デフォルトレイアウト ======== -->

    <!-- ContentId で識別される ViewModel を持つ LayoutAnchorable を、TargetLayoutName で識別される LayoutAnchorablePane に接続する -->
    <avalonDock:DockingManager.LayoutUpdateStrategy>
        <v:LayoutInitializer>
            <v:LayoutInsertTarget ContentId="SolutionExplorerPaneViewModel" TargetLayoutName="LeftSideArea" />
            <v:LayoutInsertTarget ContentId="PropertyPaneViewModel" TargetLayoutName="RightSideArea" />
            <v:LayoutInsertTarget ContentId="ErrorListPaneViewModel" TargetLayoutName="BottomSideArea" />
            <v:LayoutInsertTarget ContentId="OutputPaneViewModel" TargetLayoutName="BottomSideArea" />
        </v:LayoutInitializer>
    </avalonDock:DockingManager.LayoutUpdateStrategy>

    <!-- 初期レイアウト -->
    <avalonDock:LayoutRoot>
        <avalonDock:LayoutPanel Orientation="Horizontal">
            <avalonDock:LayoutAnchorablePane DockWidth="150" Name="LeftSideArea" />
            <avalonDock:LayoutPanel Orientation="Vertical" >
                <avalonDock:LayoutPanel Orientation="Horizontal">
                    <avalonDock:LayoutDocumentPane />
                    <avalonDock:LayoutAnchorablePane DockWidth="150" Name="RightSideArea" />
                </avalonDock:LayoutPanel>
                <avalonDock:LayoutAnchorablePane DockHeight="150" Name="BottomSideArea" />
            </avalonDock:LayoutPanel>
        </avalonDock:LayoutPanel>
    </avalonDock:LayoutRoot>

</avalonDock:DockingManager>
```

### 「avalonDock:DockingManager」の部分
DocumentsSource="{Binding DockingDocumentViewModels}"
AnchorablesSource="{Binding DockingPaneViewModels}"
で、MainWindowViewModel のコレクションをバインドしています。
ViewModel 側で DockingDocumentViewModels に何か Add すればドキュメントウィンドウがどんどん増えて行くようになります。

### 「LayoutItem コンテナ (ウィンドウやタブ) のスタイル」の部分
ウィンドウの中身はユーザーコントロールで用意しましたが、タイトルやタブ文字列のバインドは MainWindow 側でする必要があります。
タブにアイコンを増やしたりなどもココで行います。

### 「ウィンドウ内容のテンプレート」の部分
ViewModel に対応する View (先ほど用意したユーザーコントロール) を指定します。
例えば MainWindowviewModel.DocumentsSource に ProjectSettingDocumentViewModel が Add されたら ProjectSettingDocument.xaml ユーザーコントロールが配置されたウィンドウが表示されます。

### 「デフォルトレイアウト」の部分
v:LayoutInitializer では、ViewModel の ContentId で示されるウィンドウが、どの名前のエリアに配置されるか指定します。
そのエリアの配置は「初期レイアウト」の部分で行います。

なお、ここの v:LayoutInitializer や ContentId、名前で配置先を指定する機能は AvalonDock の機能ではなく、冒頭で紹介したヘルパークラスの機能です。


まとめ
--------------------
本家のサンプルだけでは「結局ビハインドコード書かなければ新しいウィンドウ増やせないのでは？」とか思ってしまいますが、今回のようなヘルパークラスを用意しておけば後はもう XAML だけで完結できます。


余談
--------------------
私が初めてAvalonDockを使ったのももう3年くらい前になりますが、MVVM ベースで非常に使い易くまとまっているドッキングウィンドウライブラリは未だにこれだけな気がします。これはもう完成されたアーキテクチャなのかな。(MaterialDesignInXamlToolkit とかありますが、あれはIDE向きじゃなさそうだしなぁ・・・)