質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

MVVM

MVVM(Model View ViewModel)は構築上のデザインパターンで、表現ロジック(ViewModel)によってデータ(Model)からページ(View)を分離させます。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

Q&A

1回答

4688閲覧

WPF(MVVM)で、Taskを使った別スレッドからビットマップイメージが表示できません。

heero

総合スコア8

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

MVVM

MVVM(Model View ViewModel)は構築上のデザインパターンで、表現ロジック(ViewModel)によってデータ(Model)からページ(View)を分離させます。

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

WPF

Windows Presentation Foundation (WPF) は、魅力的な外観のユーザー エクスペリエンスを持つ Windows クライアント アプリケーションを作成するための次世代プレゼンテーション システムです

0グッド

0クリップ

投稿2020/05/27 03:05

編集2020/05/29 07:37

こんにちは。
Windows10でVisual Studio 2017 Proを使って開発しています。

C# WPF(MVVM)で別スレッドからビットマップイメージを表示する際に問題が
発生しているため、お力添えをお願いします。

前提・実現したいこと

C# WPFで、MVVMの構成でTaskを使った別スレッドから受け取ったビットマップイメージを"1枚ずつ"表示したいです。

C#のWPF、MVVMの構成で、外部(PCとUSB接続している機械)から受信したビットマップイメージを表示しようとしています。
受信するビットマップイメージは複数枚(枚数は毎回違う)で、受信タイミングも不定期です。
データが複数枚且つ不定期受信であること、USB通信や受信データのチェック処理を行うこと、受信に成功したデータを
順次表示するため、Task.Runを使用してViewModelからModelのメソッドを呼び出すことで、別スレッドで処理しています。
1度の操作で複数枚のイメージデータを表示するため、受信したデータはデリゲートと使ってModelからViewModelへ渡し
ViewModelのでウィンドウのコントロールにバインドすることで表示しようとしています。

処理のイメージは以下通りです。

1.ボタンクリック  ↓ 2.ViewModelからModelのビットマップイメージ受信メソッドを呼び出す (ここでTask.Runを使用して別スレッド化)  ↓ 3.ModelからUSB通信処理を呼び出す  ↓ 4.USB通信でビットマップイメージデータを受信  ↓ 5.Modelへデリゲートで受信したデータを渡す  ↓ 6.USB通信処理から受け取ったデータをファイル出力  ↓ 7.ModelからViewModelへデリゲートで受信したデータを渡す  ↓ 8.Modelから受け取ったビットマップイメージデータをコントロールにバインドして表示 (ビットマップイメージのコントロールは1つのため、以降データを上書いていく)  ↓ 4~8をデータを受信し終えるまで繰り返す  ↓ 9.データ受信を終えたら処理をViewModelへ戻す

発生している問題

1枚分のビットマップイメージを受信して表示すること、一定間隔(3秒以上)で受信して表示することには
成功しているのですが、受信間隔が短い(3秒未満)場合に2枚目以降のビットマップイメージが表示されません。
表示されない状態というのは、1枚目のビットマップイメージが消え、ビットマップイメージ表示領域が
白背景の状態です。

エラーメッセージの表示やアプリケーションが落ちることはなく、2枚目以降が表示さず動き続けています。
上記、「前提・実現したいこと」に記載の処理イメージの6.に記載しているファイル出力は正常に行われており、
出力したファイルのデータは、別のビットマップイメージを表示するツールで確認すると問題なく表示できます。
おそらく、ViewModelでビットマップイメージをコントロールにバインドしている処理あたりが原因かとおもうのですが、
なぜ2枚目以降が表示されないのか原因が分かりません。

該当のソースコード

以下、実際のソースコードから問題に関連する箇所のみを掲載しています。

Window.xaml(View)

C#

1<!-- ImageタグのSourceにビットマップイメージデータをバインドする --> 2<Image Source="{Binding BitmapImageData}" Stretch="Uniform"/> 3 4<!-- データ受信実行ボタンは省略 --> 5

