業務のためのC#・C言語・C++学習

主にC#の文法やWPF周りのアウトプットに利用してます。

【C#】第一回アルゴリズム検定実技検定 過去問解答例

C#により記述された第一回アルゴリズム検定実技検定の分析と解答例

A 2倍チェック

問題

文字列⇔数値の変換をする問題である。

https://atcoder.jp/contests/past201912-open/tasks/past201912_a

解法

2つの解法がある。
・変換に失敗した場合にエラーを返すParseメソッド
・変換に失敗した場合にfalse値を返すTryParseメソッド

変換に失敗した場合にエラーを返すParseメソッド

namespace A_IsItNumber
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Type 3 characters");
            string input = Console.ReadLine();

            try
            {
                var a= int.Parse(input);
                Console.WriteLine(a * 2);
            }
            catch
            {
                Console.WriteLine("error");
            }
            
        }
    }
}

変換に失敗した場合にfalse値を返すTryParseメソッド

namespace A_IsItNumber
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Type 3 characters");
            string input = Console.ReadLine();

            int tryparsedInt;
            if (int.TryParse(input, out tryparsedInt))
            {
                Console.WriteLine(tryparsedInt * 2);
            }
            else { Console.WriteLine("error"); }
            
        }
    }
}

B 増減管理

問題

整数値の差分をとる問題である。

https://atcoder.jp/contests/past201912-open/tasks/past201912_b

解法

・数値の比較後、比較した結果を配列に格納し出力

            int N = int.Parse(Console.ReadLine());
            string[] output = new string[N - 1];

            int An0 = int.Parse(Console.ReadLine());
            for (int i = 0; i < N - 1; i++)
            {
                int An1 = int.Parse(Console.ReadLine());

                if (An1 == An0) { output[i] = "stay"; }
                else if (An1 > An0) { output[i] = $"up {An1 - An0}"; }
                else { output[i] = $"down {An0 - An1}"; }
                An0 = An1;
            }

            //出力
            for (int i = 0; i < N - 1; i++)
            {
                Console.WriteLine(output[i]);
            }

C 3番目

問題

異なる整数を順番に並べ3番目に大きい値を表示する

https://atcoder.jp/contests/past201912-open/tasks/past201912_c?lang=ja

解法

バブルソートアルゴリズムを構築

バブルソートアルゴリズムを構築

            var inputs = Console.ReadLine().Split();

            for (int i = 0; i < inputs.Length; i++)
            {
                for (int j = i + 1; j < inputs.Length; j++)
                {
                    if (int.Parse(inputs[i]) > int.Parse(inputs[j]))
                    {
                        var temp = inputs[j];
                        inputs[j] = inputs[i];
                        inputs[i] = temp;
                    }
                }
            }

            Console.WriteLine(inputs[2]);

D 重複検査

問題

整数列の中に重複があるか検査せよ

https://atcoder.jp/contests/past201912-open/tasks/past201912_d

解法

・各整数列の値の個数を調べる。重複がなければ個数はそれぞれ1である

namespace D_Duplicated
{
    class Program
    {
        static void Main(string[] args)
        {
            int N = int.Parse(Console.ReadLine());

            var A_i = new int[N];
            for (int i = 0; i < N; i++)
            {
                A_i[i] = (int.Parse(Console.ReadLine()));
            }
            //書き換えがなければある整数は1つしかないはずなので個数を数える
            var count = new int[N];
            for (int i = 0; i < N; i++)
            {
                count[A_i[i] - 1]++;
            }
            //個数から重複を探す
            int duplicatedNum = 0;
            int missingNum = 0;
            for (int i = 0; i < N; i++)
            {
                if (count[i] == 0)
                {
                    missingNum = i + 1;
                }
                else if (count[i] == 2)
                {
                    duplicatedNum = i + 1;
                }
            }
            //出力
            if (duplicatedNum == 0)
            {
                Console.WriteLine("Correct");
            }
            else {
                Console.WriteLine($"Duplicated Number is {duplicatedNum}, Missing Number is {missingNum}");
            }
        }
    }
}

【C#】csprojファイルとは

csprojファイルとは、C#ソースコードをビルドするために必要なファイルで、C#専用のビルド作業の情報が記載されたプロジェクトファイルのことである。

ビルド作業

ビルド作業とは、プログラムをPCのCPUが処理できる形にする作業のことである。1

