12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ひとりでAdvent Calendar 2019

Day 1

WPF バインディング

Last updated at Posted at 2019-11-30

##準備

####対象
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クラス

MainWindow.xaml.cs
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

MainWindow.xaml.cs
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クラスを改良

MainWindow.xaml.cs
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に登録してあげる。

MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private MainVM MyVM = new MainVM();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = MyVM;
    }
}

###初期値代入
これでバインディングの準備は完了したがバインディングが成功しているかどうかわかりづらいのでMainVMクラスのカプセル化した変数とリストにそれぞれ初期値を入れてあげる。

MainWindow.xaml.cs
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を作る。

MainWindow.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>

コード側全体

MainWindow.xaml.cs
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; }
    }
}

実行結果
wpf4.PNG

###検索機能
ここから検索機能を追加する。
MainWindowクラスButton_Clickメソッドの部分

MainWindow.xaml.cs
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

MainWindow.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#側のコード

MainWindow.xaml.cs
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; }
    }
}

12
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?