落とし穴WPF

長い間WPFを使用してアプリケーションを開発している人は誰でも、このフレームワークが一見すると思えるほど使いやすいとはほど遠いことに気付いているでしょう。 この記事では、最も一般的な問題とその解決方法のいくつかを収集しようとしました。

  1. ResourceDictionaryのメモリ詰まりインスタンス
  2. メモリリーク
  3. 視覚的なコンポーネントとスタイルの継承
  4. バインドエラー
  5. 標準検証ツール
  6. PropertyChangedイベントの誤用
  7. Dispatcherの過剰使用
  8. モーダルダイアログ
  9. ディスプレイパフォーマンス分析
  10. そして、INotifyPropertyChangedについてもう少し
  11. あとがきの代わりに

ResourceDictionaryのメモリ詰まりインスタンス


多くの場合、開発者は、次のようなユーザーコントロールのXAMLマークアップに必要なリソースディクショナリを直接明示的に含めます。

<UserControl x:Class="SomeProject.SomeControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" <UserControl.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/Styles/General.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </UserControl.Resources> 

一見したところ、このアプローチには問題はありません-制御要素に対して最小限必要なスタイルのセットを指定するだけです。 アプリケーションで、SomeControlがいずれかのウィンドウの10個のインスタンスに存在するとします。 問題は、これらの各インスタンスを作成するときに、指定されたディクショナリが再読み取りされ、処理され、メモリ内の個別のコピーとして保存されることです。 プラグイン辞書が多いほど、インスタンスが多くなります-それらを含むビューを初期化するのにより多くの時間がかかり、より多くのメモリが無駄になります。 実際には、不要なResourceDictionaryによるメモリオーバーランが約200メガバイトであるアプリケーションを処理する必要がありました。

