WindowsPhoneに限らずリスト系のコントロールは入門者がコントロールに慣れるのにちょうどよい素材だと思っている。
データバインディング、テンプレート、スタイル色々なXAMLの大事な概念を利用しないと弄れない。
というわけで、次回のWindowsPhoneハンズオンin広島はListBoxを中心にデータバインディングの話をしようかなと思っているんだけど、
スタイルの変更方法を忘れないうちにメモしておく。
(メモを書いてみて、Blend使いこなしてないなぁ自分・・・と再確認)
ListBoxに表示する対象のデータは当然だがリスト、同じ構造のデータの繰り返しだ。
繰り返しデータを動的に指定するには地道にListBox.Items.Addなどで追加を繰り返していけないこともないが、DataContextに突っ込んでバインドするのが主な使い方になる。
簡単なサンプルとしてlistboxという名前(x:Name)を持つコントロールに値を表示する場合以下のようになる。
listBox.DataContext = new List<string>() { "hoge", "hone", "hige"};
コードビハインド側では上記のようにDataContextに値を代入してあげればよい。XAML側にバインドすることを教えてあげることも忘れないで。
<ListBox Height="467" ItemsSource="{Binding}" HorizontalAlignment="Left" Margin="12,19,0,0" Name="listBox" VerticalAlignment="Top" Width="424" />
これがリストへのバインディング第一歩。
上記は各リストがTextBlockを一つだけ持つシンプルなものだったので上記のようにデフォルトのコントロールで実現することができた。
では複数のコントロールを持つようなリストはどうやって実現するのだろう、という疑問にぶつかる。
そのような場合はItemTemplateを使ってリストのひな形を定義してあげる。
サンプルは以下のようになる
<ListBox> <ListBox.ItemTemplate> <DataTemplate x:Key="DataTemplate1"> <Grid Height="148" Width="413"> <Button Content="Button" Margin="19,0,22,0" d:LayoutOverrides="Width" Height="68" VerticalAlignment="Bottom"/> <TextBlock Height="32" Margin="42,20,42,0" TextWrapping="Wrap" Text="ItemsTemplateSample" VerticalAlignment="Top"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
実行結果は以下。ItemsSourceにバインドしたデータはそのままだがテキストの内容はTextBlockコントロールのTextプロパティの表示になる。
ここでデータをバインドしたい場合は以下のようにDataTemplate内のTextBlockのTextに{Binding}を指定してあげる。
<DataTemplate x:Key="DataTemplate1"> <Grid Height="148" Width="413"> <Button Content="Button" Margin="19,0,22,0" d:LayoutOverrides="Width" Height="68" VerticalAlignment="Bottom"/> <TextBlock Height="32" Margin="42,20,42,0" TextWrapping="Wrap" Text="{Binding}" VerticalAlignment="Top"/> </Grid> </DataTemplate>
TextBlockのText属性とButtonのContent属性に別の値をバインドしたい場合はどうやって書くのだろう。
その実現にはList<string>() { “hoge”, “hone”, “hige”};では対応できない。
そのために新しいクラスを作成する。
public class listItem { public string textStr { get; set; } public string buttonStr { get; set; } }
textStrとbuttonStrという二つのプロパティを持つクラスをバインドする。
listBox.DataContext = new List<listItem>() { new listItem() { buttonStr="hoge", textStr="hoge_text" } , new listItem() {buttonStr="hige", textStr="hige_text"} };
お気づきだと思うが、これまでの{Binding}という記述では上記のようにバインド対象を指定した場合どのプロパティと対応するのかがわからない。
その対応として、XAML側も以下のように書き換える。
<DataTemplate x:Key="DataTemplate1"> <Grid Height="148" Width="413"> <Button Content="{Binding buttonStr}" Margin="19,0,22,0" d:LayoutOverrides="Width" Height="68" VerticalAlignment="Bottom"/> <TextBlock Height="32" Margin="42,20,42,0" TextWrapping="Wrap" Text="{Binding textStr}" VerticalAlignment="Top"/> </Grid> </DataTemplate>
ここまでで入門編は終了というところだ。
下記画像のようにリストをクリックすると、選択状態になり文字色が赤に変わった(赤かそれ以外かはアプリのテーマカラーによる)。
だけどボタンの方は何も変化がないのがわかると思う。
どうして、テキストは文字色が変更されてボタンには影響がないのだろうか?
その理由を確認するためにListBoxのスタイルを編集してみる。
上記画像のようにBlendからListBoxのItemContainerStyleを編集する。
ListBoxのスタイルはデフォルトで以下のような感じになる。
<Style x:Key="ListBoxItemStyle1" TargetType="ListBoxItem"> <Setter Property="Background" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="Padding" Value="0"/> <Setter Property="HorizontalContentAlignment" Value="Left"/> <Setter Property="VerticalContentAlignment" Value="Top"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBoxItem"> <Border x:Name="LayoutRoot" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"/> <VisualState x:Name="Disabled"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="LayoutRoot"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TransparentBrush}"/> </ObjectAnimationUsingKeyFrames> <DoubleAnimation Duration="0" To=".5" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="ContentContainer"/> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Unselected"/> <VisualState x:Name="Selected"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
今回関係ある部分だけ説明すると、「リストボックスが選択された場合」「ボタンが押された場合」などの状態によるスタイルの編集は、
VisualStateManagerで指定されている。 今回なら、
<VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Unselected"/> <VisualState x:Name="Selected"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup>
上記のような部分だ。 VisualStateのX:NameがSelectedの部分が選択状態でのスタイルで、
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentContainer"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames>
Foregroundの値をValue="{StaticResource PhoneAccentBrush}"とブラシを使って指定している。 ただしこのままだと、テンプレート内のすべてのForegroundプロパティにブラシで指定されてします。 個別に指定する場合は、DataTemplateではなくスタイル内にコントロールの定義も含めてしまいます。
<VisualStateGroup x:Name="SelectionStates"> <VisualState x:Name="Unselected"/> <VisualState x:Name="Selected"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="titleText"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="descriptionText"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Opacity" Storyboard.TargetName="placeImage"> <DiscreteObjectKeyFrame KeyTime="0" Value="0.3"/> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid d:DesignHeight="120" d:DesignWidth="420" Width="Auto"> <Rectangle x:Name="backGround" HorizontalAlignment="Left" Stroke="Black" Width="420" Fill="#FFFDC6C6" Opacity="0.345"/> <StackPanel HorizontalAlignment="Left" Margin="0,0,0,20" Width="420" Orientation="Horizontal"> <Image x:Name="placeImage" Width="96" Height="96" Source="/App;component/Assets/Images/Icons/momizi.JPG" Margin="2"/> <StackPanel Width="220"> <TextBlock x:Name="titleText" Height="30" Text="{Binding Text}" HorizontalAlignment="Left" Width="220" /> <TextBlock x:Name="descriptionText" TextWrapping="Wrap" Text="場所の説明、場所の説明、場所の説明、文字数超えたら..." Height="56" Margin="0,12,0,0" FontSize="16"/> </StackPanel> <Image Width="96" Source="/App;component/Assets/Images/Icons/oukan.png" Margin="2" Height="96" /> </StackPanel> </Grid>
ContentContainerだった部分がGridから始まるレイアウトに変更され、
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="descriptionText"> <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/> </ObjectAnimationUsingKeyFrames>
のように個別のコントロールのプロパティを指定している。
この辺のContentContainerをGridから始まるコントロールに置き換える方法がBlendからどうやるのかわからなかったので手書きでXAMLをしている。
ちゃんとBlendだけでできる気はするんだけどねぇ・・・。
Please give us your valuable comment