0 と 1 の世界の見習い探検家

カテゴリ: プログラミング日誌

イメージ 1


Window Handle Viewerのバージョン0.9.2.10を公開しました。
プレビュー領域中の任意の場所をクリックすると、その位置を領域内に含んでいるウィンドウハンドルを列挙する機能を追加しました。

Window Handle Viewer - ソフトウェア | 0と1の世界の見習い探検家
http://www.a32kita.tk/software/winhandlevw/

では。

TextWriter系クラスで最後まで内容が反映されないというトラブルの対処法についてご紹介いたします。

TextWriter

TextWriterとは、Stream系クラス (FileStream / MemoryStream) などに対して、データの書き込みを行うために提供されるクラスです。
TextWriterの拡張クラスとして次のようなクラスが System.IO 名前空間で提供されております。

表1 代表的なTextWriter系クラスと概要

クラス名

概要

StreamWriter

文字を特定のエンコーディングでストリームに書き込むための TextWriter を実装しています。

BinaryWriter

プリミティブ型をバイナリでストリームに書き込みます。特定のエンコーディングの文字列の書き込みをサポートします。

出典: MSDN

これらのクラス全般で発生しうるトラブルのお話です。

バッファリング機能


イメージ 1
図1 TextWriterのバッファリング機能

標準的なTextWriter系クラスは、Streamへの書き込みのトランザクションで発生するオーバーヘッドの軽減のため、内部でのバッファリング機能が実装されております。
ある程度のデータがTextWriterのインスタンス内でバッファリングされたのち、Streamへ実際の書き込みが行われます。
TextWriter系クラスからStreamへ内部バッファの書き込みが行われる前にプログラムが終了してしまうなどすると、バッファされたデータは消失します。

CloseメソッドとFlushメソッド

TextWriter系クラスには、Stream系クラス同様「Close」メソッドが実装されております。このメソッドは、ターゲットになっているStreamをクローズするものでありますが、実はそれだけではありません。
TextWriterでCloseを実施する際、内部ではStreamをCloseする前に先程紹介したバッファをStreamへ書き込む処理もおこなっております。
プログラムやスレッドが終了する前にCloseまで実施されていれば、正しくバッファの反映処理まで実行されます。
またこのClose処理は、TextWriter.Dispose()でも実行されるため、usingセクションでTextWriterを使用するなどの方法でバッファを反映されることも可能です。

バッファの反映処理は、Closeメソッド内以外でも単独で提供さております。
それがFlushメソッドです。

つまり、CloseメソッドではFlushメソッドとStreamのCloseメソッドが実行されているということになります。

AutoFlushプロパティ

TextWriterには、AutoFlushプロパティが用意されております。
このプロパティはbool型の値を取り、デフォルトではfalseです。このプロパティにtrueを設定すると、Write系メソッドが実行される度にFlush処理が実行されるようになります。
もちろん、書き込み処理でのオーバーヘッドが大きいタイプでのStreamでは、パフォーマンスに大きな影響が出てしまいますが、、

まとめ

原則としてTextWriterは必ずスレッドまたはプログラム終了前にCloseしましょう。
Closeを行う前にStreamに対してすべての書き込みを反映させる必要がある場合は、FlushメソッドやAutoFlushプロパティを活用しましょう。

イメージ 1

1年ぶりの新しいソフトの公開です。

Window Handle Viewerは、お使いのWindowsでデスクトップ上に展開されているウィンドウのハンドル情報を閲覧すためのツールアプリケーションです。ウィンドウハンドルの値、ウィンドウの名前、クラス名、位置と大きさを確認することが出来ます。
Win32の標準的なコントロールを使用しているウィンドウでは、ウィンドウ上に配置されたコントロールにもウィンドウハンドルが割り当てられており、本ソフトウェアではこれらのコントロールのハンドル情報も確認することが可能です。

こちらのページからダウンロード可能です。

Window Handle Viewer - ソフトウェア | 0と1の世界の見習い探検家
http://www.a32kita.tk/software/winhandlevw/


では。

イメージ 1
図1 ビルドエラー

MC3093: 要素 'TestControl' で Name 属性値 'testcont102' を設定することはできません。'TestControl' は、要素 'ExtendedGrid' のスコープ内にあり、この要素には、別のスコープで定義されたときに既に名前が登録されています。

WPFのアプリケーションを開発してる時に謎のエラーに当たりましたので、発生状況、対処法、原因の考察についてご紹介いたします。

発生状況

このビルドエラーには次の状況で遭遇しました。

イメージ 2
図2 XAMLを利用して既存のPanel系コントロールを拡張

イメージ 3
図3 拡張したPanel系コントロールに別のコントロールを追加

  • 既存のPanel系コントロール (Gridなど) を継承して、拡張したコントロールを定義
  • 拡張の際、XAMLを利用 (図2)
  • 拡張したPanel系コントロールをウィンドウなどの別のコントロールのXAMLで利用
  • そのXAMLの中で拡張したPanel系コントロールに別のコントロールを追加
  • 追加したコントロールにx:Nameで名前を付与 (図3)

なかなか既存のPanel系コントロールを強引に拡張する機会がまず少ないですが、だからなのかこのエラーに関する情報も見つからなかったので、あえて書いておきます。。

対処法

すべてのビルドエラーMC3093が同じ原因により発生しているという保証ができないので、なんとも言えませんが、とりあえず上記状況で発生したものに関しては、次の方法で対処しました。
対処法は、2つあります。

x:Nameの付与をやめる

拡張したPanel系コントロールの利用側で、追加したコントロールへのx:Nameをしなければ、このビルドエラーは回避できます。
不要ならば削除してしまいましょう。

