При использовании INotifyPropertyChanged
существует риск ошибок повторного входа, поскольку обработчик событий PropertyChanged
может прямо или косвенно вызвать метод отправителя. Поскольку типичная реализация вызывает эти события, как только свойство назначается, это является скрытым источником точек повторного входа во всех методах. Каждый раз, когда присваивается свойство, необходимо защищаться от повторного входа.
Кроме того, событие PropertyChanged
может раскрыть объект, когда он находится в состоянии, нарушающем один из инвариантов его класса. Например, инвариант класса утверждает, что если и только свойство A
истинно, то свойство B
будет ненулевым. Простой код для назначения A
, затем B
приведет к срабатыванию события изменения свойства, когда изменилось только одно. Затем обработчик событий будет наблюдать за объектом в этом состоянии, когда инвариант класса был нарушен, но еще не восстановлен.
Эти проблемы меньше беспокоят при реализации INotifyPropertyChanged
в моделях представлений. Обновление пользовательского интерфейса вряд ли будет включать код, который вызовет эти проблемы. Однако многие люди реализуют его и на моделях, потому что это значительно упрощает обновление пользовательского интерфейса. Там это больше беспокоит. Я вижу очень мало обсуждений этих вопросов в Интернете, даже когда ищу их. (Я нашел INotifyPropertyChanged и согласованность - когда поднимать PropertyChanged?)
Мой вопрос состоит из двух частей:
- При реализации
INotifyPropertyChanged
на объектах model следует ли защищаться от проблем с повторным входом и нарушением ограничений? Почему или почему нет? - Существует ли простой способ защиты от проблем с повторным входом и нарушением ограничений без использования шаблонов? (Я видел предложения по отсрочке вызова событий изменения, но это, по-видимому, требует либо всегда явно/вручную вызывать события изменения в конце метода, что может быть подвержено ошибкам, либо использовать фреймворк и обертывать каждое тело метода в оператор using, который откладывает события изменения внутри оператора using.)
1 ответ
Короткое и быстрое решение:
- Не используйте установщики свойств, которые поднимают
PropertyChanged
, вместо этого обновляйте состояние вашей модели, используя методыprivate
, которые обновляют состояние объекта атомарно (таким образом сохраняя инварианты и ограничения объекта) и только после этого поднимаютPropertyChanged
. - Однако вам нужно быть осторожным, чтобы избежать вызова установщиков свойств из вашего собственного кода. Это можно сделать, применив
[Obsolete]
или написав собственный Roslyn Analyzer.
Например.:
public class MyViewModel : INotifyPropertyChanged
{
public void DoSomethingThatUpdatesProperties()
{
// Update backing fields directly:
this.text1 = "foo";
this.text2 = "bar";
// Only raise events afterwards:
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( nameof(this.Text1) ) );
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( nameof(this.Text2) ) );
}
private String text1;
public String Text1
{
get => this.text1;
set
{
if( this.text1 != value )
{
this.text1 = value;
this.OnPropertyChanged( nameof(this.Text1) );
}
}
}
private String text2;
public String Text2
{
get => this.text2;
set
{
if( this.text2 != value )
{
this.text2 = value;
this.OnPropertyChanged( nameof(this.Text2) );
}
}
}
}
Еще одна идея: используйте пользовательский IDisposable
для управления откладыванием событий:
sealed class MutationMonitor : IDisposable
{
public MutationMonitor( Action onDispose )
{
this.onDispose = onDispose;
}
private readonly Action onDispose;
public void Dispose()
{
this.onDispose();
}
}
public class MyViewModel : INotifyPropertyChanged
{
private Boolean isMutating = false;
private readonly HashSet<String> changedProperties = new HashSet<String>();
public void DoSomethingThatUpdatesProperties()
{
using( this.BeginStateMutationMethod() )
{
this.Text1 = "foo";
this.Text2 = "bar";
}
}
private void MutationMonitor BeginStateMutationMethod()
{
this.isMutating = true;
return new MutationMonitor( this.EndStateMutationMethod );
}
private void EndStateMutationMethod()
{
this.isMutating = false;
foreach( String p in this.changedProperties ) this.raiseInpcEvent( p );
this.changedProperties.Clear();
}
private void OnPropertyChanged( String name )
{
if( this.isMutating )
{
this.changedProperties.Add( name );
}
else
{
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( name ) );
}
}
private String text1;
public String Text1
{
get => this.text1;
set
{
if( this.text1 != value )
{
this.text1 = value;
this.OnPropertyChanged( nameof(this.Text1) );
}
}
}
private String text2;
public String Text2
{
get => this.text2;
set
{
if( this.text2 != value )
{
this.text2 = value;
this.OnPropertyChanged( nameof(this.Text2) );
}
}
}
}
Идеалистическое решение: используйте Redux
При реализации
INotifyPropertyChanged
в объектах модели следует ли защищаться от проблем с повторным входом и нарушением ограничений? Почему или почему нет?
Я не уверен, что именно вы имеете в виду под термином "объект модели", но вкратце: да, именно по тем причинам, которые вы упомянули в своем посте, а также потому, что это может привести к к бесконечным циклам, когда два свойства объекта INotifyPropertyChanged
связаны друг с другом, и если происходит недетерминизм или где трудно определить равенство (например, с числами IEEE-754), вы, вероятно, окажетесь в рекурсивный бесконечный цикл.
Насколько я знаю, в WPF и UWP используются методы, позволяющие избежать нежелательных эффектов домкрата в одном и том же цикле диспетчеризации, но я не уверен в деталях (но именно поэтому вам нужно a Dispatcher
при модульном тестировании моделей представления)
Есть ли простой способ, без шаблонов, защититься от проблем с повторным входом и нарушением ограничений? (Я видел предложения по отсрочке вызова событий изменения, но это, по-видимому, требует либо всегда явно/вручную вызывать события изменения в конце метода, что может быть подвержено ошибкам, либо использовать фреймворк и обертывать каждое тело метода в оператор using, который откладывает события изменения внутри оператора using.)
Да: используя неизменяемое состояние с шаблоном Redux. Это приводит к одностороннему потоку данных, что значительно упрощает анализ состояния пользовательского интерфейса, а благодаря новым неизменяемым типам record
в C# 9.0 гораздо меньше утомительной работы по созданию экземпляров. следующее состояние.
Однако шаблон Redux не является панацеей: некоторые типы приложений плохо подходят для шаблона Redux: например, те, которые обрабатывают большие объемы данных с отслеживанием состояния (например, огромную электронную таблицу или точечную диаграмму с десятками тысяч точек данных). ) из-за влияния на производительность перераспределения и сопоставления неизменяемого состояния с изменяемыми компонентами пользовательского интерфейса (XAML/WPF). Но YMMV и есть некоторые удивительные оптимизации, которые можно сделать.
Вам не нужна библиотека Redux для реализации шаблона — я просмотрел несколько существующих библиотек Redux для WPF, и ни одна из них на самом деле не согласна со мной — или у них есть самоуверенные реализации, против которых я религиозно против — или когда это просто проще написать свой собственный.
Так что вместо того, чтобы делать это:
public class MyViewModel : INotifyPropertyChanged
{
private void OnPropertyChanged( String name )
{
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( name ) );
}
public event PropertyChangedEventHandler PropertyChanged;
private String text;
public String Text
{
get => this.text;
set
{
if( this.text != value )
{
this.text = value;
this.OnPropertyChanged( nameof(this.Text) );
}
}
}
}
У вас будет это:
- Обратите внимание, что это чрезвычайно упрощенный пример, в котором используется только один тип (
MyState
) для представления всего неизменяемого состояния. Это нереально, но сейчас у меня нет умственной энергии, чтобы привести более полный пример. - Кроме того, в этом примере действия Redux реализованы как методы для
MyState
(например,MyState.SetText
), а не как чистые функции сокращения перехода между состояниями, определенные централизованно.
class MyState
{
public MyState( String text )
{
this.Text = text;
}
public String Text { get; }
//
public MyState SetText( String newText )
{
return new MyState( newText ) ;
}
}
public class MyViewModel : INotifyPropertyChanged
{
private readonly MyReduxStore store;
private MyState state;
public event PropertyChangedEventHandler PropertyChanged;
public MyViewModel( MyReduxStore store )
{
this.store = store;
this.store.Subscribe( next => this.OnStateUpdated( next ) );
}
private void OnStateUpdated( MyState next )
{
MyState prev = this.state;
this.state = next;
// Call `PropertyChanged?.Invoke(...)` for all properties, if changed, here:
if( prev.Text = next.Text ) this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( nameof(this.Text) ) );
// etc
}
public String Text
{
get => this.state.Text;
set
{
this.store.Dispatch( new ReduxAction( state.SetText, value ) );
// Observe that `PropertyChanged` is NOT invoked here!
}
}
}
Но да, это утомительно, но использование таких инструментов, как T4, помогает уменьшить это.
Похожие вопросы
Связанные вопросы
Новые вопросы
c#
C# (произносится как «see Sharp») — это высокоуровневый мультипарадигменный язык программирования со статической типизацией, разработанный Microsoft. Код C# обычно нацелен на семейство инструментов и сред выполнения Microsoft .NET, которое включает в себя .NET, .NET Framework, .NET MAUI и Xamarin среди прочих. Используйте этот тег для ответов на вопросы о коде, написанном на C#, или о формальной спецификации C#.