一般的に総合開発環境であるvisual studioでアプリケーションの開発をする際、特に原理を意識することなくビルド作業をしていると思います。
一方、visual studioを使わず手動で一連のビルド作業をする場合はコンパイルアセンブラといった処理を専用ツールの利用で実現させます。

C#コンパイラとビルドツールMSBuild

C#コンパイラcscは以下のディレクトリにいくつかのVersionがある。
32ビット版
C:\Windows\Microsoft.NET\Framework
64ビット版
C:\Windows\Microsoft.NET\Framework64
OSが64ビットならcscの32ビット版と64ビット版はどちらも利用可能である。

ここでは例として32ビット版のversion3.5のcsc.exeプロパティを示す。

ビルドツールMSBuildは、csc.exeと同様のディレクトリにある。
ここでは例として64ビット版のversion4.0のディレクトリを示す。
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild

csprojファイルを利用した手動ビルド

コマンドプロンプト上でcsprojプロジェクトファイルをMSBuildを用いビルドすればcsprojで指定した箇所にアプリケーションのexeファイルが生成される。


  1. 組込みエエンジニアの教科書

【C#】ディレクトリパスを指定しフォルダ名をreturnするメソッド

自分も含めてですが、何かしらの文字列を入れた配列やコレクションをreturnするメソッドを書く場合、とりあえず処理した各要素を保存するインスタンス変数を作りがちです。 例えばこんな感じに。
DirectoryInfoクラスを利用しディレクトリの情報からファイル名を取得します。
*エラー処理は省略

    public List<string> ReturnData()
        {       
            string path = @"";
            DirectoryInfo diInfo = new DirectoryInfo(path);
            DirectoryInfo[] diInfomations = diInfo.GetDirectories();

   List<string> data = new List<string>();
            foreach (var directions in diInfomations)
            {
                data.Add(directions.Name);
            }
            return data;
        }

DirectoryInfoクラスのオブジェクトを作りディレクション情報をDirectoryInfo[]型として取得します。

            string path = @"";
            DirectoryInfo diInfo = new DirectoryInfo(path);
            DirectoryInfo[] diInfomations = diInfo.GetDirectories();

そして、foreachで取得した複数のファイル名をreturnする際に、一時的にファイル名を保存するList型の変数を使用しています。
しかし、無駄なインスタンス変数を書くコーディングは行が多く見にくかったり、無駄にメモリを使ってしまうことから推奨はされません。

より見やすく効率が良いコードにするには、returnにListではなく、IEnumerableインスタンス変数の型として利用します。IEnumerableを利用すればLINQが利用できるのでよりスッキリとしたコードになります。

public IEnumerable<string> ReturnData1()
        {
            string path = @"";
            DirectoryInfo diInfo = new DirectoryInfo(path);
            IEnumerable<DirectoryInfo> diInfomations = diInfo.EnumerateDirectories();
            IEnumerable<string> names = diInfomations.Select(s => s.Name);
            return names;
        }

DirectoryInfoクラスを宣言するところまでは同じです。
違いはIEnumerable型のディレクション情報を得るためにEnumerateDirectories()メソッドを使用しています。 その後、ディレクション情報からLINQのSelectメソッドを用いファイル名を取得します。 LINQを使用したため中間変数のインスタンス変数を宣言しなくてすんでいます。

また、上記のコードは説明の都合上Selectメソッドの部分を複数行に分けましたが、普通は1行ですませます。

        public IEnumerable<string> ReturnData2()
        {
            string path = @"";
            DirectoryInfo diInfo = new DirectoryInfo(path);
            IEnumerable<string> names = diInfo.EnumerateDirectories().Select(s => s.Name);
            return names;
        }

【C#-WPF】WindowsフォームやWPFといったGUIにおけるTaskを用いた非同期処理の問題点とその解決方法

本記事ではC#における非同期処理、WindowsフォームやWPFといったGUIでTaskを用いた非同期処理、それに直面する問題と解決方法について説明する

非同期処理とは

スレッドとは、プログラムを実行する最小単位の処理のことである。
複数のプログラムを並列処理する場合に意識する技術になる。よくあるのは、GUIアプリでフリーズに見えないように重い処理は別スレッドで処理しアプリ利用者はメインスレッドでUIの操作を行えるようにするがある。

C#では複数のスレッドで並列処理する仕組みをマルチスレッドと呼ぶ。WPFなどのGUIでは、メインスレッド=UIスレッドとワーカスレッドに分類される。メインスレッドでUIコントロールの操作を行い、ワーカスレッドではメインスレッドでの負担を軽くするため重い処理を行う。