Window.xaml.xs

C#

1private ViewModel m_viewModel;   // ViewModelクラス 2public Window() 3{ 4 InitializeComponent(); 5 6 m_viewModel = new ViewModel(); 7 DataContext = m_viewModel; 8}

ViewModelBase(ViewModelの基底クラス)

C#

1// ViewModelのプロパティにセットされたデータをViewへ反映する 2public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo 3{ 4 ~ViewModelBase() 5 { 6 ClearResource(); 7 } 8 9 private bool _isCrlResource = false; 10 11 protected virtual void ClearResource() 12 { 13 if (_isCrlResource) return; 14 15 if (PropertyChanged != null) 16 { 17 foreach (var handler in PropertyChanged.GetInvocationList()) 18 { 19 PropertyChanged -= (PropertyChangedEventHandler)handler; 20 } 21 } 22 23 PropertyChanged = null; 24 _isCrlResource = true; 25 } 26 27 /// <summary> 28 /// INotifyPropertyChanged.PropertyChanged の実装。 29 /// INotifyPropertyChanged.PropertyChanged implement. 30 /// </summary> 31 public event PropertyChangedEventHandler PropertyChanged; 32 33 /// <summary> 34 /// INotifyPropertyChanged.PropertyChangedイベントを発生させる。 35 /// INotifyPropertyChanged.PropertyChanged event is generated. 36 /// </summary> 37 protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) 38 { 39 if (object.Equals(storage, value)) return false; 40 41 storage = value; 42 43 if (PropertyChanged != null) 44 { 45 PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); 46 } 47 else 48 { 49 PropertyChanged = null; 50 } 51 52 return true; 53 } 54}

WindowViewModel.cs(ViewModel)

C#

1// ViewModelBase->INotifyPropertyChanged等を実装した基底クラス 2public class WindowViewModel : ViewModelBase 3{ 4 private WindowModel m_model; // Modelクラス 5 6 private BitmapImage _bitmapImageData; 7 // 受信したビットマップイメージをバインドするプロパティ 8 public BitmapImage BitmapImageData 9 { 10 get { return _bitmapImageData; } 11 set { SetProperty(ref _bitmapImageData, value); } 12 } 13 14 public WindowViewModel() 15 { 16 m_model = new WindowModel(); 17 m_model.SetDelegateFunc(DisplayBitmapImage); 18 BitmapImageData = null; 19 } 20 21 private async void ReadBitmapImage() 22 { 23 // ボタングレーアウト処理 24 25 await Task.Run(() => m_model.ReadBitmapImageData()); 26 27 // ボタングレーアウト解除処理 28 } 29 30 // Modelからビットマップイメージを受け取るデリゲート 31 private void DisplayBitmapImage(byte[] bitmapData) 32 { 33 BitmapImage bitmapImg = null; 34 try 35 { 36 using (var memStream = new System.IO.MemoryStream(bitmapData)) 37 { 38 bitmapImg = new BitmapImage(); 39 bitmapImg.BeginInit(); 40 bitmapImg.CacheOption = BitmapCacheOption.OnLoad; 41 bitmapImg.StreamSource = memStream; 42 bitmapImg.EndInit(); 43 bitmapImg.Freeze(); 44 } 45 } 46 catch (Exception) 47 { 48 // エラー処理 49 bitmapImg = null; 50 } 51 52 BitmapImageData = bitmapImg; // ビットマップコントロールにデータをセット 53 } 54}

WindowModel.cs(Model)

C#

