レガシーコードからの脱却

レガシーな職場でも独学でどうにか頑張っていくブログです。

【お勉強記録】WPFデータバインディング3

コレクションのバインディング

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _0721Study
{
    class UserCollenction
    {
        public UserCollenction()
        {
            User user1 = new User();
            user1.Name = "山田太郎";
            user1.Birthday = new DateTime(1990, 7, 21);
            user1.Adress = "東京都中央区";
            this.users.Add(user1);
            User user2 = new User();
            user2.Name = "田中次郎";
            user2.Birthday = new DateTime(1985, 11, 15); ;
            user2.Adress = "福岡県福岡市博多区";
            this.users.Add(user2);
        }

        public List<User> Users
        {
            get { return users; }
        }
        private List<User> users = new List<User>();
    }
}

<Window x:Class="_0721Study.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:_0721Study"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:UserCollenction x:Key="oUserCollenction"/>
    </Window.Resources>
    <StackPanel Margin="10" DataContext="{Binding Source={StaticResource oUserCollenction}}">
        <ListBox ItemsSource="{Binding Path=Users}" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="200" />
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Path=Name}"></TextBlock>
                        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Adress}"></TextBlock>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>                
</Window>

f:id:Gappory:20170723123225p:plain

ListをListBoxにバインドしています。 DataTemplateを使うことで、どのようにバインドしたオブジェクトを表示するかを定義できます。

DataTemplateは以下のようにも書き換えられそう。

<Window x:Class="_0721Study.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:_0721Study"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:UserCollenction x:Key="oUserCollenction"/>
        <DataTemplate x:Key="userTemplate">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Path=Name}"></TextBlock>
                <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Adress}"></TextBlock>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <StackPanel Margin="10" DataContext="{Binding Source={StaticResource oUserCollenction}}">
        <ListBox ItemsSource="{Binding Path=Users}" ItemTemplate="{StaticResource userTemplate}" >
        </ListBox>
    </StackPanel>                
</Window>

DataTemplateをResourcesに定義しておくことで、複数箇所で使いまわしができますね。

INotifyPopertyChanged

ソースからターゲットへの同期をするには、ソースとなるオブジェクトがINotifyPropertyChangedを実装する必要があるとのこと。 どうでもいいですが、何を実装させるかすぐにわかる素晴らしい命名ですね。

INotifyPropertyChangedは「PropertyChanged」イベントを持っていて、バインドソースに変更があった場合にターゲットに通知します。

バインディングの値の同期の方法は4つあり、そのうち「OneWay」と「TwoWay」でソースの変更されるとターゲットが再バインディングされます。

  • OneWay
  • TwoWay
  • OneWayToSource
  • OneTime
using System.ComponentModel;

namespace _0721Study
{
    class Test : INotifyPropertyChanged
    {
        public string Title
        {
            get { return title; }
            set
            {
                title = value;
                NotifyPropertyChange("Title");
            }
        }
        private string title;
      
        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChange(string propertyName)
        {
            if (null != this.PropertyChanged)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
<Window x:Class="_0721Study.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:_0721Study"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:Test x:Key="oTest"
                    Title="Test"/>        
    </Window.Resources>
    <StackPanel Margin="10" DataContext="{StaticResource oTest}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="OneTime"></TextBlock>
            <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Title, Mode=OneTime}"></TextBlock>
            <TextBlock Grid.Row="1" Grid.Column="0" Text="OneWay"></TextBlock>
            <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Title, Mode=OneWay}"></TextBlock>
        </Grid>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition Height="20px"></RowDefinition>
            </Grid.RowDefinitions>
            <TextBox Grid.Row="0" Text="{Binding Path=Title ,UpdateSourceTrigger=PropertyChanged}"></TextBox>            
        </Grid>        
    </StackPanel>                
</Window>

f:id:Gappory:20170723134153p:plain

TextBlockのうちOneTimeの方は起動時にバインドされたままになっていますが、OneWayの方はTextBoxの入力に応じて、値が変わるのが確認できました。

また上の例ではTextBoxにUpdateSourceTrigger属性をPropertyChangedにしているため、テキストを入力するたびにその場で上のTextBlockの値が変わります。 これをLostFocusにするとフォーカスがほかに移動した時点で再バインドされるようになります。

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition></RowDefinition>
                <RowDefinition Height="20px"></RowDefinition>
            </Grid.RowDefinitions>
            <TextBox Grid.Row="0" Text="{Binding Path=Title ,UpdateSourceTrigger=LostFocus}"></TextBox>
            <Button Grid.Row="1" Content="Focus用"></Button>
        </Grid>        

f:id:Gappory:20170723134832p:plain

フォーカスをボタンに移動した時点で再バインドされてます。

f:id:Gappory:20170723134909p:plain

何日かすこしずつコードを書きながら勉強して行くことで、WPFのBindingについておぼろげながら、掴めてきました。

まだまだ奥深いBindingですが、ここからは実際に何かしらのアプリケーションを作りつつ理解を深めていった方がいい気もします。 さて、何を作ろうか… どうせ作るならMVVMで作っていきたいですなー

【お勉強記録】WPFデータバインディング2

コンバーター

データバインド「もと」と「さき」で異なる型になる場合があると思います。 例えば、Textプロパティに対して日付型をバインドした場合、日付型をToString()形で自動変換されたものが表示されます。

f:id:Gappory:20170721215128p:plain

前回の例でいうとDateTime型の「誕生日」はToString()された状態で表示されていたため、日本で一般的な表記ではありませんでした。

型コンバータを使うことで、「さき」と「もと」で相互の型変換を行うことができます。 型コンバータは「IValueConverter」を実装することで使用できます。

「IValueConverter」は、以下の2つのメソッド定義を持っています。