マルチスレッドを利用するとき処理の仕方は 「非同期処理」、「同期処理と非同期処理の併用」の2つがある。

非同期処理は並列処理で利用される。処理の終了に責任を持たない仕組みのことで、メインスレッドとワーカースレッドの処理はお互い無関係に動作する。

非同期処理

同期処理は逐次的に処理を一つ一つ実行していくことを単一スレッド内で行う。したがってそれぞれ処理の終了を待機する仕組みと言っても良い。

同期処理

ワーカスレッドに依頼した処理の結果を利用したい場合などに「同期処理と非同期処理の併用」をする。 よく非同期処理とマルチスレッドと並列処理の違いは何ですかとあるが、非同期処理はマルチスレッドで並列処理をするが、処理の終了は認知しないこと。マルチスレッドは並列処理をすることが前提だが、非同期処理だけをする場合や非同期処理と同期処理の併用をする場合がある。並列処理はマルチスレッドを使うこと同義である。

非同期処理と書かれていたら並列処理するんだな、マルチスレッドを使うんだなと考えればよい。

しかし、マルチスレッドと書かれていても非同期処理だけで完結するかは分からない。ほとんどの場合は同期処理を導入しないといけないが、マルチスレッドから見たら非同期なのか同期処理するのかは開発者による。

非同期処理の実現

昔は非同期処理を実現するのに以下の方法を用いていた
・Thread
・ThreadPool
Delegate(InvokeやBegingInvoke)
・Timer

https://atmarkit.itmedia.co.jp/ait/articles/0504/20/news111.html

現在はタスクTaskを用いるのが一般的である。

また、先ほどの図では省略したが、Delegate(InvokeやBegingInvoke)、タスク、await/asyncではスレッドプールの機能を内部的に利用している。スレッドプールとはオーバヘッド削減のため一度作成したスレッドを保管し再利用する仕組みである。

Task

タスクを用いた非同期処理について説明する。

Task task = new Task(デリゲート);
 task.Start();

Start()で、メインスレッドとは別にワーカースレッドでデリゲートが実行される。
またデリゲートの代わりにラムダ式を使う場合が多い。

Task task = new Task(() =>
{
//処理内容を記述
});
task.Start();

Start()の代わりにRun()を使う方法もある

Task task = Task.Run(デリゲート);

ラムダ式を使うと

Task task = Task.Run(()=>
{

});

Start()を使った場合どんな処理順番になるのか。

例1

            Task task = new Task(() =>
            {
                Console.WriteLine("処理A");
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理B");
            });
            task.Start();
            Console.WriteLine("処理C");
            Console.WriteLine("処理D");

処理の結果は処理C→D→A→Bとなる。

例2

            Task task = new Task(() =>
            {
                Console.WriteLine("処理A");
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理B");
            });
            task.Start();
            Console.WriteLine("処理C");
            System.Threading.Thread.Sleep(5000);
            Console.WriteLine("処理D");

処理の結果は処理C→A→D→Bとなる。

例3

            Task task = new Task(() =>
            {
                Console.WriteLine("処理A");
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理B");
            });
            task.Start();
            Console.WriteLine("処理C");
            System.Threading.Thread.Sleep(6000);
            Console.WriteLine("処理D");

処理の結果は処理C→A→B→Dとなる。

例4

            Task task = new Task(() =>
            {
                Console.WriteLine("処理A");
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理B");
            });
            Console.WriteLine("処理C");
            task.Start();
            Console.WriteLine("処理D");

処理の結果は処理C→D→A→Bとなる。

以上のことから非同期処理では、処理時間が分からないと開発者がその順番を簡単に制御できてない。処理時間は普通は分からないで処理を待って処理結果を利用したい場合には困ることが分かる

処理順番の制御をするには?

1つはwaitを使う。

            Task task = new Task(() =>
            {
                Console.WriteLine("処理A");
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理B");
            });
            task.Start();
            Console.WriteLine("処理C");
            task.Wait();
            Console.WriteLine("処理D");

処理の結果は処理C→A→B→Dとなる。処理Dはタスク完了後に実施される。

タスクの戻り値を非同期で利用するには