この問題を解決するための2つのオプションを知っています。 1つ目は、App.xamlでのみ必要なすべてのスタイル辞書を接続し、他の場所には接続しないことです。 小さなアプリケーションには適しているかもしれませんが、複雑なプロジェクトでは受け入れられないかもしれません。 2番目の方法は、標準のResourceDictionaryの代わりに後続のものを使用することです。これにより、各インスタンスが1つのインスタンスのみのメモリに格納されるように辞書がキャッシュされます。 残念ながら、WPFは何らかの理由でそのような機会を提供していませんが、自分で簡単に実装できます。 最も完全なソリューションの1つは、最後の回答( http://stackoverflow.com/questions/6857355/memory-leak-when-using-sharedresourcedictionary)にあります。

メモリリーク


イベントリーク


自動ガベージコレクション環境でも、メモリリークは簡単に取得できます。 リークの最も一般的な原因は、WPFプロジェクトだけでなく、その後ハンドラを削除しないイベントサブスクリプションです。 これはテクノロジー自体の問題ではありませんが、イベントは多くの場合WPFプロジェクトで使用され、エラーの可能性が高いため、さらに詳しく説明する価値があります。

たとえば、アプリケーションには、編集ウィンドウでプロパティを変更できるオブジェクトのリストがあります。 このウィンドウを実装するには、編集されたオブジェクトのプロパティを変更するときに、ビューモデル内でIsModifiedをtrueに設定する必要がありました。

編集用のビューモデルが次のように実装されているとします。

 public class EntityEditorViewModel { //... public EntityEditorViewModel(EntityViewModel entity) { Entity = entity; Entity.PropertyChanged += (s, e) => IsModified = true; } } 

ここで、コンストラクターはビジネスエンティティとエディターのプレゼンテーションモデルの間に「強力な」リンクを確立します。 ウィンドウが表示されるたびにEntityEditorViewModelのインスタンスを作成すると、そのようなオブジェクトはメモリに蓄積され、それらを参照するビジネスエンティティがゴミになった場合にのみ削除されます。

この問題の1つの解決策は、ハンドラーを削除することです。 たとえば、IDisposableを実装し、Dispose()メソッドでイベントから「サブスクライブ解除」します。 しかし、ここで、例のようにラムダ式で指定されたハンドラーを簡単な方法で削除することはできません。つまり、 これは機能しません:

 //     ! entity.PropertyChanged -= (s, e) => IsModified = true; 

問題の正しい解決策として、C#でラムダ式が現れる前に常に行われていたように、別のメソッドを宣言し、IsModifiedインストールをその中に配置してハンドラーとして使用する必要があります。

ただし、明示的な削除を使用する方法では、メモリリークがないことを保証しません。Dispose()の呼び出しを忘れることができます。 さらに、いつ呼び出すかを決定することは非常に問題になる可能性があります。 または、より扱いにくいが効果的なアプローチである弱いイベントを検討することもできます。 実装の一般的な考え方は、イベントソースとサブスクライバの間に「弱い」リンクが確立され、それへの「強力な」リンクがなくなったときにサブスクライバを自動的に削除できることです。

Weak Eventsパターンの実装の説明はこの記事の範囲を超えているため、このトピックが詳細に議論されているリンク( http://www.codeproject.com/Articles/29922/Weak-Events-in-C)を指摘します

バインディングリーク


上記の潜在的な問題に加えて、WPFには、このテクノロジに固有の少なくとも2種類のリークがあります。

単純なオブジェクトがあるとします:

 public class SomeModelEntity { public string Name { get; set; } } 

そして、任意のコントロールからこのプロパティにアタッチされます:

 <TextBlock Text="{Binding Entity.Name, Mode=OneWay}" /> 

バインド先のプロパティがDependencyPropertyではない場合、またはそれを含むオブジェクトがINotifyPropertyChangedを実装していない場合、バインドメカニズムはSystem.ComponentModel.PropertyDescriptorクラスのValueChangedイベントを使用して変更を追跡します。 ここでの問題は、フレームワークがPropertyDescriptorのインスタンスへの参照を保持しており、そのインスタンスがソースオブジェクトを参照しており、このインスタンスをいつ削除できるかが明確でないことです。 OneTimeバインディングの場合、変更を追跡する必要がないため、問題は関係ないことに注意してください。

この問題に関する情報は、マイクロソフトサポート技術情報( https://support.microsoft.com/en-us/kb/938416 )にも記載されていますが、リークに関する1つの追加条件を示しています。 前の例に適用すると、SomeModelEntityインスタンスは、リークが発生するために直接または間接的にTextBoxを参照する必要があります。 一方で、この条件は実際にはめったに満たされませんが、実際には「よりクリーンな」アプローチに従う方が常に良いです-変更を監視する必要がない場合はOneTimeバインディングモードを明示的に示すか、ソースオブジェクトにINotifyPropertyChangedを実装するか、DependencyPropertyプロパティを設定します(視覚コンポーネントのプロパティにとって意味があります)。

バインダーのインストール時に発生する可能性のある別の問題は、INotifyCollectionChangedインターフェイスを実装しないコレクションにバインドすることです。 この場合の漏れのメカニズムは、前のものと非常に似ています。 これに対処する方法は明らかです。OneTimeバインディングモードを明示的に指定するか、またはINotifyCollectionChangedを実装するコレクション(ObservableCollectionなど)を使用する必要があります。

視覚的なコンポーネントとスタイルの継承


機能を拡張し、動作を変更するために、標準コントロールの継承が必要になる場合があります。 一見したところ、これは基本的なものです。

 public class CustomComboBox : ComboBox { //… } 

しかし、アプリケーションがデフォルトのスタイル以外の要素スタイルを使用している場合、そのような継承を使用する問題はすぐに顕著になります。 次のスクリーンショットの断片は、PresentationFramework.Aeroテーマが有効な場合のベースコントロールと派生物の表示の違いを示しています。



これを修正する最も簡単な方法は、テーマリソースを含めた後、XAMLファイルのベース要素から継承される派生要素のスタイルを定義することです。 これは、BasedOn属性を使用して簡単に実行できます。

 <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/PresentationFramework.Aero;component/themes/Aero.NormalColor.xaml" /> </ResourceDictionary.MergedDictionaries> <Style TargetType="{x:Type my:CustomComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}"> </Style> </ResourceDictionary> </Application.Resources> 


しかし、派生コントロールを使用する場合、リソースにスタイルを追加することを常に覚えておく必要があることがわかりました。 または、この派生スタイルでファイルを作成し、新しい要素を使用する必要があるたびに添付します。

XAMLを変更せずに行う方法が1つあります-派生要素のコンストラクターで、ベース要素から取得したスタイルを明示的に設定します。

 public CustomComboBox() { SetResourceReference(StyleProperty, typeof(ComboBox)); } 

したがって、基本スタイルに変更を追加する必要がない場合、この方法が最適です。 それ以外の場合は、前のオプションを使用することをお勧めします。

バインドエラー


もちろん、コントロールをモデルのフィールドと宣言的にリンクすることには利点がありますが、その整合性を監視するのはそれほど簡単ではありません。 何らかの理由でバインディングで示されたプロパティが見つからない場合、エラーはデバッグログに書き込まれます...そしてそれだけです。 デフォルトでは、ユーザーにはメッセージは表示されませんが、デバッグなしで起動すると、これらのエラーはログに記録されません。

開発者がこのようなエラーをより目立つようにするために、メッセージの形式でエラーを表示する特別なトレースリスナーを作成できます。

 public class BindingErrorTraceListener : TraceListener { private readonly StringBuilder _messageBuilder = new StringBuilder(); public override void Write(string message) { _messageBuilder.Append(message); } public override void WriteLine(string message) { Write(message); MessageBox.Show(_messageBuilder.ToString(), "Binding error", MessageBoxButton.OK, MessageBoxImage.Warning); _messageBuilder.Clear(); } } 

そして、アプリケーションの起動時にアクティブにします。

 PresentationTraceSources.DataBindingSource.Listeners.Add(new BindingErrorTraceListener()); PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Error; 

これらの変更後、各バインディングエラーはダイアログメッセージとして表示されますが、 デバッグ開始する場合のみです。そのため、ハンドラーがリリースバージョンに登録されないように条件付きコンパイルを使用するのが理にかなっています。

標準検証ツール


WPFでデータを検証する方法はいくつかあります。

ValidationRule-このクラスを継承し、XAMLマークアップのフィールドにバインドされる特別な検証ルールを作成できます。 「条件付き」利点-検証を実行するためにモデルクラスを変更する必要はありませんが、場合によってはこれが最良のオプションではない可能性があります。 しかし、同時に重大な欠点があります-ValidationRuleはDependencyObjectを継承しないため、相続人には後でバインドできるプロパティを作成する方法がありません。 これは、たとえば、一方の値を他方の値より大きくすることができない場合など、プロパティを相互に検証するための単純で明白な方法がないことを意味します。 この方法で実装された検証ルールは、このルールのインスタンスを作成するときに指定された現在のフィールド値と固定プロパティ値のみを処理できます。

IDataErrorInfo、INotifyDataErrorInfo-これらのインターフェイスをプレゼンテーションモデルのクラスに実装すると、個々のプロパティといくつかのプロパティの両方を簡単に検証できます。 通常、コードの量を減らすために、これらのインターフェイスの1つはモデルの基本クラスに実装され、相続人のルールを簡潔に説明する手段を提供します。 たとえば、各タイプの静的コンストラクターにルールを登録することにより:

 static SomeModelEntity() { RegisterValidator(me => me.Name, me => !string.IsNullOrWhiteSpace(me.Name), Resources.RequiredFieldMessage); } 

または、属性を通じて:

 [Required] public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged(); } } 

2番目のオプションの説明は、 http://www.codeproject.com/Articles/97564/Attributes-based-Validation-in-a-WPF-MVVM-Applicatで確認できます

しかし、DataErrorInfoインターフェースを使用するアプローチは、すべての検証タスクをカバーしていません-検証されたエンティティの外部のオブジェクトにアクセスする必要があるルールをチェックする場合、問題が発生し始めます。 たとえば、一意性をチェックするには、オブジェクトの完全なコレクションにアクセスする必要があります。つまり、そのようなコレクションの各要素にリンクを設定する必要があり、オブジェクトの処理が非常に複雑になります。

残念ながら、WPFにはこの問題を簡単に回避するための標準ツールがないため、独自のツールを作成する必要があります。 最も単純なケースでは、保存する前にレコードの一意性を確認する必要がある場合、保存を呼び出す前にコードで明示的にこれを実行し、エラーの場合にメッセージを表示できます。

実際、このアプローチは一般化することもできます。 静的コンストラクターにバリデーターを登録する際に、上記のアイデアを使用します。 基本クラスの例を次に示します。

 public class ValidatableEntity<TEntity> : IDataErrorInfo { //  ""  protected static void RegisterValidator<TProperty>( Expression<Func<TProperty>> property, Func<TEntity, bool> validate, string message) { //... } // ,         - ,     protected static void RegisterValidatorWithState<TProperty>( Expression<Func<TProperty>> property, Func<TEntity, object, bool> validate, string message) { //... } public bool Validate(object state, out IEnumerable<string> errors) { //        .  ,   RegisterValidatorWithState,   state    . } // IDataErrorInfo,   ,   RegisterValidator } 

また、使用例:

 public class SomeModelEntity : ValidatableEntity<SomeModelEntity> { public string Name { get; set; } static SomeModelEntity() { RegisterValidator(me => me.Name, me => !string.IsNullOrWhiteSpace(me.Name), Resources.RequiredFieldMessage); RegisterValidatorWithState(me => me.Name, (me, all) => ((IEnumerable<SomeEntity>)all).Any(e => e.Name == me.Name), Resources.UniqueNameMessage); } } 

したがって、すべての検証ルールはエンティティ自体の中にあります。 「外部」オブジェクトを必要としないものは、基本クラスからのIDataErrorInfoの実装で使用されます。 残りの部分を確認するには、適切な場所でValidate関数を呼び出し、その結果を使用してさらにアクションを決定するだけで十分です。

PropertyChangedイベントの誤用


私はWPFプロジェクトでこの種のコードに頻繁に会わなければなりませんでした。

 private void someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Quantity") { //- ,  ,   ,    } } 

そして多くの場合、それは独自のイベントのハンドラーでした。 宣言された同じクラスのプロパティの変更を「リスニング」しました。

このアプローチにはいくつかの重大な欠点があり、その結果、サポートと拡張の点で非常に難しいコードになります。 それらのいくつかは明らかです。たとえば、プロパティの名前を変更するとき、条件の定数を変更するのを忘れることができますが、これはマイナーで簡単に解決できる欠点です。 さらに深刻な問題は、このアプローチでは、特定のロジックが実行されるすべてのシナリオを追跡することがほぼ不可能であることです。

PropertyChangedイベントハンドラーが正しく使用されているかどうかを自己チェックするための次の基準を定式化できます。ハンドラー内のアルゴリズムが特定のプロパティ名に依存しない場合、すべてが正常です。 そうでなければ、より良い解決策を探す必要があります。 正しいアプリケーションの例は、たとえば、ビューモデルのプロパティを変更するときにIsModifiedプロパティをtrueに設定することです。

Dispatcherの過剰使用


WPFプロジェクトで繰り返し、UIスレッドでの操作の強制的な実行に遭遇しました。これが必要でない場合でもです。 問題の規模を説明するために、Core i7-3630QM 2.4GHzプロセッサを搭載したラップトップで簡単なテストを使用して取得した数値をいくつか示します。


最初の数字は怖く見えませんが、コードがUIスレッドで実行されることがわかっている場合、Dispatcherを介して何かを呼び出すことも間違っています。 しかし、2番目の図はすでに顕著に見えます。 特に複数の並列スレッドからディスパッチする場合、実際の複雑なアプリケーションでは、この時間がはるかに長くなる可能性があることに注意してください。 そして、より弱いデバイスで-さらに。

生産性への害を減らすには、簡単なルールを守るだけで十分です。



モーダルダイアログ


WPFプロジェクトで標準のモーダルメッセージ(MessageBox)を使用することは歓迎されません。アプリケーションの視覚スタイルに従って外観をカスタマイズすることは単に不可能だからです。 標準メッセージの代わりに、独自の実装を作成する必要があります。これは、条件付きで2つのタイプに分けることができます。


それぞれのアプローチには長所と短所があります。 最初のオプションは実装が簡単ですが、モーダルダイアログが表示されるウィンドウのコンテンツ全体を「薄暗くする」などの効果を実現することはできません。 一部のアプリケーションでは、非標準形式のダイアログが必要になる場合がありますが、これは通常のウィンドウでは簡単ではありません。

通常、2番目のオプションは実装で多くの問題を引き起こします。これは、このようなウィンドウの表示を同期できないという事実が原因です。 つまり、通常のメッセージのように書くことはできません。

 if (MessageBox.Show(Resources.ResetSettingsQuestion, Resources.ResetSettings, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { 

ユーザーが質問に答えるまで、メインスレッドで他に何も起こらないことを期待します。

「エミュレートされた」ダイアログの最も単純な実装の1つを検討してください。

まず、プレゼンテーションモデルがダイアログを表示するためのダイアログマネージャーインターフェイスを宣言します。 そもそも、ウィンドウから「応答」を受信する可能性を考慮せず、ダイアログを閉じるボタンで表示するだけです。

 public interface IModalDialogHelper { public string Text { get; } ICommand CloseCommand { get; } void Show(string text); void Close(); } 

次に、必要に応じて、マネージャーに「スナップ」し、残りの要素の上にウィンドウを表示するコントロールを実装します。

 <UserControl x:Class="TestDialog.ModalDialog" 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" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Panel.ZIndex="1000"> <UserControl.Style> <Style TargetType="{x:Type UserControl}"> <Setter Property="Visibility" Value="Collapsed" /> <Style.Triggers> <DataTrigger Binding="{Binding DialogHelper.IsVisible}" Value="True"> <Setter Property="Visibility" Value="Visible" /> </DataTrigger> </Style.Triggers> </Style> </UserControl.Style> <Grid> <Border HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="DarkGray" Opacity=".7" /> <Grid HorizontalAlignment="Stretch" Height="200" Background="AliceBlue"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="{Binding DialogHelper.Text}" /> <Button Grid.Row="1" Content="Close" Command="{Binding DialogHelper.CloseCommand}" HorizontalAlignment="Right" /> </Grid> </Grid> </UserControl> 

繰り返しますが、単純化するために、このコントロールは、ビューモデルのDialogHelperプロパティにIModalDialogHelper実装のインスタンスが確実に含まれるように設計されています。より普遍的なソリューションでは、任意のプロパティを代用することが可能であるべきです。

ここではIModalDialogHelperの最も単純な実装の例を示しません。これは明らかなので、Show()およびClose()メソッドはIsVisibleを設定し、CloseCommandコマンドは単にClose()メソッドを呼び出します。 Show()はTextプロパティを設定します。

すべてがシンプルなようです。目的のメッセージテキストを使用してShow()メソッドを呼び出し、メッセージとボタンが表示されたパネルを表示し、閉じるボタンをクリックするとIsVisibleが元の値に設定され、「ダイアログ」が画面から消えます。しかし、すでに最初の問題があります-Show()メソッドは前のダイアログを閉じることを期待していないため、いくつかのメッセージを順番に表示すると、ユーザーには最後のメッセージのみが表示されます。

この問題を解決するために、Showメソッドのプロトタイプをわずかに変更します。

 Task Show(string text); 

このメソッドがawaitで完了するのを待つことができると、ラインストーンにいくつかの利点があります。


ここで、上記のポイントに対応する非同期表示を使用してIModalDialogHelperインターフェイスを実装するためのオプションの1つを提供します(この実装では、同じモーダル結果が常に返されますが、押されたボタンに依存することは難しくありません)。

 class ModalDialogHelper : INotifyPropertyChanged, IModalDialogHelper { private readonly Queue<TaskCompletionSource<MessageBoxResult>> _waits = new Queue<TaskCompletionSource<MessageBoxResult>>(); private readonly object syncObject = new object(); private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher; //... public async Task Show(string text) { List<TaskCompletionSource<MessageBoxResult>> previousWaits; TaskCompletionSource<MessageBoxResult> currentWait; lock (syncObject) { //  ,    previousWaits = _waits.ToList(); //      currentWait = new TaskCompletionSource<MessageBoxResult>(); _waits.Enqueue(currentWait); } //  ,      foreach (var wait in previousWaits) { await wait.Task; } //        _dispatcher.Invoke(() => { Text = text; IsVisible = true; }); await currentWait.Task; } public void Close() { IsVisible = false; TaskCompletionSource<MessageBoxResult> wait; lock (syncObject) { //     wait = _waits.Dequeue(); } //            wait.SetResult(MessageBoxResult.OK); } //... } 

このソリューションの主なアイデアは、Show呼び出しごとにTaskCompletionSourceのインスタンスが作成されることです。内部で作成されたタスクを待つことは、結果がSetResultの呼び出しを通じて指定されるまで続きます。 Showは、メッセージを表示する前に、すでにキューにあるすべてのタスクを待機し、showの後、独自のタスクを待機し、Closeで現在のタスクの結果を設定して完了します。

また、CancelEventHandlerのようなイベントハンドラでの「新しい」ダイアログの使用について、いくつかの言葉を言う必要があります。このようなイベントでのアクションの確認も、以前とは少し異なる方法で実装する必要があります。

 //     ! private async void Window_Closing(object sender, CancelEventArgs e) { e.Cancel = true; if(await dialogHelper.Show("Do you really want to close the window", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { e.Cancel = false; } } 

問題は、awaitはフローを停止せず、非同期タスクの完了後にメソッド内の適切な場所に「戻る」機能を作成するため、Window_Closingを呼び出したコードに対してe.Cancelが常にtrueになることです。呼び出しコードの場合、e.Cancelをtrueに設定すると、Windows_Closingはすぐに終了します。

正しい解決策は、条件本体がe.Cancelで動作しないようにすることです。ただし、このハンドラーの繰り返しの呼び出しをバイパスして、追加要求なしで実行されることが保証されるように「キャンセル」アクションを明示的に呼び出します。たとえば、プログラムのメインウィンドウが閉じている場合、アプリケーション全体を終了する明示的な呼び出しである可能性があります。

ディスプレイパフォーマンス分析


多くの開発者は、「プロファイラー」が何であるかを知っており、アプリケーションのパフォーマンスを分析し、メモリ消費を分析するために利用可能なツールを知っています。しかし、WPFアプリケーションでは、プロセッサの負荷の一部は、たとえばXAMLマークアップ処理メカニズム(解析、マークアップ、描画)から生じます。 「標準の」プロファイラーが、どのXAML関連のアクティビティリソースが消費されているかを正確に判断することは容易ではありません。

既存のツールの機能については説明せず、単にそれらに関する情報へのリンクをリストします。それらの使用方法を見つけることは、どの開発者にとっても簡単です。



INotifyPropertyChanged


WPFテクノロジで最も人気のある討論トピックの1つは、INotifyPropertyChangedを最も合理的に実装する方法です。Aspect Injectorに関する記事の 1つの例で既に説明したように、最も簡潔なオプションはAOPを使用することです。しかし、誰もがこのアプローチを好むわけではなく、スニペットを代替手段として使用できます。しかし、ここでは、最適なスニペットコンテンツの問題が発生します。最初に、最も成功したオプションではない例を示します。

 private string _name; public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged("Name"); } } 

この場合、プロパティの名前は定数で示され、名前付き定数であるか、例のように通知メソッドの呼び出しで直接ハードコードされているかどうかは関係ありません。問題は同じままです。プロパティ自体の名前を変更すると、定数の古い値を残す可能性があります 多くの人がNotifyPropertyChangedメソッドを変更することでこの問題を解決しています:

 public void NotifyPropertyChanged<T>(Expression<Func<T>> property) { var handler = PropertyChanged; if(handler != null) { string propertyName = ((MemberExpression)property.Body).Member.Name; handler(this, new PropertyChangedEventArgs(propertyName)); } } 

この場合、名前の代わりに、目的のプロパティを返すラムダ式を指定できます。

 NotifyPropertyChanged(() => Name); 

残念ながら、このオプションには欠点もあります。このメソッドの呼び出しは常にReflectionに関連付けられます。これは、以前のNotifyPropertyChangedオプションを呼び出すよりも数百倍遅いです。モバイルアプリケーションの場合、これは重要です。

.NET 4.5では、特別な属性CallerMemberNameAttributeが使用可能になりました。これにより、上記の問題の最初の問題を解決できます。

 public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { //... } public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged(); } } 

この属性でマークされたパラメーターが明示的に指定されていない場合、コンパイラーはメソッドを呼び出すクラスメンバーの名前に置き換えます。したがって、上記の例からNotifyPropertyChanged()を呼び出すことは、NotifyPropertyChanged(“ Name”)と同等です。しかし、プロパティの変更をセッターからではなく「外部」に報告する必要がある場合はどうでしょうか。

たとえば、「計算された」プロパティがあります。

 public int TotalPrice { get { return items.Sum(i => i.Price); } } 

アイテムコレクションのアイテムを追加、削除、または変更する場合、ユーザーインターフェイスに常に現在の値が表示されるように、TotalPriceに変更を報告する必要があります。上記の最初の2つのソリューションの欠点を考えると、次の動きをすることができます-まだReflectionを使用してラムダ式からプロパティ名を取得しますが、静的変数に保存します。したがって、個々のプロパティごとに、「重い」操作は1回だけ実行されます。

 public class ResultsViewModel : INotifyPropertyChanged { public static readonly string TotalPricePropertyName = ExpressionUtils.GetPropertyName<ResultsViewModel>(m => m.TotalPrice); //... NotifyPropertyChanged(TotalPricePropertyName); //... } public static class ExpressionUtils { public static string GetPropertyName<TEntity>(Expression<Func<TEntity, object>> property) { var convertExpression = property.Body as UnaryExpression; if(convertExpression != null) { return ((MemberExpression)convertExpression.Operand).Member.Name; } return ((MemberExpression)property.Body).Member.Name; } } 

静的関数GetPropertyName自体も、すべての「通知可能な」エンティティの基本クラスに入れることができます-これは重要ではありません。関数が通常、重要なタイプのプロパティを処理するために、UnaryExpressionのチェックが必要です。コンパイラはボクシング操作を追加して、指定されたプロパティをオブジェクトにキャストします。

プロジェクトですでにC#6.0を使用している場合、別のプロパティの名前を取得する同じタスクを、キーワードnameofを使用してはるかに簡単に解決できます。名前を記憶する静的変数はもう必要ありません。

その結果、何らかの理由でINotifyPropertyChangedにAOPを使用するのが適切でない場合、次のコンテンツのスニペットを使用できると言えます。



あとがきの代わりに


WPFは、マイクロソフトが「デスクトップ」アプリケーションを開発するための主要なフレームワークとして位置付け続けている優れたテクノロジーです。残念ながら、「計算機」よりも複雑なプログラムを作成すると、一見して目立たない多くの問題が発見されますが、それらはすべて解決されます。マイクロソフトによる最近の声明によると、彼らはテクノロジーの開発に投資しており、新しいバージョンにはすでに多くの改善があります。主にツールとパフォーマンスに関連しています。将来、ツールだけでなくフレームワーク自体にも新しい機能が追加され、プログラマの作業が容易になり、今やらなければならない「ハック」や「自転車」がなくなることを願っています。

UPD:「ビジュアルコンポーネントとスタイルの継承」セクションの2番目のソリューションをコメントからより最適なものに変更し、C#6.0

UPD2の nameof()からINotifyPropertyChangedのセクションにソリューションを追加しました

Source: https://habr.com/ru/post/J262299/


All Articles