2010年08月31日

MenuItemとCompositeCollectionをバインド /WPF

前回 の続き。今回は固定メニューと動的メニューが混在しているケースを考えます。題材は前回に引き続きブラウザのブックマーク。

なお、次回 は今回とは独立した階層メニューの話です。

まず概要を考えましょう。

ウィンドウには上部にメニューが、下部にはブラウザが表示されます。

メニューのトップレベルには「ブックマーク」が一つだけ存在し、その子メニューに、「ブックマークに追加」、区切り線、そして追加済みブックマークの一覧が(全て同じ階層に)表示されます。

「ブックマークに追加」をクリックすると現在表示中のページがブックマークに追加されます。

追加済みブックマークのいずれかをクリックすると、ブラウザはそのブックマークされているページにジャンプします。

さて、前回から修正するコード量が多いので、まず一通りコードを示し、その後に解説を加えることにします。なお、App.xaml / App.xaml.cs は前回のままです。


<!-- Window1.xaml -->
<Window x:Class="Sample.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
    xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
    xmlns:my="clr-namespace:WpfApplication1"
    Title="{Binding ElementName=browser, Path=DocumentTitle}">
  <DockPanel>
    <Menu DockPanel.Dock="Top">
      <MenuItem Header="ブックマーク(_B)" DisplayMemberPath="Title">
        <MenuItem.Resources>
          <CollectionViewSource x:Key="bookmarks" Source="{Binding}"/>
        </MenuItem.Resources>
        <MenuItem.ItemsSource>
          <CompositeCollection>
            <my:StaticMenuItem Title="ブックマークに追加"
               Command="{x:Static my:BookmarkCommands.AddBookmark}"/>
            <my:StaticMenuItem/> <!-- 区切り線 -->
            <CollectionContainer
               Collection="{Binding Source={StaticResource bookmarks}}"/>
          </CompositeCollection>
        </MenuItem.ItemsSource>
        <MenuItem.CommandBindings>
          <CommandBinding Exected="Navigate"
             Command="{x:Static my:BookmarkCommands.Navigate}"/>
          <CommandBinding Exected="AddBookmark"
             Command="{x:Static my:BookmarkCommands.AddBookmark}"/>
        </MenuItem.CommandBindings>
        <MenuItem.ItemContainerStyle>
          <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Command" Value="{Binding Command}"/>
            <Setter Property="CommandParameter" Value="{Binding}"/>
            <Style.Triggers>
              <DataTrigger Binding="{Binding Command}" Value="{x:Null}">
                <Setter Property="Command"
                   Value="{x:Static my:BookmarkCommands.Navigate}"/>
              </DataTrigger>
              <DataTrigger Binding="{Binding Title}" Value="{x:Null}">
                <Setter Property="Template">
                  <Setter.Value>
                    <ControlTemplate TargetType="{x:Type MenuItem}">
                      <Separator Style="{DynamicResource
                                   {x:Static MenuItem.SeparatorStyleKey}}"/>
                    </ControlTemplate>
                  </Setter.Value>
                </Setter>
              </DataTrigger>
            </Style.Triggers>
          </Style>
        </MenuItem.ItemContainerStyle>
      </MenuItem>
    </Menu>
    <i:WindowsFormsHost>
      <wf:WebBrowser x:Name="browser"/>
    </i:WindowsFormsHost>
  </DockPanel>
</Window>

// Window1.xaml.cs
private void Navigate(object sender, ExectedRoutedEventArgs e) {
    Bookmark bm = e.Parameter as Bookmark;
    if (bm != null) {
        this.browser.Navigate(bm.Url);
    }
}
private void AddBookmark(object sender, ExectedRoutedEventArgs e) {
    if (this.browser.Url != null) {
        ICollection<Bookmark> bookmarks
            = (ICollection<Bookmark>)this.DataContext;
        string title = this.browser.DocumentTitle;
        string url = this.browser.Url.ToString();
        bookmarks.Add(new Bookmark(title, url));
    }
}