継続タスクContinueWithを利用する。

        static void Main(string[] args)
        {
            Task<string> taskA = new Task<string>(()=>
             {
                 //処理A
                 var resultA = "処理A";
                 Console.WriteLine(resultA);
                 return resultA;
             });
            //継続タスク
            Task<string> taskB = taskA.ContinueWith<string>(t =>
            {
                //処理B
                var resultB = "処理B:" + t.Result;
                Console.WriteLine(resultB);
                return resultB;
            });
            Task taskC = taskB.ContinueWith(t =>
            {
                //処理C
                Console.WriteLine("処理C:" + t.Result);
            });
            taskA.Start();
            taskC.Wait();
            Console.WriteLine("完了");
        }

出力結果
処理A
処理B:処理A
処理C:処理B:処理A
完了

GUIにおけるTaskによる非同期処理の問題点

今まで述べた通り非同期処理でタスクを使うと処理結果の利用が簡単には出来ない、waitやcountinuewithを使えなくもないが昨今はデットロックの問題もあり使用は推奨されない

GUIアプリケーションでTaskを使う問題点が2つある。 1つは、処理完了後にUI変更をしたいが非同期処理をしているので処理結果が利用できないのである。

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = "処理中";

            Task.Run(() =>
            {
                System.Threading.Thread.Sleep(5000);
                Console.WriteLine("処理完了");
            });

            textBox1.Text = "処理完了";
        }

本来は処理が終了した後に処理完了の表示をしたいが、処理が完了する前に表示されてしまっている。

もう1つの問題点は、処理結果をUIに反映させようとし、ワーカースレッドからUIにアクセスするがエラーが出力されてしまうことだ。

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = "処理中";

            Task.Run(() =>
            {
                System.Threading.Thread.Sleep(5000);
                textBox1.Text = "処理完了";
            });
            
        }

以下の通りエラーが出力される。

なぜエラーが出力されるのか?
それはWindowsフォームやWPFにおけるコントロール(UI)は、コントロールが作成されたメインスレッド=UIスレッドからのみ呼び出すことが許されており、ワーカースレッドでコントロールを呼び出すとエラーが起きるからである。
マイクロソフトさんのサイトに以下のように言及されてる。
Most methods on a control can only be called from the thread where the control was created.」
learn.microsoft.com

以上、WindowsフォームやWPFといったGUIにおけるTaskによる非同期処理の問題点をあげた。次にこれら問題点を解決するawait/asyncとInvokeを説明する。

解決方法

await/async

継続タスクを簡潔に記述する機能がawait/asyncである。
await以降の処理をawait内の処理が全て完了したら実行する順になる。
asyncはawaitを利用するために決まり文句である。

        private async void button2_Click(object sender, EventArgs e)
        {
            textBox1.Text = "処理中";

            await Task.Run(() =>
            {
                System.Threading.Thread.Sleep(5000);            
            });

            textBox1.Text = "処理完了";
        }

Invokeを使ったUIコントロールの操作

ワーカースレッドからUI操作をするにはInvokeを用いる。

windowsフォームの場合
          private void button3_Click(object sender, EventArgs e)
        {
            textBox1.Text = "処理中";

            Task.Run(() =>
            {
                System.Threading.Thread.Sleep(5000);
                this.Invoke(new Action(() =>
                {
                    textBox1.Text = "処理完了";
                }));
            });           
        }         
WPFの場合
        private void button_Click(object sender, RoutedEventArgs e)
        {
            textBox1.Text = "処理中";

            Task.Run(() =>
            {
                System.Threading.Thread.Sleep(5000);
                this.Dispatcher.Invoke(new Action(() =>
                {
                    textBox1.Text = "処理完了";
                }));

            });
        }

処理の概略をスレッド毎に示す。

System.WindowsのApplication.Current.Dispatcher

System.Windows.ThreadingのDispatcherクラスでUIコントロールの操作を行ったが、もう一つSystem.WindowsのApplication.Current.Dispatcherを利用する方法がある。これら違いを調べても出てこないのでここではWPFで以下の動作検証をすることにとどめる。

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            ListBox.Items.Add("処理1:UIスレッドで変更");
            Task t1 = Task.Run(() =>
            {
                System.Threading.Thread.Sleep(100);//想定する重い処理2
                this.Dispatcher.Invoke(() =>
                {
                    ListBox.Items.Add("処理3:ワーカースレッドからUIスレッドに変更要求");
                });
                Application.Current.Dispatcher.Invoke(() =>
                {
                    ListBox.Items.Add("処理4:ワーカースレッドからUIスレッドに変更要求");
                });
            });

            ListBox.Items.Add("処理2:UIスレッドで変更");
        }

