INotifyPropertyChanged вызывает межпотоковую ошибку
Вот мой сценарий:
У меня есть GridControl, связанный с BindingList. Сначала я создавал рабочий поток и получал прямой доступ к BindingList, но это вызывало "Обнаружена операция с несколькими потоками", поэтому я следовал приведенному здесь руководству:
http://www.devexpress.com/Support/Center/p/AK2981.aspx
Клонировав оригинальный BindingList в рабочий поток и изменив его, я получил желаемый эффект. Однако недавно я реализовал INotifyPropertyChanged в объект, который содержится в BindingList, и я снова начал получать ошибку.
Я предполагаю, что GridView все еще прослушивает INotifyPropertyChanged от объекта.
Как я могу это исправить?
Мои занятия:
public class Proxy : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
4 ответа
Если вы манипулируете пользовательским интерфейсом извне потока пользовательского интерфейса (например, из рабочего потока), вам необходимо присоединиться к потоку пользовательского интерфейса. Вы можете сделать это, позвонив Invoke
на контроле пользовательского интерфейса. Вы можете проверить, требуется ли это, используя InvokeRequired
,
Обычно используется шаблон:
public void ChangeText(string text)
{
if(this.InvokeRequired)
{
this.Invoke(new Action(() => ChangeText(text)));
}
else
{
label.Text = text;
}
}
В вашем случае пользовательский интерфейс манипулирует в результате INotifyPropertyChanged
, поэтому вам нужно убедиться, что вы либо всегда изменяете свою сущность в потоке пользовательского интерфейса (используя описанную выше технику), либо используете универсальный асинхронный помощник INotifyPropertyChanged. Это обертка вокруг привязываемого предмета. Он использует вышеуказанную технику, чтобы обеспечить ChangeProperty
событие запускается в потоке пользовательского интерфейса.
Вот очень грубый пример прокси для Entity
учебный класс. Это гарантирует, что событие изменения свойства присоединяется к потоку пользовательского интерфейса, и сохраняет саму сущность неизмененной. Очевидно, вы, вероятно, захотите реализовать это более широко, например, с помощью DynamicObject.
public class NotificationHelper : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly ISynchronizeInvoke invokeDelegate;
private readonly Entity entity;
public NotificationHelper(ISynchronizeInvoke invokeDelegate, Entity entity)
{
this.invokeDelegate = invokeDelegate;
this.entity = entity;
entity.PropertyChanged += OnPropertyChanged;
}
public string Name
{
get { return entity.Name; }
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
if (invokeDelegate.InvokeRequired)
{
invokeDelegate.Invoke(new PropertyChangedEventHandler(OnPropertyChanged),
new[] { sender, e });
return;
}
PropertyChanged(this, e);
}
}
}
Я применил аналогичный подход к возможному решению TheGateKeeper. Однако я был привязан ко многим различным объектам. Поэтому мне нужно что-то более общее. Решение состояло в том, чтобы создать оболочку, которая также реализовала ICustomTypeDescriptor. Таким образом, мне не нужно создавать свойства оболочки для всего, что может отображаться в пользовательском интерфейсе.
public class SynchronizedNotifyPropertyChanged<T> : INotifyPropertyChanged, ICustomTypeDescriptor
where T : INotifyPropertyChanged
{
private readonly T _source;
private readonly ISynchronizeInvoke _syncObject;
public SynchronizedNotifyPropertyChanged(T source, ISynchronizeInvoke syncObject)
{
_source = source;
_syncObject = syncObject;
_source.PropertyChanged += (sender, args) => OnPropertyChanged(args.PropertyName);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
var handler = PropertyChanged;
_syncObject.BeginInvoke(handler, new object[] { this, new PropertyChangedEventArgs(propertyName) });
}
public T Source { get { return _source; }}
#region ICustomTypeDescriptor
public AttributeCollection GetAttributes()
{
return new AttributeCollection(null);
}
public string GetClassName()
{
return TypeDescriptor.GetClassName(typeof(T));
}
public string GetComponentName()
{
return TypeDescriptor.GetComponentName(typeof (T));
}
public TypeConverter GetConverter()
{
return TypeDescriptor.GetConverter(typeof (T));
}
public EventDescriptor GetDefaultEvent()
{
return TypeDescriptor.GetDefaultEvent(typeof (T));
}
public PropertyDescriptor GetDefaultProperty()
{
return TypeDescriptor.GetDefaultProperty(typeof(T));
}
public object GetEditor(Type editorBaseType)
{
return TypeDescriptor.GetEditor(typeof (T), editorBaseType);
}
public EventDescriptorCollection GetEvents()
{
return TypeDescriptor.GetEvents(typeof(T));
}
public EventDescriptorCollection GetEvents(Attribute[] attributes)
{
return TypeDescriptor.GetEvents(typeof (T), attributes);
}
public PropertyDescriptorCollection GetProperties()
{
return TypeDescriptor.GetProperties(typeof (T));
}
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return TypeDescriptor.GetProperties(typeof(T), attributes);
}
public object GetPropertyOwner(PropertyDescriptor pd)
{
return _source;
}
#endregion ICustomTypeDescriptor
}
Затем в пользовательском интерфейсе я связываюсь с этой оболочкой, используя что-то вроде:
private void CreateBindings()
{
if (_model == null) return;
var threadSafeModel = new SynchronizedNotifyPropertyChanged<MyViewModel>(_model, this);
directiveLabel.DataBindings.Add("Text", threadSafeModel, "DirectiveText", false, DataSourceUpdateMode.OnPropertyChanged);
}
MyViewModel имеет свойство "DirectiveText" и реализует INotifyPropertyChanged без особого учета потоков или классов представлений.
Я подкласс BindingList
так что я мог проверить на необходимый Invoke
, Таким образом, мои бизнес-объекты не имеют ссылки на пользовательский интерфейс.
public class InvokingBindingList<T> : BindingList<T>
{
public InvokingBindingList(IList<T> list, Control control = null) : base(list)
{
this.Control = control;
}
public InvokingBindingList(Control control = null)
{
this.Control = control;
}
public Control Control { get; set; }
protected override void OnListChanged(ListChangedEventArgs e)
{
if (Control?.InvokeRequired == true)
Control.Invoke(new Action(() => base.OnListChanged(e)));
else
base.OnListChanged(e);
}
}
На всякий случай, если кто-то столкнулся с той же проблемой... Мне удалось исправить это через несколько часов. Вот что я сделал:
В основном проблема заключалась в том, что объект, реализующий INotifyPropertyChanged, жил в рабочем потоке, и это вызывает проблемы при доступе к потоку пользовательского интерфейса.
Поэтому я передал ссылку на объект, который необходимо обновить, на объект INotifyPropertyChanged, а затем использовал invoke для него.
Вот как это выглядит:
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
//If the Proxy object is living in a non-UI thread, use invoke
if (c != null)
{
c.BeginInvoke(new Action(() => handler(this, new PropertyChangedEventArgs(name))));
}
//Otherwise update directly
else
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
//Use this to reference the object on the UI thread when there is need to
public Control C
{
set { c = value; }
}
Из потока, все, что я сделал, было:
prox.c = this;
//Logic here
prox.c = null;
Надеюсь, это поможет кому-то!!