拡張時にXAMLを使用しない

この方法は、ざっくり言えばPanel系コントロールを拡張する際に図2のようなXAMLを使用しないことです。
私がこの例外に陥った時は、XAMLとコード混みでGridなどを継承して拡張をおこなっておりました。しかし、実際の拡張の実装はすべてコードでやっていたので本当はXAMLは不要だったんです。ということでとりあえずコードファイル (*.cs) ファイルだけ残して、XAMLファイル (*.xaml) を削除したら、問題は解決しました。

原因の考察

Panel系コントロールを拡張する際、XAMLソースも混みでやってしまうと、その拡張したコントロールに添付されているXAMLファイル側でコントロールの追加が可能になります。エラーメッセージの言う「別のスコープ」とは恐らく、この拡張時に作ってしまったXAMLファイルのことを指しているのかと。
よくわかりませんんが、拡張側のXAMLスコープと利用側のXAMLスコープの両方でx:Nameが付与されることを警戒しているのがこのビルドエラーなのかなと。
はっきり言って原因はよくわからないのが現状です…。

その他

Panel系コントロールを継承して拡張するのはあまり正攻法とされていないようですので、もっと別の手段で拡張したほうが良さそうですね。

先日取り上げた、「ObservableCollection」でClearメソッド実行直前まで代入されていたアイテムたちを、CollectionChangedで取得するお話の続きです。

【C#備忘録】ObservableCollection.Clearメソッドの罠 (2018/2/27(火) 午後 7:21)
https://blogs.yahoo.co.jp/a32kita/15752321.html

こんなの実装してみました。

a32kita/ExtendedObservableCollection
https://github.com/a32kita/ExtendedObservableCollection

ちょっとその解説を…。

先行研究「ObservableCollectionの拡張」

まず、CollectionChangedイベントでClearメソッドされる直前の中身を取得する方法として、前回の記事の最後でObservableCollectionを継承した拡張クラスの実装についてご紹介致しました。
前回の記事は、結果としては未解決のまま終了してしまいました。

失敗の主な理由は、CollectionChangedのイベントデータ格納型であるNotifyCollectionChangedEventArgsクラスでは、ActionにコレクションのClearを表す「Reset」を設定したとき、変更されたアイテムの一覧を表すChangedItemsには何も設定が行えない、というところにありました。
NotifyCollectionChangedEventArgsのコンストラクタにResetを設定した状態で、ChangedItemsの設定を行おうとしてしまうと、ArgumentExceptionを吐かれてしまいます。
つまり、NotifyCollectionChangedEventArgs.OldItemsにReset前のコレクションの中身を放り込む行為は、.NETを創造した神様たちがお許しにならないということのようです。
神様の思し召しとなれば仕方がありません。それが天啓というのであれば、我々は抗うべきではありません。
OldItemsにデータを格納するのは諦めましょう。

というわけで、ObservableCollectionの拡張に加え、もう1つ拡張することにしました。

提案手法「NotifyCollectionChangedEventArgsの拡張」

まず前回の記事の末尾で述べた通り、ObservableCollectionを拡張した「ExtendedObservableCollection」なるものを定義しました。
ObservableCollectionを継承し、protectedなメソッド、ClearItemsをオーバーライドすることで、Clear時の処理を変更します。

しかし、これだけではCollectionChangedを通して、Clear実行前のコレクションの内容を通知することはできません。
だったらOldItemsではなく、NotifyCollectionChangedEventArgsに新しいプロパティを作ってしまえば良いというのが今回の提案手法です。

わざわざ既存のNotifyCollectionChangedEventArgsを拡張するという手段を選んだのは、CollectionChangedイベントとの互換性を保つためです。

イメージ 1
図1 定義したExtendedObservableCollectionChangedEventArgs

名前を付けるのが下手で、えらく長い名前になってしまいました。
名前をつけるのが得意な方、新しい名前を付けてやって下さい。

このクラスの特長は、OldItems、NewItemsに加え「PreviousItems」という名前のプロパティを有しているというところです。
ExtendedObservableCollectionでは、Clear時にこのPreviousItemsにそれまでの内容を格納します。

NotifyCollectionChangedEventArgsの子クラスであるため、CollectionChangedイベントでイベントデータとして飛ばすことが可能です。
もちろん、CollectionChangedをハンドルする際に必要に応じてNotifyCollectionChangedEventArgs型のイベントデータをExtendedObservableCollectionChangedEventArgs型へキャストしていただく必要がありますが。

その他拡張

List<T>では利用できるのにObservableCollection<T>では利用できなかったAddRangeをExtendedObservableCollection<T>では実装しました。
AddRangeを実行すると、アイテムが全て追加されたあとにCollectionChangedイベントが発生し、NewItemsで追加されたアイテムたちを取得できる形にしています。

評価実験

一応動作実験。
Clear時の挙動は完全にオーバーライドしてしまっている都合上、正しく動作するかわからないんですよね。

イメージ 2
図2 CollectionChangedをハンドルするメソッド

イメージ 3
図3 Clear直後のCollectionChanged発生時のイベントデータ

一応正しくデータは受け取れているようですが、WPFなんかに利用するために実装したPropertyChangedの挙動に不安が。。
WPFのMVVMの中でお使いになる際は、ClearとAddRangeの挙動にご注意下さい。
私のほうでも気がついたら適宜修正します。。

最後に

名前がクソなクラスを定義してしまいましたが、一応前回の記事の解決編ということにはなったのではないでしょうか。
一応今回この記事で取り上げているプログラムは完全ライセンスフリーと宣言しておきます。
使えると思った方、もしいらっしゃいましたらご自由にお使い下さい。


このページのトップヘ