処理順番は処理1→処理2→処理3→処理4となる。特に違いがあるようにはみえない

非同期処理にawait/asyncをつけて継続タスクの機能を追加するとどうなるか

awaitは、awaitをつけた非同期処理内からreturnされる値をawait以降のコードで利用するときに便利であるが、ここでは特にreturnは記述しない。ただし、await以降にある処理2が継続タスクとして処理される。

        private async void Button_Click_2(object sender, RoutedEventArgs e)
        {
            ListBox.Items.Add("処理1:UIスレッドで変更");
            await Task.Run(() =>
            {
                System.Threading.Thread.Sleep(1000);//想定する重い処理
                this.Dispatcher.Invoke(() =>
                {
                    ListBox.Items.Add("処理3:ワーカースレッドからUIスレッドに変更要求");
                });
                Application.Current.Dispatcher.Invoke(() =>
                {
                    ListBox.Items.Add("処理4:ワーカースレッドからUIスレッドに変更要求");
                });
            });

            ListBox.Items.Add("処理2:UIスレッドで変更");
        }

処理順番は処理1→処理3→処理4→処理2となる。await/asyncを利用すると継続タスク扱いになり処理の順番が変更されるので注意してください。

【C#】delegateの基礎とActionを用いた応用

delegate「メソッドの引数にメソッドを代入する仕組み」です。そのメリットは複数あるメソッドの共通部分を抜き出し異なる部分だけメソッドとして定義すれば効率良くコードが書けます。今となってはdelegate→匿名メソッド→ラムダ式と進化し、ラムダ式がを良く使いますが、メソッドの非同期処理でdelegateが利用されている場合があるので学習するメリットはあります。

delegateそのものに2つのメソッドを代入する

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

namespace Delegate0
{
    delegate void Goal(string str);//宣言
    class Program
    {
        static void Run(string s)
        {
            Console.WriteLine($"現在ゴールまで{s}kmです");
        }
        static void Energy(string s)
        {
            Console.WriteLine($"残りの体力は{s}%です");
        }
        static void Main(string[] args)
        {
            //Goal g = new Goal(Run);//Delegateのインスタンス化
            Goal g = Run;//省略形
            g("10");
            Goal e = Energy;
            e("50");
        }
    }
}

出力結果

delegateの使い方は「delegateの宣言→delegateインスタンス化→delegateインスタンスにメソッド代入」です。
delegateの宣言

delegate 戻り値の型 デリゲート名(引数の型 引数、 ….)
delegate void Goal(string str)でdelegateの宣言。

delegateインスタンス
RunメソッドをdelegateのGoalに代入しつつ、インスタンス化を行う。

Goal g = new Goal(Run);

new演算子の省略形としてメソッドを直接渡してもよい。

Goal g = Run;//省略形

生成したインスタンスに値を代入して出力。
ただし、これだけではdelegateを使わずRunとEnergyメソッドをそのまま利用すればいいのではないかということになりdelegateの良さがまだ見えてきません。

delegateを経由し共通メソッドに2つのメソッドを代入する

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

namespace Delegate1
{
    delegate void DelegateName(string str);

    class Program
    {
        static void WhereToGo(string place)
        {
            Console.WriteLine($"Go to {place}");
        }

        static void HowToGo(string vehicle)
        {
            Console.WriteLine($"Go by {vehicle}");
        }
        void MethodCommon(string[] data, DelegateName output)
        {
            foreach (var value in data)
            {
                output(value);
            }
        }
        static void Main(string[] args)
        {
            var dm = new Program();

            var data = new[] { "Tokyo", "Kyoto" };
            DelegateName delegatename = WhereToGo;
            dm.MethodCommon(data, delegatename);

            var data1 = new[] { "Train", "Car" };
            delegatename = HowToGo;
            dm.MethodCommon(data1, delegatename);
        }
    }
}

出力結果

delegateの使い方を、「delegateの宣言→delegateインスタンス化→delegateインスタンスにメソッド代入」から「delegateの宣言→delegateインスタンス化→delegateインスタンスにメソッド代入→共通メソッドにdelegateインスタンスを代入」にUpdateしています。
共通メソッドMethodCommonにデリゲートDelegateNameを引数としメソッドWhereToGoとHowToGoを代入してます。メソッドが代入されたdelegatenameをdm.MethodCommonの引数に代入すると、「WhereToGoとHowToGoそれぞれのメソッド」と「共通メソッドMethodCommon」の実行内容がそれぞれ実施される。代入するメソッドがさらに増えていけばdelegateを利用する価値があると言えます。

