アプリケーションが応答しない?!
WPFアプリケーションをプログラミングする人の多くは、おそらくビューコンストラクトを何千回も書いているでしょう。
{アイテムのバインド }
Itemsコレクションからの
アイテムの受信がメインアプリケーションスレッドで行われ、時間がかかる場合、「デッド」ユーザーインターフェイスが表示されます。 アプリケーションはしばらくの間、状態の変化を描画せず、ユーザー入力に応答しません。
また、処理時間がWindowsウィンドウシステムで定義された特定の時間制限を超えると、システムはこのウィンドウを応答なしとしてマークします。最後に成功したウィンドウレンダリングの画像に白いマスクが適用され、タイトルに特別なマーカー(応答なし)が追加されます(
ロシア語のローカライズ ):
はい、あなた自身はおそらく、障害または同期操作の結果としていくつかのアプリケーションが「自分自身に入る」ときに何度も似たような画像を見たことがあるでしょう。 そして、もちろん、ユーザーがどれほど迷惑なのか知っています。 ほとんどの人
はヒステリーと戦い、モニターのキーボードを壊し、理解できないまま画面を見て、プログラムで何が起こっているのかを理解しようとします。 長時間の操作の終了を待たずに、より「上級」のユーザーは、タスクマネージャーを使用して「滞納」ウィンドウを閉じます。
解決策1:非同期ObjectDataProvider
このソリューションは非常にシンプルであり、現在のプロジェクトで
ObjectDataProviderをデータソースとして使用する場合に最適です。
ステップ#1:単純な静的データプロバイダーを実装する
プロバイダーは、1つのメソッドを持つ通常の静的クラスです。
// Emulates a long items getting process using some delay of getting of each item <br/>
public static class AsyncDataProvider<br/>
{ <br/>
private const int _DefaultDelayTime = 300 ; <br/>
<br/>
public static ReadOnlyCollection < string > GetItems ( ) <br/>
{ <br/>
return GetItems ( _DefaultDelayTime ) ; <br/>
} <br/>
<br/>
public static ReadOnlyCollection < string > GetItems ( int delayTime ) <br/>
{ <br/>
List < string > items = new List < string > ( ) ; <br/>
foreach ( string item in Enum . GetNames ( typeof ( AttributeTargets ) ) . OrderBy ( item => item. ToLower ( ) ) ) <br/>
{ <br/>
items. Add ( item ) ; <br/>
// Syntetic delay to emulate a long items getting process <br/>
Thread. Sleep ( delayTime ) ; <br/>
} <br/>
<br/>
return items. AsReadOnly ( ) ; <br/>
} <br/>
}
ステップ#2:XAMLで非同期データソースを宣言する
<Window.Resources > <br/>
<ObjectDataProvider x:Key = "AsyncDataSource" <br/>
IsAsynchronous = "True" <br/>
ObjectType = "Providers:AsyncDataProvider" <br/>
MethodName = "GetItems" /> <br/>
<Converters:NullToBooleanConverter x:Key = "NullToBooleanConverter" /> <br/>
</Window.Resources >
NullToBooleanConverterコンバーターは単なる補助オブジェクトであり、その目的は名前で読み取ることができます(
その実装は、トピックにアタッチされたプロジェクトにあります )。 すべての魔法は、
ObjectDataProviderの
IsAsynchronous =
"True"属性にあります。 この属性はデータの受信方法を制御します。この属性が
「True」に設定されている場合、WPFコアは
Dispatcherバックグラウンドオブジェクトを作成してこのプロパティの値を取得します。
ステップ#3:コードでデータプロバイダーを使用する
<ListBox x:Name = "ItemsListBox" <br/>
ItemsSource = "{Binding Source={StaticResource AsyncDataSource}, IsAsync=True}" > <br/>
<ListBox.Style > <br/>
<Style TargetType = "{x:Type ListBox}" > <br/>
<Style.Triggers > <br/>
<Trigger Property = "ItemsSource" Value = "{x:Null}" > <br/>
<Setter Property = "Template" Value = "{StaticResource WaitControlTemplate}" /> <br/>
</Trigger > <br/>
</Style.Triggers > <br/>
</Style > <br/>
</ListBox.Style > <br/>
</ListBox >
注意-リストにはトリガーが使用されます。これにより、ユーザーのデータを取得するプロセスを視覚化できます。 これは非常に重要です-長時間の操作についてユーザーに通知しないと、リスト上のアクションが利用できないため、アプリケーションは単に機能しないと考えられます(
もちろん、状態が正しく処理されている場合は後で詳しく説明します)。
ステップ#4:アクションの可用性を処理することを忘れないでください
<Button Grid.Column = "1" <br/>
Content = "Edit" <br/>
Width = "70" <br/>
IsEnabled = "{Binding SelectedItem, ElementName=ItemsListBox, Converter={StaticResource NullToBooleanConverter}}" <br/>
Click = "EditButton_Click" /> <br/>
ステップ#5:実行中
すべての変更を加えた起動後のメインウィンドウは次のとおりです。
そして、これはデータが受信された後の外観です:
[
編集 ]ボタンは、単純なコンバーターを介して選択した要素に添付されます。 選択したアイテムが
ItemsListBoxのメインリストに
ない場合、ボタンは使用できません。 また、非同期データプロバイダーの
AsyncDataSourceがリストを要素で満たした後にのみ、要素を選択できます。 [
閉じる ]ボタンは、アプリケーションを制御する機能を視覚化するために追加されます-データを受信してメインウィンドウを閉じるプロセス中にクリックすることを妨げるものは何もありません。 同時に、アプリケーションはリクエストに正しく応答して終了します。これは、データソースが同期の場合には発生しませんでした。
ソリューション#2非同期バインディング
この問題の2番目の解決策は、
MV-VM(Model-View-ViewModel)パターンを使用します。これはおそらく、現在最も一般的なWPFおよびSilverlightのモジュールアプリケーション構築パターンの1つです。 このパターンの説明は、この記事の範囲を超えています-必要に応じて、ネットワーク上でそれに関する多くの情報を簡単に見つけることができます(
探すのが面倒すぎる場合は、記事の最後にあるリンクセクションをご覧
ください )。
ステップ#1:
メインアプリケーションウィンドウのビューモデルを作成します。
public class MainViewModel<br/>
{ <br/>
private ICommand _commandClose ; <br/>
<br/>
private ICommand _commandEdit ; <br/>
<br/>
private ReadOnlyCollection < string > _items ; <br/>
<br/>
public ReadOnlyCollection < string > Items<br/>
{ <br/>
get<br/>
{ <br/>
if ( _items == null ) <br/>
{ <br/>
_items = AsyncDataProvider. GetItems ( ) ; <br/>
} <br/>
<br/>
return _items ; <br/>
} <br/>
} <br/>
<br/>
public ICommand CommandClose<br/>
{ <br/>
get<br/>
{ <br/>
if ( _commandClose == null ) <br/>
{ <br/>
_commandClose = new RelayCommand ( p => OnClose ( ) ) ; <br/>
} <br/>
<br/>
return _commandClose ; <br/>
} <br/>
} <br/>
<br/>
public ICommand CommandEdit<br/>
{ <br/>
get<br/>
{ <br/>
if ( _commandEdit == null ) <br/>
{ <br/>
_commandEdit = new RelayCommand ( p => OnEdit ( p ) , p => CanEdit ) ; <br/>
} <br/>
<br/>
return _commandEdit ; <br/>
} <br/>
} <br/>
<br/>
public string SelectedItem<br/>
{ <br/>
get ; <br/>
set ; <br/>
} <br/>
<br/>
private void OnClose ( ) <br/>
{ <br/>
App. Current . Shutdown ( ) ; <br/>
} <br/>
<br/>
private void OnEdit ( object parameter ) <br/>
{ <br/>
MessageBox. Show ( String . Format ( "Edtiting item: {0}" , <br/>
parameter != null ? parameter. ToString ( ) : "Not selected" ) ) ; <br/>
} <br/>
<br/>
private bool CanEdit<br/>
{ <br/>
get<br/>
{ <br/>
return SelectedItem != null ; <br/>
} <br/>
} <br/>
}
ステップ#2:メインビューのXAMLコードでバインディング宣言をわずかに変更する
<ListBox x:Name = "ItemsListBox" <br/>
Grid.Row = "0" <br/>
ItemsSource = "{Binding Items, IsAsync=True}" <br/>
SelectedItem = "{Binding SelectedItem}" > <br/>
<ListBox.Style > <br/>
<Style TargetType = "{x:Type ListBox}" > <br/>
<Style.Triggers > <br/>
<Trigger Property = "ItemsSource" Value = "{x:Null}" > <br/>
<Setter Property = "Template" Value = "{StaticResource WaitControlTemplate}" /> <br/>
</Trigger > <br/>
</Style.Triggers > <br/>
</Style > <br/>
</ListBox.Style > <br/>
</ListBox >
このシナリオでは、バインディングマークアップで指定された属性が非同期を担当します:
"{Binding Items、IsAsync = True}" 。
ObjectDataProviderの例のよう
に 、WPFコアは個別のバックグラウンドディスパッチャーを作成して、個別の非同期コンテキストでバインディング値を取得します。
それとは別に、このシナリオでは、メインウィンドウビューのXAMLコードでコントロールを表示するためのルールをコーディングする必要はありません。 上記のビューモデルコードでは、
MainViewModel.CommandEditコマンドの一部である
MainViewModel.CanEditプロパティが、
フォームの [
編集 ]ボタンの表示を担当します。
コマンドセクションの詳細については、
リンクセクションをご覧ください。 ここでは、手動で何もする必要がないことに注意するのが適切です
。CommandManagerクラスが
すべてを処理し
ます 。 必要なのは、
RelayCommandクラスが
提供する
ICommandコントラクトの正しい実装だけです(添付プロジェクトでのこのクラスの実装に慣れることができます)。
宿題
自由に実行します-確認することも、尋ねることもしません:)すべてのルールに従って本格的なコントロールを作成する場合は、
Borderクラスまたはその祖先
Decoratorから継承される本格的なコントロールに変更することで、
WaitControlTemplateテンプレートをわずかに改善できます。 この要素の動作とロジックは簡単です:
- 要素内には、 ItemsControlまたはその子孫( ListBox、ListView、TreeViewなど )のみを追加できます-ContentプロパティがItemsControlでない場合、例外をスローするまで、最も厳密なレベルでこれを制御することが望ましい
- Contentプロパティが変更されると、コントロールはコンテンツをItemsControlにキャストし、 ItemsSourceプロパティのデータバインディング値を取得しようとします。
- 前のステップが成功した場合-バインディングを非同期モードにします
- コントロールの視覚化は、 プロキシパターンのロジックに部分的に基づいて行うことができます-コントロールが非同期にデータをロードしている間、コントロールはその内容(回転するロードインジケーターと待機を要求する碑文)を表示し、ロードの完了後、Contentプロパティのコンテンツが表示されます
まとめ
開発者の主な顧客はユーザーです。 また、各開発者は、単純なユーザーの代わりに自分自身を置き、アプリケーションをテストするフルサイクルを実行し、自分(ユーザーとして)に何が悩み、何を(ユーザーとして)改善したいのかを正確に分析しようとすると便利です。
ソースコード
AsyncBinding.zip
参照資料
プレゼンテーションのデザインモデルモデルビューモデルを備えたWPFアプリケーション
コマンドシステムの概要
Windows Presentation Foundationの非同期データバインディング
非同期WPF
トピックのメイン画像の元は
ここから取得さ
れます。