ApplicationBar のコマンド実装・バインディングとメニューを動的に変更する
Windows Phone7 で Silverlight プログラミングをしていく上で、やっかいなのが AplicationBar です。
どうも内部的には ApplicationBar は Silverlight オブジェクトではないらしく、デフォルトでは様々な制約があります。
- バーやボタン、メニューの各プロパティに Binding が出来ない。
- コマンド指定することが出来ない。
- ボタンやメニューに名前(x:Name)でアクセスすると null。ApplicationBar.Button[0] 等としてアクセスする必要がある。
- ボタンのアイコン(PNG)は "コンテンツ" でビルドしたものだけを指定出来る(Resource でビルドした png は不可)。
などおかしなところがあります。
ApplicationBar はプロパティに Binding が出来ないわけですから、Text プロパティへの文字列指定もハードコードするかコードビハインドで変えてやる必要が出てきます。日本語だけならともかく、英語などのローカライズを考えたら嫌になりますね。またコマンドの指定が出来ないので、ボタンが押されたときの実行もコードビハインドで Click イベントハンドラを記述する必要があります。MVVM をするにはかっこ悪いですね。
だけれどもこれらをすべて解決するクラス BindableApplicationBar クラスが Codeplex で公開されています。
Phone7.Fx
http://phone7.codeplex.com/
BindableApplicationBar の使い方
使い方は簡単です。まず codeplex から Phone7.Fx.zip をダウンロードし、同梱されている Phone7.Fx.dll をプロジェクトの参照に追加します。
後は xaml で名前空間を定義し、
xmlns:fx="clr-namespace:Phone7.Fx.Controls;assembly=Phone7.Fx"
BindableApplicationBar の定義をするだけ、簡単でしょ?
※BindableApplicationBar の定義は必ず LayoutRoot 内部に入れるようにして下さい。
<!--ContentPanel - 追加コンテンツをここに入力します--> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"></Grid> ~中略~ <fx:BindableApplicationBar x:Name="AppBar" BarOpacity="1.0" IsVisible="{Binding IsBarVisible}" > <fx:BindableApplicationBarIconButton Command="{Binding AddCommand}" Text="Add" IconUri="/images/appbar.add.rest.png" /> <fx:BindableApplicationBar.MenuItems> <fx:BindableApplicationBarMenuItem Text="Settings" Command="{Binding SettingsCommand}" /> </fx:BindableApplicationBar.MenuItems> </fx:BindableApplicationBar> </Grid>
これですべてのプロパティにバインディング出来るようになりました。他のプロパティも ApplicationBar と同じく使用することが出来ます。ただし Button.Click イベントは発生しないので、コマンドで実装する必要があります。つまるところ、BindableApplicationBar や BindableApplicationBarIconButton は、デフォルトの ApplicationBar, ApplicationBarIconButton の Wrapper クラスとして動作します。
めでたしめでたし。
ちなみに Expression Blend では BindableApplicationBar を配置することが出来ませんので、XAML を直接編集する必要があります。また Blend で開くと "Cannot clear the icon while in a list" というエラーメッセージが BindableApplicationBarIconButton 上に表示されますが、問題なくコンパイルは出来ますのでご安心を。
Text プロパティにバインドして言語環境によって文字列リソースが自動的に変わる&コマンドが実行されるサンプルを用意しました。
WP7 の設定→「地域&言語」→「英語か日本語」にして起動すると文字列が変わるのが分かると思います。※設定変更後は「ここをタップ」して再起動して下さいね。
動的に ApplicationBar を変更する
これだけではおもしろくないので、もう少しやってみます。
Pivot や Panorama で表示するアイテムに応じてメニューを変えたいときがあります。
ApplicationBar でメニュー構成を変えるには、コードビハインド上で ApplicationBarIconButton のインスタンスを生成しプロパティを設定した上で入れ替えるなど手数が多くなります。が、BindableApplicationBar を使えばコードビハインドのコードは必要ですがはるかに簡単にできます。しかし少し工夫が必要です。
XAML でのメニューとボタンの定義
まず XAML です。
いつものように ApplicationBar を定義しておきますが、ボタンなどは何も定義しません。
上の例では BindableApplicationBar をボタンの入れ物にしていましたが、動的に変更する場合は ApplicationBar を入れ物にします。
<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar/> </phone:PhoneApplicationPage.ApplicationBar>
次にボタンとメニューを XAML に定義します。
XAML のどこでも良いのですが、LayoutRoot 内に BindableApplicationBarIconButton, BindableApplicationBarMenuItem だけを定義しておきます。
<!-- <fx:BindableApplicationBar x:Name="AppBar" BarOpacity="1.0" IsVisible="{Binding IsBarVisible, ElementName=phoneApplicationPage}"/> --> <fx:BindableApplicationBarIconButton x:Name="AddButton" Command="{Binding AddCommand, ElementName=phoneApplicationPage}" Text="{Binding Localizedresources.Add_String, Source={StaticResource LocalizedStrings}}" IconUri="/icons/appbar.add.rest.png" /> <fx:BindableApplicationBarIconButton x:Name="RemoveButton" Command="{Binding AddCommand, ElementName=phoneApplicationPage}" Text="{Binding Localizedresources.Remove_String, Source={StaticResource LocalizedStrings}}" IconUri="/icons/appbar.minus.rest.png" /> <fx:BindableApplicationBarMenuItem x:Name="SettingsMenuItem" Text="{Binding Localizedresources.Settings_String, Source={StaticResource LocalizedStrings}}" Command="{Binding SettingsCommand, ElementName=phoneApplicationPage}" /> </Grid> </phone:PhoneApplicationPage>
これで PhoneApplicationPage 上に、ApplicationBar だけ、BindableApplicationBarIconButton だけ、BindableApplicationBarMenuItem だけのインスタンスが生成されます。
動的にメニューを構成する
ここまででメニューの定義は出来ましたが、このままではメニューには何も表示されません。
次にコードビハインド上に以下のようなメソッドを用意しておきます。
このメソッドは指定したインデックスに応じたメニューを構成するメソッドです。
このメソッドでの肝は、ApplicationBar.Buttons に、BindableApplicationBarIconButton.Button を追加しているところです。
private void SetButtons(int index) { this.ApplicationBar.Buttons.Clear(); this.ApplicationBar.MenuItems.Clear(); if (index == 0) { this.ApplicationBar.Buttons.Add(this.AddButton.Button); this.ApplicationBar.MenuItems.Add(this.SettingsMenuItem.MenuItem); } if (index == 1) { this.ApplicationBar.Buttons.Add(this.RemoveButton.Button); } }
本来であれば BindableApplicationBar.Buttons.Add(this.AddButton) としておけば正しく動きそうですが、実際にはこの画像のように例外が発生してしますいます。
どうも Buttons.Clear() をしても、ビジュアルツリー上にボタンとメニューが残ってしまうようです。
と言う訳で ApplicationBar を入れ物として用意し、入れるものは BindableApplicationBarIconButton.Button や BindableApplicationBarMenuItem.MenuItem を入れます。
BindableApplicationBarIconButton.Button プロパティには、Wrap された ApplicationBarIconButton の実体が入っていますので、正しくメニューが表示されかつバインディングなども問題なく行われます。
Panorama が遷移したらメニューも変えるには、以下の XAML のように Panorama がロードされたときと遷移したときに MenuUpdateCommand を実行し、コマンド内で SetButtons メソッドを呼べば画面遷移にあわせてメニューが変わります。
XAML
<!--LayoutRoot は、すべてのページ コンテンツが配置されるルート グリッドです--> <Grid x:Name="LayoutRoot" Background="Transparent"> <!--パノラマ コントロール--> <controls:Panorama x:Name="panorama" Title="マイ アプリケーション"> <controls:Panorama.Background> <ImageBrush ImageSource="PanoramaBackground.png"/> </controls:Panorama.Background> <i:Interaction.Triggers> <i:EventTrigger> <i:InvokeCommandAction Command="{Binding MenuUpdateCommand, ElementName=phoneApplicationPage, Mode=OneWay}" CommandParameter="{Binding ElementName=panorama, Mode=OneWay}"/> </i:EventTrigger> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding MenuUpdateCommand, ElementName=phoneApplicationPage, Mode=OneWay}" CommandParameter="{Binding ElementName=panorama, Mode=OneWay}"/> </i:EventTrigger> </i:Interaction.Triggers>
コードビハインド
#region コマンド (MenuUpdateCommand) private ICommand _MenuUpdateCommand; public ICommand MenuUpdateCommand { get { return this._MenuUpdateCommand ?? (this._MenuUpdateCommand = new RelayCommand<Panorama>( item => { this.SetButtons(item.SelectedIndex); }, item => { return true; } )); } } #endregion
コードビハインドで行う場合は、
private void panorama_SelectionChanged(object sender, SelectionChangedEventArgs e) { this.SetButtons(this.panorama.SelectedIndex); }
としておけば良いですね。
参照情報
How to have binding on the ApplicationBar
http://blog.humann.info/post/2010/08/27/How-to-have-binding-on-the-ApplicationBar.aspx
How to: Build a Localized Application for Windows Phone
http://msdn.microsoft.com/en-us/library/ff637520%28VS.92%29.aspx
後記
Nel 開発のかなり最初の頃(NoDoあたりか?) から BindableApplicationBar を使っていましたが、開発に一生懸命だったのでなかなか Tips を書く余裕がありませんでした。Nel 起動ページの Panorama 遷移に合わせてメニューが変わりますが、ここで紹介した技を使っています。
WP7 の ApplicationBar はなにげにはまる要素の一つなので、書いておきます。
あ、ApplicationBarIconButton.IconUri に指定するアイコン(PNG)は、コンテンツでビルドしたアイコンのみを表示する事が出来ます。"Resource" だと表示されませんので注意を。
これもバグだと思うんだよな-。
DLL 個別配布する場合 ApplicationBarIconButton の画像を DLL 使う側で用意しなきゃいけないし。