Actionを用いたdelegateの応用その1

先ほど説明したdelegateの宣言→delegateインスタンス化→delegateインスタンスにメソッド代入→共通メソッドにdelegateインスタンスを代入」は少し手間が多くないかい?ということで「delegateの宣言」と「delegateインスタンスにメソッド代入」を削除できるActionを紹介します。

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

namespace anonymousmethod
{
    //delegate void DelegateName(string str);

    class Program
    {
        static void WhereToGo(string place)
        {
            Console.WriteLine($"Go to {place}");
        }

        static void HowToGo(string vehicle)
        {
            Console.WriteLine($"Go by {vehicle}");
        }
        void MethodCommon(string[] data, Action<string> output)
        {
            foreach (var value in data)
            {
                output(value);
            }
        }
        static void Main(string[] args)
        {
            var dm = new Program();

            var data = new[] { "Tokyo", "Kyoto" };  
            //DelegateName delegatename = WhereToGo;
            dm.MethodCommon(data, WhereToGo);

            var data1 = new[] { "Train", "Car" };
            //delegatename = HowToGo;
            dm.MethodCommon(data1, HowToGo);

        }
    }
}

前回は共通メソッドの引数の型に宣言したdelegateの変数を使っていました。ここでは代わりにActionの型を利用してます。 delegateをわざわざ宣言しなくてもActionを導入することで共通メソッドにメソッドを代入できるようになっています。
Actionはデリゲートとして利用する「値を返さないメソッドの型」だと思ってください。

Actionを用いたdelegateの応用その2

共通メソッドがない場合のAction実用例を示します。

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

namespace Action0
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> DelegateName = Method1;
            DelegateName += Mehhod2;

            DelegateName("こんにちは");
        }

        static void Method1(string value)
        {
            Console.WriteLine($"Method1 argument is {value}");
        }

        static void Mehhod2(string value)
        {
            Console.WriteLine($"Method2 argument is {value}");
        }
    }
}

出力結果

ここでのdelegateの使用の流れは「Actionを利用したdelegateインスタンス化→delegateインスタンスにメソッド代入」とたいぶ簡潔しています。メソッドの引数の型が同じで、戻り値がないメソッドが複数ある場合、Actionデリゲートを使えばすっきりとしたコーディングができると言えるでしょう。

【C#-WPF】MVVMパターンとContentControlで画面遷移する方法

WPFで画面遷移しようとしてWEB検索をかけるとPageとFrameを使った画面遷移が上位に表示されます。これらは開発者が実現したい機能がない場合があるため、MVVMパターンとContentControlの利用を推奨します。

コンストラクタで初期画面を反映させる場合

大まかな流れ

AView.xamlをMainWindow.xaml上に表示します。

プロジェクトのファイル構成

大まかな流れ
①表示したいUIをUserControlとして作成
②MainWindow.xamlにResources、DataTemplate、ContentControlを設定 ③ContentControlで設定した変数ActiveContentに表示したいUIのViewModelを設定

①表示したいUIをUserControlとして作成

AView.xaml
AViewまず作成する。
WindowではなくUserControlを追加する。Windowだとうまくいかない。UserControlを以下のように選んでください。
中身の実装は、ここではTextboxを表示するようにしています。

<UserControl x:Class="ScreenTransition1.Views.AView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ScreenTransition1.Views"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <DockPanel>
        <DockPanel DockPanel.Dock="Bottom"/>
        <DockPanel DockPanel.Dock="Top">
            <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="10">
                <TextBox Text="AAAAAAAAAAAAAA"/>
            </StackPanel>
        </DockPanel>
        <Grid>
            <TextBox Text="AAAAAAAAAA"/>
        </Grid>
    </DockPanel>
</UserControl>

AViewModel.cs
AViewの ViewModelを作成する。
後の利便性を考慮しMainContentを継承

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

namespace ScreenTransition1.ViewModels
{
    public class AViewModel:MainContent
    {
    }
}

MainContent
INotifyPropertyChangedを継承したクラスである。

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