// StaticMenuItem.cs
public class StaticMenuItem {
    public string Title { get; set; }
    public StaticMenuItem() {
    }
    public StaticMenuItem(string title) {
        this.Title = title;
    }
}

// BookmarkCommands.cs
public static class BookmarkCommands {
    public static RoutedCommand AddBookmark { get; private set; }
    public static RoutedCommand Navigate { get; private set; }
    static BookmarkCommands() {
        BookmarkCommands.AddBookmark = new RoutedCommand();
        BookmarkCommands.Navigate = new RoutedCommand();
    }
}

xmlns:i および xmlns:wf は、WinForm の WebBrowser を使用するために定義しています。同時に WindowsFormsIntegration.dll および System.Windows.Forms.dll を参照に追加する必要があります。

わざわざ WinForm の WebBrowser を使うのは、WPF の System.Windows.Controls.WebBrowser だとドキュメントのタイトルの取得が難しい、Navigate(Uri) メソッドが残念な仕様、というのが主な理由です。ちなみに後者は .NET 4 で Navigate(String) メソッドが追加されて問題なくなりました。

MenuItem.Resources と MenuItem.ItemsSource について。

前回は ItemsSource="{Binding}" で済ませましたが、今回は要素プロパティ構文で CompositeCollection を使用しています。CompositeCollection を使用することにより、既存のコレクションに手を加えないまま、既存のコレクションと他の要素をいっしょくたに扱うことが可能になります。

StaticMenuItem クラスは、メニューアイテムの表示に使用する Title プロパティと、そのメニューアイテムがクリックされたときにどんなコマンドを実行するかを表す Command プロパティが用意されています。区切り線は「Title が null の StaticMenuItem」と定義します。

Title ってプロパティ名は微妙ですが、Bookmark と合わせるのに都合が良いのでこうしておきます。V のクラスなので、VM の実装に対しある程度妥協するのもいいんじゃないかなと。

また、コマンドは前回はフレームワークが用意している NavigationCommands.Favorites を使っていましたが、今回は BookmarkCommands クラスの中に、必要なコマンドを定義することにしました。

既存のコレクションを CompositeCollection 内に入れるのには CollectionContainer を使いますが、ここでネックとなるのは、Collection="{Binding}" という記述が有効ではないことです。CollectionContainer が FrameworkElement / FrameworkContentElement 派生ではないため要素ツリーに参加できないのが原因かと思いますが、CollectionContainer は DataContext を親から継承できないようなんですね。RelativeSource とかも使えません。

今回はこの問題を解決するために、CollectionContainer.Collection 自体には StaticResource をバインディングし、その StaticResource である CollectionViewSource の Source をさらにバインディングする、という二重バインディングする方法を採りました。

ItemContainerStyle は結構様変わりしました。

まず Command プロパティ。既定ではバインディングソースの Command プロパティにバインディングしています。バインディングソースは Bookmark オブジェクトまたは StaticMenuItem オブジェクトであり、このうち Command プロパティを持っているのは StaticMenuItem の場合のみです。Bookmark の場合は無視されます。

しかし Style.Triggers の方で、DataTrigger によってバインディングソースの Command プロパティが null の場合、Command には BookmarkCommands.Navigate を強制的に設定するようにしています。これにより、Bookmark オブジェクトがバインディングソースになった場合 Navigate コマンドが実行されることになります。

実は区切り線を意味する StaticMenuItem も、Command は未設定なので Navigate コマンドが設定されているんですが、Separator はクリックできないので問題ありません。

もう一つの DataTrigger が、Title が null の場合、つまり区切り線を表す場合のトリガです。

メニュー項目を区切り線に置き換えるのは、Template を差し替えることで実現しています。

「ブックマークに追加」をクリックすると、CommandBindings により AddBookmark メソッドが呼び出されます。

手抜きですが、取り敢えず DataContext を使ってコレクションを操作することにしています。

ObservableCollection に Add されると、アイテム追加イベントは CollectionViewSource、CollectionContainer と順に通知され、無事に MenuItem の末尾に追加したブックマークが表示されるようになります。

posted by Hongliang at 06:32| Comment(0) | TrackBack(0) | WPF | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/161084723

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。