1public class WindowModel() 2{ 3 private Action<byte[]> m_displayBitmap; // ViewModelのビットマップイメージ表示メソッド 4 private byte[] m_bitmapData; // ビットマップイメージデータ 5 6 public WindowModel() 7 { 8 m_displayBitmap = null; 9 m_bitmapData = null; 10 } 11 12 public void ReadBitmapImageData() 13 { 14 USBComm.SetDelegateFunc(DisplayBitmapImage); // イメージデータを取得するためのデリゲート設定 15 USBComm.ReadImageData(); // イメージデータ取得 16 } 17 18 // USB通信処理クラスからビットマップイメージを受け取るデリゲート 19 private void DisplayBitmapImage(uint dataSize, bool dataErrFlag) 20 { 21 // 受信したデータに異常がない場合のみDLLから取得する 22 if (dataErrFlag == false) 23 { 24 byte[] buff = new byte[dataSize]; 25 USBComm.GetReceiveData(ref buff); 26 27 m_bitmapData = bitmapData; // 意味ないと思うが念のためメンバにセット 28 m_displayBitmap(m_bitmapData); // Viewのビットマップイメージ表示メソッド呼び出し 29 m_bitmapData = null; 30 } 31 32 USBComm.ClearReceiveData(); // DLL内のデータをクリア 33 } 34}

USBComm.cs(USB通信処理DLLインターフェイス)

C#

1public static class USBComm 2{ 3 /// <summary> 4 /// DLL内で取得したイメージデータのサイズとエラー情報を取得するデリゲート 5 /// </summary> 6 /// <param name="dataSize">イメージデータサイズ</param> 7 /// <param name="errFlag">データエラーフラグ : 0 = Not error / !0 = Error</param> 8 public delegate void DelegateDisplayBitmapImage(uint dataSize, byte errFlag); 9 10 static DelegateDisplayBitmapImage m_delegateFunc; 11 12 // イメージデータ取得用デリゲートメソッドのセット 13 [DllImport("USBComm.dll", EntryPoint = "SetDelegateFunc", CallingConvention = CallingConvention.StdCall)] 14 private static extern void DllSetDelegateFunc(DelegateDisplayBitmapImage func); 15 // イメージデータ受信 16 [DllImport("USBComm.dll", EntryPoint = "ReadImageData", CallingConvention = CallingConvention.StdCall)] 17 private static extern void DllReadImageData(); 18 // 受信データ取得 19 [DllImport("USBComm.dll", EntryPoint = "GetReceiveData", CallingConvention = CallingConvention.StdCall)] 20 private static extern void DllGetReceiveData(IntPtr iPtr); 21 // 受信データクリア 22 [DllImport("USBComm.dll", EntryPoint = "ClearReceiveData", CallingConvention = CallingConvention.StdCall)] 23 private static extern void DllClearReceiveData(); 24 25 public static void SetDelegateFunc(DelegateDisplayBitmapImage func) 26 { 27 m_delegateFunc = func; 28 DllSetDelegateFunc(m_delegateFunc ); 29 } 30 31 public static void ReadImageData() => DllReadImageData(); 32 33 public static void GetReceiveData(ref byte[] buff) 34 { 35 // 取得するデータサイズ分IntPtrのメモリ領域を確保 36 IntPtr ptr = Marshal.AllocHGlobal(buff.Length); 37 // DLLからデータを取得 38 DllGetReceiveData(ptr); 39 // DLLから取得したデータをコピー 40 Marshal.Copy(iPtr, buff, 0, buff.Length); 41 // IntPtrのメモリ解放 42 Marshal.FreeHGlobal(ptr); 43 } 44 45 public static void ClearReceiveData() => DllClearReceiveData; 46}

USBCommクラスで参照しているDLL(USBComm.dll)はC++で実装しています。
DLLのソースコードは、セキュリティの問題で掲載できません。
同じDLLを使用したC++ MFCのツールが存在し、そちらで同じように複数枚のイメージデータを
非同期処理で受信→表示することができています。

確認したこと

Taskを使用した別スレッドでUIスレッド外からのアクセスのため、表示ができない
(1枚目や一定間隔はできているが)のかと思いネットで情報を探してみたのですが、
MVVMでバインディングを介してウィンドウを更新する場合は問題ないという内容が
いくつか見つかりました。
ビットマップイメージを表示するための"Imageタグ"のプロパティにデータがセット
されていることはデバッグで確認しているのですが、先に表示されていたイメージが
消え白背景に更新されてしまいます。
そのため、現状何が原因か分からず、手詰まりの状態です。

補足情報(FW/ツールのバージョンなど)

開発環境
Windows 10 Pro
Microsoft Visual Studio Prodessional 2017 Version 15.9.15
.NET Framework Version 4.8.03752
ターゲットフレームワーク 4.5
言語バージョン C# 6

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ebiryo

2020/05/27 08:35

「4~8をデータを受信し終えるまで繰り返す」とありますが、繰り返しを実行している箇所のソースはありますか?
heero

2020/05/28 01:30

ご確認頂きありがとうございます。 繰り返しは以下のメソッドを順に実行しています。 4.USB通信でビットマップイメージデータを受信 →ModelクラスのReadBitmapImageDataメソッドでDLLのReadImageData関数を呼び出す。 5.Modelへデリゲートで受信したデータを渡す →DLLからModelクラスのDisplayBitmapImageメソッドが呼び出される。 6.USB通信処理から受け取ったデータをファイル出力 →ModelクラスのDisplayBitmapImageメソッドが引数に指定されたサイズ分  byte配列を確保し、DLLのGetReceiveData関数を使って受信データを取得しファイル出力。 7.ModelからViewModelへデリゲートで受信したデータを渡す →DLLから取得したデータをViewModelクラスのDisplayBitmapImageメソッドを呼び出して  ViewModelへ渡す。 8.Modelから受け取ったビットマップイメージデータをコントロールにバインドして表示 (ビットマップイメージのコントロールは1つのため、以降データを上書いていく) →ViewModelクラスのDisplayBitmapImageメソッドは受け取ったデータをバインドして終了。  (処理がModelクラスのDisplayBitmapImageメソッドへ戻る。) USB通信を行うDLLのソースコード(ReadImageData関数のソース)はセキュリティの関係上 掲載できませんので、ご了承ください。 DLL内の処理の簡単な説明としては、受信データのフォーマットチェック後、上位(App)から セットされたコールバック関数の引数に受信データサイズとフォーマットチェック結果を 指定して呼び出し、コールバック関数(ModelクラスのDisplayBitmapImageメソッド)から 処理が返ってきたら、またデータ受信を行うといった流れです。 下手な文章での説明となってしまい申し訳ございません。 ご尽力、よろしくお願い致します。
hihijiji

2020/05/28 03:17

View側の問題なのかModel側の問題なのか切り分ける必要があります。 Viewを確認するには別途、BitmapImageDataを更新するModelを書いてください。
heero

2020/05/28 07:39

ご確認頂きありがとうございます。 ViewのImageタグへデータをバインドする際に変更を通知しているViewModelの 基底クラス、"ViewModelBase"のソースコードを追加致しました。 ViewModelではBitmapImageDataをBitmapImageでプロパティ宣言し、 setメソッド内でViewModelBaseクラスの"SetPropertyメソッド"を 呼び出すことで、データ変更を通知しています。 データ変更は、ViewModelクラスの"DisplayBitmapImageメソッド"の 最後の行、「BitmapImageData = bitmapImg;」で行っています。 記載内容やソースの不備がございましたら、ご指摘お願い致します。
ebiryo

2020/05/28 07:56

念のため確認なのですが、WindowViewModel のDisplayBitmapImage自体は複数回呼ばれていて、 BitmapImageData に新しいBitmapがセットされているのに、表示されないということですよね?
heero

2020/05/29 00:01

ebiryoさん 仰る通りです。デバッグで確認した際は、DisplayBitmapImageがModelから呼び出され、 BitmapImageData にBitmapデータがセットされています。 リリースビルドでは、DisplayBitmapImageが呼び出されることを確認するため、 テキストボックスを用意してそこに文字を表示する形で確認しましたが、問題ありませんでした。
ebiryo

2020/05/29 00:54 編集

Modelから呼び出されたDisplayBitmapImageのところで画像をファイルに落として確認してみてはいかがでしょうか。 呼び出されていることが確認ずみなら、3回呼ばれたらVMのDisplayBitmapImageで3ファイル生成されますよね?その内容を確認してみてはいかがでしょうか。 Modelから渡されたデータが同じものである可能性も否定できない気がするのですが。
heero

2020/05/29 07:52

ebiryoさん コメントありがとうございます。DisplayBitmapImageメソッドでのファイル出力、確認してみます。 "表示されない"状態というのが、表示されていた1枚目のイメージが消えて何も表示されない(白背景)状態に なってしまいます。"2回目以降が表示されない"の"表示されない"という状態についての説明が足りないため 誤解を招いてしまったみたいです。 質問内容の「発生している問題」と「確認したこと」に同様の詳細を明記致しました。
退会済みユーザー

退会済みユーザー

2020/05/29 08:40

非同期処理をお使いとのことですが、スレッドセーフとか排他制御とか大丈夫です? メソッドはきちんと想定通りの順番で呼び出されているか、ログに出力したりして確認しました?
heero

2020/06/02 06:52

radianさん コメントありがとうございます。 Taskのため、指定したメソッド内で起きた例外はtry-catchで拾うようにしています。 ログに出力して確認はしていませんが、デバッグ実行でブレークを張って呼び出し順は 確認しています。
退会済みユーザー

退会済みユーザー

2020/06/02 07:41 編集

うーん、言ってる事が伝わってないかな?例外の話では無いです。 > 受信するビットマップイメージは複数枚(枚数は毎回違う)で、受信タイミングも不定期です。 と記述されていて、間隔が短い場合に問題が発生しているということは、不完全なデータを受け取ったり、同時に実行されてはまずい処理が同時に動いている=排他制御出来てないのではということです。 ブレークポイントでのチェックは、十分な時間的余裕が与えられてしまうので、間隔が短い状況の再現は出来なくないです?面倒くさがらずにログに出しましょう。受信したバイトデータの出力→受信でのビットマップのハッシュ値を取ったりして、データが化けてないかもチェックしましょう。
guest

回答1

0

MVVMがスレッドフリーかどうかは知らないですが、VMクラスのロジックをスレッドフリーで設計する必要があり、破綻してもおかしくないと思います。

Model部でTaskを大量に非同期実行して、VM部で結果をListViewに表示するプログラムを作ったことがあるので実現方法を紹介します。

Model部の各タスクから通知を受けたときに、データをVM部で持っているキューに保存します。この関数のみマルチスレッドで動きます。
それとは別に、VMで定期実行しているタイマーハンドラーで、キューを取り出してUIに反映させます。
こうすることで、スレッドフリーの関数を1つに集約しつつ、UIへの表示タイミングはVM部で調整できます。

キューにはスレッドフリーの ConcurrentQueueを使用することで、queueの出し入れに排他ロックは不要になります。

以下、VM部のソース抜粋します

c#

1 2private ConcurrentQueue<Node> queue; 3 4// この関数はModelのスレッドからコールされる。 5// マルチスレッドからコールされるので、スレッドフリーの設計が必要 6public void fromModel(Node n) 7{ 8 this.queue.Enqueue(n); // VMのキューに保存 9}; 10 11// VMの定期実行タイマーより起動 12private void OnTimer() 13{ 14 bool f = false; 15 while (this.queue.TryDequeue(out Node n)) 16 { 17 this.list.Add(new NodeView(n)); // ListViewに登録 18 f = true; 19 } 20 if(f) 21 { 22 base.RaisePropertyChanged("List"); // Viewに更新通知 23 } 24} 25

投稿2020/09/24 14:09

ruruucky

総合スコア18

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問