namespace ScreenTransition1.ViewModels
{
    public class MainContent : INotifyPropertyChanged
    {
        //UIに自動更新を行うためのイベント
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

②MainWindow.xamlにResources、DataTemplate、ContentControlを設定

MainWindow.xaml
DataTemplateとContentControlを用いMainWindow.xamlとAView.xamlを関連付ける。

<Window x:Class="ScreenTransition1.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:ScreenTransition1"
        xmlns:vm="clr-namespace:ScreenTransition1.ViewModels"
        xmlns:view="clr-namespace:ScreenTransition1.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800">
    <DockPanel>
        <DockPanel DockPanel.Dock="Top"/>
        <DockPanel DockPanel.Dock="Bottom"/>
        <DockPanel Margin="5">
            <GroupBox Padding="5">
                <GroupBox.Header>
                    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                        <ComboBox Width="300"/>
                    </StackPanel>
                </GroupBox.Header>
                <Grid>
                    <Grid.Resources>
                        <DataTemplate DataType="{x:Type vm:AViewModel}">
                            <view:AView/>
                        </DataTemplate>
                    </Grid.Resources>
                    <ContentControl Content="{Binding ActiveContent}"/>
                </Grid>
            </GroupBox>
        </DockPanel>
    </DockPanel>
</Window>

DockPanelを用い外観の整列を行っている。DockPanelのTopとButtomを設定しているが今回はコントールを設定しない。
重要なのはDockPanelのCenter部分にあるGridである。

Gridのリソース内でDataTemplateを用い表示したいUIであるAViewのViewModelとViewを関連付ける。
表示したいUIはContentControlで管理する。

MainWindow.xaml.cs

namespace ScreenTransition1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainViewModel();
        }
    }
}

③ContentControlで設定した変数ActiveContentに表示したいUIのViewModelを設定

MainViewModel.cs
ContentControlで設定した変数ActiveContentに表示したいUIのViewModelを設定。

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

namespace ScreenTransition1
{
    using ViewModels;
    public class MainViewModel:MainContent
    {
        public MainViewModel()
        {
            AViewModel = new AViewModel();
            ActiveContent = AViewModel;
        }
        private MainContent _activeContent;
        public MainContent ActiveContent
        {
            get { return _activeContent; }
            set
            {
                if (_activeContent != value)
                {
                    _activeContent = value;
                    RaisePropertyChanged("ActiveContent");
                }
            }
        }

        AViewModel _aViewModel;
        public AViewModel AViewModel
        {
            get { return _aViewModel; }
            set
            {
                if (_aViewModel != value)
                {
                    _aViewModel = value;
                    RaisePropertyChanged("AViewModel");
                }
            }

        }

    }
}

出力結果

コンボボックスで複数画面の画面遷移を行う場合

OverView

この例ではMainWindow.xamlをUIの受け皿とし、AView.xamlとBView.xamlを受け皿に入れるUIとする。UIの変更=画面選択はコンボボックスで行う

プロジェクト階層

プロジェクトはMVVMパターンを採用しており、Viewに関するXAMLやクラス、ViewModelに関するクラスがある。
各プログラムの関係図を示す。引き続きでViewとViewModelでContentControlを利用し画面遷移をする。

各コードやXAMLを解説していく。

MainWindow.xaml
<Window x:Class="ScreenTransition1.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:ScreenTransition1"
        xmlns:vm="clr-namespace:ScreenTransition1.ViewModels"
        xmlns:view="clr-namespace:ScreenTransition1.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800">
    <DockPanel>
        <DockPanel DockPanel.Dock="Top"/>
        <DockPanel DockPanel.Dock="Bottom"/>
        <DockPanel Margin="5">
            <GroupBox Padding="5">
                <GroupBox.Header>
                    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                        <ComboBox ItemsSource="{Binding Contents}" SelectedItem="{Binding ActiveContent}" Width="300">
                            <ComboBox.ItemTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Title}"/>
                                </DataTemplate>
                            </ComboBox.ItemTemplate>
                        </ComboBox>
                    </StackPanel>
                </GroupBox.Header>
                <Grid>
                    <Grid.Resources>
                        <DataTemplate DataType="{x:Type vm:AViewModel}">
                            <view:AView/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type vm:BViewModel}">
                            <view:BView/>
                        </DataTemplate>
                    </Grid.Resources>
                    <ContentControl Content="{Binding ActiveContent}"/>
                </Grid>
            </GroupBox>
        </DockPanel>
    </DockPanel>
</Window>

