まえがき
C#に限った話ではありませんがプログラミングを勉強していくと、ある処理が終わるのを待たずに別の処理を実行する非同期処理というものがあります。
初学者の方には少し難しい項目ではありますが、下記のような分かりやすく解説した記事は多数存在するため仕組みや使い方などは学ぶことができます。
私も初めて非同期処理を勉強した時はこういった記事を参考にすることでコードの実行を行い、交互に出力される文字を見て非同期処理できてるなーと確認しながらこうも思いました。
「便利なのはわかるんだけど、これいつ使うの?」と
非同期処理は処理速度を向上させることができるなど利点がたくさんあることは分かるのですが、当時の私は同期処理で問題ないようなコードばかり書いていたため、必要性によるありがたみを感じることができませんでした。また非同期処理が必要なサンプルを調査しても簡単に環境が用意できるかつ非同期処理でないとならないことが直感的に分かるようなものが見つけることができませんでした。
最近になってこれらを満たすサンプルを思いついたので記事にしました。
方法
こちらのサイトにあるVSのアイコンの画像を共有フォルダに入れ、この画像をWPFで取得するという動きを行う際に、間違った場所へ画像を取得しに行くことで非同期処理のありがたみを感じます。
共有フォルダ準備
まず初めに表示する画像を入れる共有フォルダを用意します。
適当な場所にフォルダを作成し、右クリック⇒プロパティ⇒共有タブ⇒詳細な共有ボタンを選択し、「このフォルダーを共有する」のチェックボックスに入力すれば準備完了です。
Pathの調査
次に共有フォルダにアクセスするためにPCのIPv4 アドレスを調べます。方法はいくつかありますが、コマンドプロンプトの「ipconfig」コマンドで調べるのが一番簡単だと思います。
エクスプローラーのアドレスバーに「\\IPv4 アドレス\フォルダ名」を入力し、共有したフォルダの中身が表示できれば準備完了です。
「IPv4 アドレス」は調べたIPアドレスに書きかえてください
もし「\\IPv4 アドレス\フォルダ名」で表示されなければ「\\IPv4 アドレス」とアドレスバーに入力してみてください。共有設定を行ったフォルダが出てくると思います。
これでも出ない場合はIPv4 アドレスが間違っていると思われます。環境にもよりますがipconfigコマンドで複数のIPv4 アドレスが表示される場合があるため他のIPv4 アドレスを試してみてください。
WPFの準備
次にWPFで準備した画像を表示させるためにShareFolderという名前のWPFプロジェクトを作成します。
作成後に右方にあるソリューションエクスプローラーのShareFolderを右クリック⇒追加⇒クラスを選択し、MainViewModel.csを追加します。
追加したクラスとMainWindow.xamlに下記のコードを書きこみます。簡単にコードの解説すると、ボタンを押すことでMainViewModelの変数にあるPathと画像名を使用してBitmapImage Image変数に画像が入力され、バインドしている Image Source="{Binding Image}"に画像が表示されるといった具合です。
<Window
x:Class="ShareFolder.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:local="clr-namespace:ShareFolder"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<!-- 第1行 -->
<RowDefinition Height="*" />
<!-- 第2行 -->
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<Image Source="{Binding Image}" />
<Button
Grid.Row="1"
Command="{Binding ImageButton}"
Content="ボタン" />
</Grid>
</Window>
using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
namespace ShareFolder
{
public class MainViewModel : INotifyPropertyChanged
{
private BitmapImage _Image = new BitmapImage();
/// <summary>
/// 表示する画像を入れる変数
/// </summary>
public BitmapImage Image
{
get { return _Image; }
set
{
_Image = value;
OnPropertyChanged(nameof(Image));
}
}
/// <summary>
/// 共有フォルダまでのPath
/// 「IPv4」の箇所をipconfigコマンドで調査したものに変える
/// </summary>
public string IP = @"\\IPv4\共有フォルダ";
/// <summary>
/// 表示する画像名
/// </summary>
public string ImageName = "VSIcon.png";
/// <summary>
/// ボタンを押すと実行されるコマンド
/// </summary>
public ImageButtonCommand ImageButton { get; set; }
public MainViewModel()
{
//ボタンに画像の更新のために自身を渡す
ImageButton = new ImageButtonCommand(this);
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 変化を通知する関数
/// </summary>
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ImageButtonCommand : ICommand
{
/// <summary>
/// コマンドを読み出す側のクラス(View Model)を保持するプロパティ
/// </summary>
private MainViewModel _view { get; set; }
/// <summary>
/// コンストラクタ
/// コマンドで処理したいクラス(View Model)をここで受け取る
/// </summary>
public ImageButtonCommand(MainViewModel view)
{
_view = view;
}
/// <summary>
/// コマンドの動作を定義するメソッド
/// コマンドのルールとして必ず実装しておくメソッド
/// </summary>
public void Execute(object parameter)
{
//ディレクトリが存在するか確認
if (!Directory.Exists(_view.IP))
{
MessageBox.Show($"{_view.IP}に接続できませんでした", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
BitmapImage bitmap2 = new BitmapImage();
bitmap2.BeginInit();
//共有フォルダの画像を取得
bitmap2.UriSource = new System.Uri(Path.Combine(_view.IP, _view.ImageName));
bitmap2.EndInit();
//取得した画像を表示
_view.Image = bitmap2;
}
/// <summary>
/// コマンドのルールとして必ず実装しておくイベントハンドラ
/// 通常、このメソッドを丸ごとコピーすればOK
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// コマンドの有効/無効を管理するbool
/// </summary>
public bool CommandSwitch = true;
/// <summary>
/// コマンドの有効/無効を判定するメソッド
/// コマンドのルールとして必ず実装しておくメソッド
/// 有効/無効を制御する必要が無ければ、無条件にTrueを返しておく
/// </summary>
public bool CanExecute(object parameter)
{
return CommandSwitch;
}
}
}
コード入力後に実行ボタンを押し、表示される画面下方のボタンを押すと、下の画像のように共有フォルダに置いた画像が表示されれば準備完了です。
もし表示されない場合は、Pathを入れているstring IPと画像名を入れているstring ImageNameの変数の値が正しいか確認してください。
不具合の体験
準備ができたら非同期処理の必要性を感じるために不具合を発生させます。
MainViewModel.csのstring IP変数の中身のIPアドレスの部分の一番後ろの数字を変えるなどして存在しないIPアドレスに書きかえてから実行しボタンを押します。すると準備のように画像が出てこないだけではなく、数十秒ほど画面が固まりウィンドウの移動もできないと思います。これは同期的に応答するまでアクセスしてしまい、タイムアウトするまで他の操作を受け付けないことが原因です。
注意
ブロードキャストアドレスである255.255.255.255など形式にあっていないIPアドレスを入力するとすぐにエラーが発生するので気を付けてください(一敗)
非同期処理の実装
この現象が発生すると閉じるボタンを押して強制終了できないなど様々な問題があります。しかし非同期処理で画像を取得することで解決することができます。
画像の取得部分を非同期で実現したものが下記のコードです。
using System;
using System.ComponentModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
namespace ShareFolder
{
public class MainViewModel : INotifyPropertyChanged
{
private BitmapImage _Image = new BitmapImage();
/// <summary>
/// 表示する画像を入れる変数
/// </summary>
public BitmapImage Image
{
get { return _Image; }
set
{
_Image = value;
OnPropertyChanged(nameof(Image));
}
}
/// <summary>
/// 共有フォルダまでのPath
/// 「IPv4」の箇所をipconfigコマンドで調査したものに変える
/// </summary>
public string IP = @"\\IPv4\共有フォルダ";
/// <summary>
/// 表示する画像名
/// </summary>
public string ImageName = "VSIcon.png";
/// <summary>
/// ボタンを押すと実行されるコマンド
/// </summary>
public ImageButtonCommand ImageButton { get; set; }
public MainViewModel()
{
//ボタンに画像の更新のために自身を渡す
ImageButton = new ImageButtonCommand(this);
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 変化を通知する関数
/// </summary>
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ImageButtonCommand : ICommand
{
/// <summary>
/// コマンドを読み出す側のクラス(View Model)を保持するプロパティ
/// </summary>
private MainViewModel _view { get; set; }
/// <summary>
/// コンストラクタ
/// コマンドで処理したいクラス(View Model)をここで受け取る
/// </summary>
public ImageButtonCommand(MainViewModel view)
{
_view = view;
}
/// <summary>
/// コマンドの動作を定義するメソッド
/// コマンドのルールとして必ず実装しておくメソッド
/// </summary>
public async void Execute(object parameter)
{
//非同期で画像を取得
await UpdateImageAsync();
}
/// <summary>
/// 画像を非同期で取得する関数
/// </summary>
private async Task UpdateImageAsync()
{
await Task.Run(() =>
{
//ディレクトリが存在するか確認
if (Directory.Exists(_view.IP))
{
//UI スレッドでの操作を行う
//別スレッドからUIの操作はできない
Application.Current.Dispatcher.Invoke(() =>
{
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
//共有フォルダの画像を取得
bitmap.UriSource = new System.Uri(Path.Combine(_view.IP, _view.ImageName));
bitmap.EndInit();
//取得した画像を渡す
_view.Image = bitmap;
});
}
else
{
MessageBox.Show($"{_view.IP}に接続できませんでした", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
});
}
/// <summary>
/// コマンドのルールとして必ず実装しておくイベントハンドラ
/// 通常、このメソッドを丸ごとコピーすればOK
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// コマンドの有効/無効を管理するbool
/// </summary>
public bool CommandSwitch = true;
/// <summary>
/// コマンドの有効/無効を判定するメソッド
/// コマンドのルールとして必ず実装しておくメソッド
/// 有効/無効を制御する必要が無ければ、無条件にTrueを返しておく
/// </summary>
public bool CanExecute(object parameter)
{
return CommandSwitch;
}
}
}
IP4の箇所を不具合をが発生した時と同じように存在しないIPアドレスに書きかえてから実行しボタンを押すと、画像が表示されないのは同じですが今度はウィンドウを動かせたり、閉じるボタンで強制終了ができるようになります。
非同期処理の実装前と実装後を比べるとありがたみを感じることができると思います。
あとがき
この記事では共有フォルダにある画像をWPFで取得して表示するときに、間違った場所へ同期的に接続するか非同期的に接続するか比べることで、非同期処理の必要性を直感的に感じてもらうことが目的でした。
「非同期処理 何が嬉しい」
「非同期処理 必要性」
といった検索をしている方の助けとなれば幸いです。
おまけ
本記事で使用したコードは読みやすくするためできるだけ簡潔に書きました。そのため実用的ではない書き方をしている箇所が多々あり、ものによっては深刻な不具合が発生します。それらをここで修正していきます。
ボタンの2度押し
ボタンを押したときに実行されるExecute関数ですが、このままでは非同期に画像を取得中にボタンを押すと、再度関数が実行できてしまいます。競合が起きてうまくデータを取得できない場合もあるので、画像を取得中はボタンを押せないように修正します。
public async void Execute(object parameter)
{
if (!CommandSwitch) return;
//非同期タスク中にコマンドを実行できないようにする
CommandSwitch = false;
//非同期で画像を取得
await UpdateImageAsync();
//以下の行はUpdateImageAsyncが終了したら実行される
CommandSwitch = true;
}
画像情報の変更ができない不具合
上記記事のコードでは画像を表示中に画像の変更、削除などの操作を行おうとすると、画像のようにエラーが発生して操作を完了することができないといった不具合が発生します。
この不具合は画像を取得するときに
bitmap.CacheOption = BitmapCacheOption.OnLoad;
をすることにより回避することができます。
private async Task UpdateImageAsync()
{
await Task.Run(() =>
{
//ディレクトリが存在するか確認
if (Directory.Exists(_view.IP))
{
//UI スレッドでの操作を行う
//別スレッドからUIの操作はできない
Application.Current.Dispatcher.Invoke(() =>
{
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
// 削除変更ができるようにする
bitmap.CacheOption = BitmapCacheOption.OnLoad;
//共有フォルダの画像を取得
bitmap.UriSource = new System.Uri(Path.Combine(_view.IP, _view.ImageName));
bitmap.EndInit();
//取得した画像を渡す
_view.Image = bitmap;
});
}
else
{
MessageBox.Show($"{_view.IP}に接続できませんでした", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
});
}
画像の差し替えができない不具合
本記事では共有フォルダに置いてある、VSアイコンの画像を取得して表示するといった動きを行いました。当然のことですが他の画像を共有フォルダに入れて名前を指定するとその画像を表示させることができます。しかし表示している画像を他の画像に差し替えることができないといった不具合が発生します。
例えば表示中に共有フォルダの画像をこちらのサイトにあるqiitaのアイコンの画像で上書きをしてボタンを押してみます。
警告
画像の名前は「VSIcon.png」と変えてください
直感的な動作としてはボタンを押すと差し替えられた画像の情報を取得し、変数BitmapImage Imageに上書きされるため画像が更新されると考えられますが、そういった動作にはなりません。VSのアイコンの画像が表示されます。
この不具合は画像を取得するときに
using (FileStream stream = File.OpenRead(ImageFilePath))
を使用することで回避できます。
private async Task UpdateImageAsync()
{
await Task.Run(() =>
{
//ディレクトリが存在するか確認
if (Directory.Exists(_view.IP))
{
//UI スレッドでの操作を行う
//別スレッドからUIの操作はできない
Application.Current.Dispatcher.Invoke(() =>
{
BitmapImage bitmap = new BitmapImage();
using (FileStream stream = File.OpenRead(Path.Combine(_view.IP, _view.ImageName)))
{
bitmap.BeginInit();
// 削除変更ができるようにする
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
stream.Close();
}
_view.Image = bitmap;
});
}
else
{
MessageBox.Show($"{_view.IP}に接続できませんでした", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
});
}
おわり
おまけは以上です。
このおまけもどなたかの助けとなれば幸いです。