 // 概要:
    //     カスタム ロジックをバインディングに適用する方法を提供します。
    public interface IValueConverter
    {
        // 概要:
        //     値を変換します。
        //
        // パラメーター:
        //   value:
        //     バインディング ソースによって生成された値。
        //
        //   targetType:
        //     バインディング ターゲット プロパティの型。
        //
        //   parameter:
        //     使用するコンバーター パラメーター。
        //
        //   culture:
        //     コンバーターで使用するカルチャ。
        //
        // 戻り値:
        //     変換された値。 メソッドが null を返す場合は、有効な null 値が使用されています。
        object Convert(object value, Type targetType, object parameter, CultureInfo culture);
        //
        // 概要:
        //     値を変換します。
        //
        // パラメーター:
        //   value:
        //     バインディング ターゲットによって生成される値。
        //
        //   targetType:
        //     変換後の型。
        //
        //   parameter:
        //     使用するコンバーター パラメーター。
        //
        //   culture:
        //     コンバーターで使用するカルチャ。
        //
        // 戻り値:
        //     変換された値。 メソッドが null を返す場合は、有効な null 値が使用されています。
        object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
    }

メソッド名を見れば大体想像できますが、Convert()はバインド「もと」から「さき」へ値を渡すときの変換を、ConvertBack()は「さき」から「もと」の変換を実装しているようです。

DateTimeの型コンバータの実装

using System;
using System.Globalization;
using System.Windows.Data;

namespace _0721Study
{
    [ValueConversion(typeof(DateTime), typeof(string))]
    class DateTimeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DateTime date = (DateTime)value;
            return date.ToString(parameter.ToString());
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value.ToString();
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return value;
        }
    }
}

Convert()メソッドは受け取った値をparameterをもとにstringに変換して返しています。 ConvertBack()メソッドはstringをDateTime型に変換して返します。

xaml側を書きます。

<Window x:Class="_0721Study.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:_0721Study"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:User x:Key="oUser"
                       Name="山田 太郎"
                       Birthday="1990/07/21"/>
        <local:DateTimeConverter x:Key="dateTimeConverter" />
    </Window.Resources>
    <WrapPanel Orientation="Horizontal" DataContext="{StaticResource oUser}">
        <Label Content="名前:"  VerticalAlignment="Center" ></Label>
        <TextBlock Text="{Binding Path=Name}" VerticalAlignment="Center"></TextBlock>
        <Label Content="誕生日:"  VerticalAlignment="Center" ></Label>
        <TextBlock Text="{Binding Path=Birthday, Converter={StaticResource dateTimeConverter},ConverterParameter=yyyy/MM/dd}" VerticalAlignment="Center"></TextBlock>
    </WrapPanel>
</Window>

BindingでConverterにlocal:DateTimeConverter のオブジェクトと、ConverterParameterを指定しています。

f:id:Gappory:20170722094059p:plain

実行結果を見ると誕生日がしっかり、日本人になじみのある形に変換されているのが確認できました!