BViewも画面遷移させたいのでDataTemplate にBViewのViewとViewModelを追加している。

AView.xaml,BView.xaml,MainWindow.xaml.cs

AView.xamlとBView.xamlは前節とコンテンツの配置や数は変わっていない。テキストがAAAAになっているかBBBBBBになっているかの違いだけである。
MainWindow.xaml.csは変更点なし。
ComboBoxで画面の選択を行うためにContentControlのバインディン変数であるActiveContentをSelectedItemにも使用している。また、ComboBoxに表示するテキストも追加している。

MainContent.cs

コンボボックスに表示するテキストをバインディングするためのプロパティをコンストラクタで使用する。

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

namespace ScreenTransition1.ViewModels
{
    public class MainContent : INotifyPropertyChanged
    {
        //UIに自動更新を行うためのイベント
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        public MainContent(string title)
        {
            Title = title;
        }
        public string Title { get; private set; }
    }
}
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ScreenTransition1
{
    using ViewModels;
    public class MainViewModel:MainContent
    {
        public MainViewModel() : base("MainView")
        {

        }

        private IList<MainContent> _contents;
        public IList<MainContent> Contents
        {
            get { return _contents ?? (_contents = CreateContents().ToArray()); }
        }
        private IEnumerable<MainContent> CreateContents()
        {
            yield return new AViewModel();
            yield return new BViewModel();
        }
        private MainContent _activeContent;
        public MainContent ActiveContent
        {
            get { return _activeContent ?? (_activeContent = AViewModel); }
            set
            {
                if (_activeContent != value)
                {
                    _activeContent = value;
                    RaisePropertyChanged("ActiveContent");
                }
            }
        }
        public AViewModel AViewModel { get { return Contents.First() as AViewModel; } }
    }
}
出力結果

【C#-WPF】MVVMパターンにおけるViewとViewModel間のインスタンス生成方法について

MVVMパターンにおけるViewとViewModel間のインスタンス生成方法は主に2つある。
・Viewのコンストラクタでインスタンスを生成する方法。
XAMLのWindow.DataContextにインスタンスを生成する方法である。

準備

WPFのプロジェクトを作成したら自動生成されるMainWindow.xamlと同じ階層にMainViewModel.csを追加する。

ViewModelの役割:MainViewModel.cs 
Viewの役割:MainWindow.xaml(MainWindow.xaml.cs)

MainViewModel.cs, MainWindow.xamlについて述べる。

ViewModel:MainViewModel.cs

MainWindow.xamlのTextBlockで利用するNameプロパティ値を設定。
RaisePropertyChangedはUI更新のためのお約束設定である。詳細は割愛。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;//for INotifyPropertyChanged

namespace DataContext
{
    public class MainViewModel : INotifyPropertyChanged
    {
        //UIに自動更新を行うためのイベント
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        //TextBlock用プロパティ
        public string name = "Nameの初期設定を表示";
        public string Name
        {
            get { return name; }
            set
            {
                if (name != value)
                {
                    name = value;
                    RaisePropertyChanged("Name");
                }
            }
        }
    }
}
View:MainWindow.xaml

TextBlockをNameをプロパティ値とし設定。

<Window x:Class="DataContext.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:DataContext"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock Text="{Binding Name}"/>
    </Grid>
</Window>

Viewのコンストラクタでインスタンスを生成する方法

MainWindow.xaml.cs

コンストラクタにおいてDataContextにMainViewModelのクラスを代入しインスタンス変数を生成する。これによりViewとViewModelを関連づける。

namespace DataContext
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainViewModel();
        }
    }
}

出力結果
MainViewModel.csで設定したTextBlock用プロパティがUIに正常に表示された。

XAMLのWindow.DataContextにインスタンスを生成する方法

XAMLにWindow.DataContextを追加しMainViewModelを指定。これによりViewとViewModelを関連づける。

<Window x:Class="DataContext.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:DataContext"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <TextBlock Text="{Binding Name}"/>
    </Grid>
</Window>

Window.DataContext内のlocalはXAML名前空間で指定した変数のことである。

   <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>

ここでlocalはxaml名前空間にあるように"clr-namespace:DataContext""部分に等しいという意味であり、clr-namespaceはプロジェクトがあるMainWindowやMainViewModelと同じ階層を意味しています。

 xmlns:local="clr-namespace:DataContext"

出力結果
出力結果はViewのコンストラクタでインスタンスを生成する方法と同じです。