##準備
####対象
C#なんとなく分かってる。駆け出しプログラマー
####目標
簡単なバインディングの流れをつかむ。なので、検索機能のみあるものを作る。
バインディング主軸で説明するので、SQLなどライブラリとかは使いません。
####どのようなアプリにするか?
生徒のテストの成績を管理するアプリ。
####要件
生徒はクラスIdと、出席番号と、男か女と、点数を持つ。
検索、追加、削除を行えるようにする。
####設計
生徒一人の状態を表すStudentクラスを作成する。
WPFで、Xaml側への反映を行うためViewModelクラスの作成を行う。
Studentのフィールドは
変数名 | 型 | 説明 |
---|---|---|
ClassId | int | クラスの番号 |
Id | int | 出席番号 |
Gender | bool | 男か女か |
Score | int | 点数 |
ViewModelのフィールドは
変数名 | 型 | 説明 |
---|---|---|
ScoreList | ObservableCollection<Student> |
生徒全体のリスト |
ResultList | ObservableCollection<Student> |
検索結果のリスト |
ClassId | int | クラスの番号(検索用) |
Id | int | 出席番号(検索用) |
IsManChecked | bool | 男がチェックされているか |
IsWomanChecked | bool | 女がチェックされているか |
Score | int | 点数(検索用) |
####コード作成
Studentクラス
public class Student
{
/// <summary>
/// クラスの番号
/// </summary>
public int ClassId { get; set; }
/// <summary>
/// 出席番号
/// </summary>
public int Id { get; set; }
/// <summary>
/// 男か女か
/// </summary>
public bool Gender { get; set; }
/// <summary>
/// 点数
/// </summary>
public int Score { get; set; }
}
MainWindowのVMクラスMainVM
public class MainVM
{
public ObservableCollection<Student> ScoreList { get; set; } = new ObservableCollection<Student>();
public ObservableCollection<Student> ResultList { get; set; } = new ObservableCollection<Student>();
private int _ClassId;
/// <summary>
/// クラスの番号(検索用)
/// </summary>
public int ClassId
{
get => _ClassId;
set
{
_ClassId = value;
}
}
private int _Id;
/// <summary>
/// 出席番号(検索用)
/// </summary>
public int Id
{
get => _Id;
set
{
_Id = value;
}
}
private bool _IsManChecked;
/// <summary>
/// 男がチェックされているか
/// </summary>
public bool IsManChecked
{
get => _IsManChecked;
set
{
_IsManChecked = value;
}
}
private bool _IsWomanChecked;
/// <summary>
/// 女がチェックされているか
/// </summary>
public bool IsWomanChecked
{
get => _IsWomanChecked;
set
{
_IsWomanChecked = value;
}
}
private int _Score;
/// <summary>
/// 点数(検索用)
/// </summary>
public int Score
{
get => _Score;
set
{
_Score = value;
}
}
}
Studentクラスの方は入れ物としての役割なのでこれでいいだろう。
VMクラスではまだ色々と準備が必要になる。説明の後に記述する。
###説明:なぜカプセル化を行うのか?
VMクラスでなぜprivateのカプセル化を行っているかというと、
バインディングを行うには通知を送る必要がある。VMクラスで値が代入された瞬間に通知を行うとなるとsetterに入れるのがいいが、setterに入れるとなると値の代入はコードで明示的に示してあげないと代入されてくれなくなる。
なので、明示的にClassId = value
のようにしようとするとこれがまた代入とみなされsetterが動いてしまい、無限ループのようになってエラーが起こってしまう。
なのでsetterに代入の処理を書いてあげるときはカプセル化するのがよい。
###説明:なぜバインディングするときにリストを使ってはいけないのか?
上の方にあるObservableCollection
はリストのようなもの。
なぜリストを使わずにこちらを使うのかというと、intやboolであれば代入=状態の変更なので代入のときに変更通知を出してあげれば、状態変更したら変更通知が出されたことになる。
しかし、Listなどの場合Add,Removeなどがあるため代入≠状態の変更なので、代入の時に、変更通知を出してあげればいいというだけでは十分ではない。
ObservableCollection
はAdd,Removeなどしたときに、変更通知を出してくれるクラスである。
###INotifyPropertyChanged実装
次にバインディングを行うためのINotifyPropertyChangedインターフェースをVMクラスに実装する。
MainVMクラスを改良
public class MainVM:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string PropertyName)
{
var e = new PropertyChangedEventArgs(PropertyName);
PropertyChanged?.Invoke(this, e);
}
public ObservableCollection<Student> ScoreList { get; set; } = new ObservableCollection<Student>();
public ObservableCollection<Student> ResultList { get; set; } = new ObservableCollection<Student>();
private int _ClassId;
/// <summary>
/// クラスの番号(検索用)
/// </summary>
public int ClassId
{
get => _ClassId;
set
{
_ClassId = value;
NotifyPropertyChanged(nameof(ClassId));
}
}
private int _Id;
/// <summary>
/// 出席番号(検索用)
/// </summary>
public int Id
{
get => _Id;
set
{
_Id = value;
NotifyPropertyChanged(nameof(Id));
}
}
private bool _IsManChecked;
/// <summary>
/// 男がチェックされているか
/// </summary>
public bool IsManChecked
{
get => _IsManChecked;
set
{
_IsManChecked = value;
NotifyPropertyChanged(nameof(IsManChecked));
}
}
private bool _IsWomanChecked;
/// <summary>
/// 女がチェックされているか
/// </summary>
public bool IsWomanChecked
{
get => _IsWomanChecked;
set
{
_IsWomanChecked = value;
NotifyPropertyChanged(nameof(IsWomanChecked));
}
}
private int _Score;
/// <summary>
/// 点数(検索用)
/// </summary>
public int Score
{
get => _Score;
set
{
_Score = value;
NotifyPropertyChanged(nameof(Score));
}
}
}
InofiryPropertyChangedはただ一つPropertyChangedだけ持っており、それを実装する。
PropertyChanged.Invoke(object, PropertyChangedEventArgs)
でPropertyChangedイベントを実行することができobjectはthisで済ませればよく、PropertyChangedEventArgsはnew PropertyChangedEventArgs(PropertyName)
のように変数名を入れて宣言してあげればよい。
変数名に関して、間違いが起こらないようにnameof演算子で実行する。
そしてメソッド化したものをlist以外の各変数のsetterに定義する。
そしてMainWindowクラスでVMクラスをグローバル変数にして、DataContextに登録してあげる。
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainVM MyVM = new MainVM();
public MainWindow()
{
InitializeComponent();
DataContext = MyVM;
}
}
###初期値代入
これでバインディングの準備は完了したがバインディングが成功しているかどうかわかりづらいのでMainVMクラスのカプセル化した変数とリストにそれぞれ初期値を入れてあげる。
public class MainVM:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string PropertyName)
{
var e = new PropertyChangedEventArgs(PropertyName);
PropertyChanged?.Invoke(this, e);
}
public ObservableCollection<Student> ScoreList { get; set; } = new ObservableCollection<Student>()
{
new Student(){ClassId = 1, Id = 1, Gender = true, Score = 82},
new Student(){ClassId = 1, Id = 2, Gender = false, Score = 89},
new Student(){ClassId = 1, Id = 3, Gender = true, Score = 74},
new Student(){ClassId = 2, Id = 1, Gender = false, Score = 79},
new Student(){ClassId = 2, Id = 2, Gender = true, Score = 94},
new Student(){ClassId = 2, Id = 3, Gender = false, Score = 87},
new Student(){ClassId = 3, Id = 1, Gender = true, Score = 69},
new Student(){ClassId = 3, Id = 2, Gender = false, Score = 75},
new Student(){ClassId = 3, Id = 3, Gender = true, Score = 94}
};
public ObservableCollection<Student> ResultList { get; set; } = new ObservableCollection<Student>();
private int _ClassId = 1;
/// <summary>
/// クラスの番号(検索用)
/// </summary>
public int ClassId
{
get => _ClassId;
set
{
_ClassId = value;
NotifyPropertyChanged(nameof(ClassId));
}
}
private int _Id = 1;
/// <summary>
/// 出席番号(検索用)
/// </summary>
public int Id
{
get => _Id;
set
{
_Id = value;
NotifyPropertyChanged(nameof(Id));
}
}
private bool _IsManChecked = true;
/// <summary>
/// 男がチェックされているか
/// </summary>
public bool IsManChecked
{
get => _IsManChecked;
set
{
_IsManChecked = value;
NotifyPropertyChanged(nameof(IsManChecked));
}
}
private bool _IsWomanChecked = true;
/// <summary>
/// 女がチェックされているか
/// </summary>
public bool IsWomanChecked
{
get => _IsWomanChecked;
set
{
_IsWomanChecked = value;
NotifyPropertyChanged(nameof(IsWomanChecked));
}
}
private int _Score = 0;
/// <summary>
/// 点数(検索用)
/// </summary>
public int Score
{
get => _Score;
set
{
_Score = value;
NotifyPropertyChanged(nameof(Score));
}
}
}
これで初期値が入ったので、画面側のxamlを作る。
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp3"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal">
<TextBlock Text="クラス"></TextBlock>
<TextBox Text="{Binding ClassId}"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="出席番号"></TextBlock>
<TextBox Text="{Binding Id}"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox Content="男" IsChecked="{Binding IsManChecked}"></CheckBox>
<CheckBox Content="女" IsChecked="{Binding IsWomanChecked}"></CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="点数"></TextBlock>
<TextBox Text="{Binding Score}"></TextBox>
</StackPanel>
<Button Content="検索" Click="Button_Click"></Button>
</StackPanel>
<StackPanel Orientation="Vertical" HorizontalAlignment="Right">
<DataGrid ItemsSource="{Binding ScoreList}"></DataGrid>
<DataGrid ItemsSource="{Binding ResultList}"></DataGrid>
</StackPanel>
</Grid>
</Window>
コード側全体
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace WpfApp3
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainVM MyVM = new MainVM();
public MainWindow()
{
InitializeComponent();
DataContext = MyVM;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//まだ未実装
}
}
public class MainVM:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string PropertyName)
{
var e = new PropertyChangedEventArgs(PropertyName);
PropertyChanged?.Invoke(this, e);
}
public ObservableCollection<Student> ScoreList { get; set; } = new ObservableCollection<Student>()
{
new Student(){ClassId = 1, Id = 1, Gender = true, Score = 82},
new Student(){ClassId = 1, Id = 2, Gender = false, Score = 89},
new Student(){ClassId = 1, Id = 3, Gender = true, Score = 74},
new Student(){ClassId = 2, Id = 1, Gender = false, Score = 79},
new Student(){ClassId = 2, Id = 2, Gender = true, Score = 94},
new Student(){ClassId = 2, Id = 3, Gender = false, Score = 87},
new Student(){ClassId = 3, Id = 1, Gender = true, Score = 69},
new Student(){ClassId = 3, Id = 2, Gender = false, Score = 75},
new Student(){ClassId = 3, Id = 3, Gender = true, Score = 94}
};
public ObservableCollection<Student> ResultList { get; set; } = new ObservableCollection<Student>();
private int _ClassId = 1;
/// <summary>
/// クラスの番号(検索用)
/// </summary>
public int ClassId
{
get => _ClassId;
set
{
_ClassId = value;
NotifyPropertyChanged(nameof(ClassId));
}
}
private int _Id = 1;
/// <summary>
/// 出席番号(検索用)
/// </summary>
public int Id
{
get => _Id;
set
{
_Id = value;
NotifyPropertyChanged(nameof(Id));
}
}
private bool _IsManChecked = true;
/// <summary>
/// 男がチェックされているか
/// </summary>
public bool IsManChecked
{
get => _IsManChecked;
set
{
_IsManChecked = value;
NotifyPropertyChanged(nameof(IsManChecked));
}
}
private bool _IsWomanChecked = true;
/// <summary>
/// 女がチェックされているか
/// </summary>
public bool IsWomanChecked
{
get => _IsWomanChecked;
set
{
_IsWomanChecked = value;
NotifyPropertyChanged(nameof(IsWomanChecked));
}
}
private int _Score = 0;
/// <summary>
/// 点数(検索用)
/// </summary>
public int Score
{
get => _Score;
set
{
_Score = value;
NotifyPropertyChanged(nameof(Score));
}
}
}
public class Student
{
/// <summary>
/// クラスの番号
/// </summary>
public int ClassId { get; set; }
/// <summary>
/// 出席番号
/// </summary>
public int Id { get; set; }
/// <summary>
/// 男か女か
/// </summary>
public bool Gender { get; set; }
/// <summary>
/// 点数
/// </summary>
public int Score { get; set; }
}
}
###検索機能
ここから検索機能を追加する。
MainWindowクラスButton_Clickメソッドの部分
private void Button_Click(object sender, RoutedEventArgs e)
{
var linq = new ObservableCollection<Student>(MyVM.ScoreList).AsEnumerable();
if(MyVM.ClassId != 0)
{
linq = linq.Where(x => x.ClassId == MyVM.ClassId);
}
if(MyVM.Id != 0)
{
linq = linq.Where(x => x.Id == MyVM.Id);
}
//三項演算子,x.GenderがtrueであればIsMancheckedをみて、falseであればIsWomanCheckを見る。
linq = linq.Where(x => x.Gender?MyVM.IsManChecked:MyVM.IsWomanChecked);
if(MyVM.Score >= 0)
{
linq = linq.Where(x => x.Score >= MyVM.Score);
}
MyVM.ResultList = new ObservableCollection<Student>(linq);
MyVM.NotifyPropertyChanged(nameof(MyVM.ResultList));
}
検索の大体はLinqでやっているので特にいうこともないが、最後のMyVM.NotifyPropertyChangedについて
ObservableCollection型の時はAdd,Removeは変更通知を出してくれるが、代入の時は変更通知を出してくれないので自分でやる必要がある。
ただし、intやboolと同じようにprivateにしてカプセル化を行うとAdd,Removeなどをした場合に状態が想定したものと一致しなくなるのでカプセル化をやるのは難しそうではある。
そこで、MainWindowクラスで直接呼び出して変更通知を行い反映させてやる。
(きちんとやればVMクラスで完結するはず。)
###全コード
xaml
<Window x:Class="WpfApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp3"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal">
<TextBlock Text="クラス"></TextBlock>
<TextBox Text="{Binding ClassId}"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="出席番号"></TextBlock>
<TextBox Text="{Binding Id}"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox Content="男" IsChecked="{Binding IsManChecked}"></CheckBox>
<CheckBox Content="女" IsChecked="{Binding IsWomanChecked}"></CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="点数"></TextBlock>
<TextBox Text="{Binding Score}"></TextBox>
</StackPanel>
<Button Content="検索" Click="Button_Click"></Button>
</StackPanel>
<StackPanel Orientation="Vertical" HorizontalAlignment="Right">
<DataGrid ItemsSource="{Binding ScoreList}"></DataGrid>
<DataGrid ItemsSource="{Binding ResultList}"></DataGrid>
</StackPanel>
</Grid>
</Window>
C#側のコード
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace WpfApp3
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private MainVM MyVM = new MainVM();
public MainWindow()
{
InitializeComponent();
DataContext = MyVM;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var linq = new ObservableCollection<Student>(MyVM.ScoreList).AsEnumerable();
if(MyVM.ClassId != 0)
{
linq = linq.Where(x => x.ClassId == MyVM.ClassId);
}
if(MyVM.Id != 0)
{
linq = linq.Where(x => x.Id == MyVM.Id);
}
//三項演算子,x.GenderがtrueであればIsMancheckedをみて、falseであればIsWomanCheckを見る。
linq = linq.Where(x => x.Gender?MyVM.IsManChecked:MyVM.IsWomanChecked);
if(MyVM.Score >= 0)
{
linq = linq.Where(x => x.Score >= MyVM.Score);
}
MyVM.ResultList = new ObservableCollection<Student>(linq);
MyVM.NotifyPropertyChanged(nameof(MyVM.ResultList));
}
}
public class MainVM:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string PropertyName)
{
var e = new PropertyChangedEventArgs(PropertyName);
PropertyChanged?.Invoke(this, e);
}
public ObservableCollection<Student> ScoreList { get; set; } = new ObservableCollection<Student>()
{
new Student(){ClassId = 1, Id = 1, Gender = true, Score = 82},
new Student(){ClassId = 1, Id = 2, Gender = false, Score = 89},
new Student(){ClassId = 1, Id = 3, Gender = true, Score = 74},
new Student(){ClassId = 2, Id = 1, Gender = false, Score = 79},
new Student(){ClassId = 2, Id = 2, Gender = true, Score = 94},
new Student(){ClassId = 2, Id = 3, Gender = false, Score = 87},
new Student(){ClassId = 3, Id = 1, Gender = true, Score = 69},
new Student(){ClassId = 3, Id = 2, Gender = false, Score = 75},
new Student(){ClassId = 3, Id = 3, Gender = true, Score = 94}
};
public ObservableCollection<Student> ResultList { get; set; } = new ObservableCollection<Student>();
private int _ClassId = 0;
/// <summary>
/// クラスの番号(検索用)
/// </summary>
public int ClassId
{
get => _ClassId;
set
{
_ClassId = value;
NotifyPropertyChanged(nameof(ClassId));
}
}
private int _Id = 0;
/// <summary>
/// 出席番号(検索用)
/// </summary>
public int Id
{
get => _Id;
set
{
_Id = value;
NotifyPropertyChanged(nameof(Id));
}
}
private bool _IsManChecked = true;
/// <summary>
/// 男がチェックされているか
/// </summary>
public bool IsManChecked
{
get => _IsManChecked;
set
{
_IsManChecked = value;
NotifyPropertyChanged(nameof(IsManChecked));
}
}
private bool _IsWomanChecked = true;
/// <summary>
/// 女がチェックされているか
/// </summary>
public bool IsWomanChecked
{
get => _IsWomanChecked;
set
{
_IsWomanChecked = value;
NotifyPropertyChanged(nameof(IsWomanChecked));
}
}
private int _Score = 0;
/// <summary>
/// 点数(検索用)
/// </summary>
public int Score
{
get => _Score;
set
{
_Score = value;
NotifyPropertyChanged(nameof(Score));
}
}
}
public class Student
{
/// <summary>
/// クラスの番号
/// </summary>
public int ClassId { get; set; }
/// <summary>
/// 出席番号
/// </summary>
public int Id { get; set; }
/// <summary>
/// 男か女か
/// </summary>
public bool Gender { get; set; }
/// <summary>
/// 点数
/// </summary>
public int Score { get; set; }
}
}