ViewModel, Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток
В общем есть публичный класс RootViewModel наследуемый интерфейс INotifyPropertyChanged, с публичной коллекцией :
private ObservableCollection<string> m_Logs;
public ObservableCollection<string> Logs
{
get
{
return m_Logs;
}
set
{
m_Logs = value;
RaisePropertyChanged(nameof(Logs));
}
}
Забинден в xaml к элементу ListView
<ListView ItemsSource="{Binding Path=Logs, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" BorderThickness="1" FontSize="11"/>
Собственно, если обращаться к коллекции из синхронного метода, то все хорошо, как только запускаю в Task, сразу выскакивает ошибка "Вызывающий поток не может получить доступ к данному....." Дело в том, что хочу запустить сокет-сервер таким кодом:
public async void RunServer()
{
// получаем адреса для запуска сокета
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port);
// создаем сокет
Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await Task.Run(() =>
{
// связываем сокет с локальной точкой, по которой будем принимать данные
listenSocket.Bind(ipPoint);
// начинаем прослушивание
listenSocket.Listen(10);
App.ViewModel.Logs.Add("Сервер запущен. Ожидание подключений...");
while (true)
{
Socket handler = listenSocket.Accept();
// получаем сообщение
StringBuilder builder = new StringBuilder();
int bytes = 0; // количество полученных байтов
byte[] data = new byte[256]; // буфер для получаемых данных
do
{
bytes = handler.Receive(data);
builder.Append(Encoding.Unicode.GetString(data, 0, bytes));
}
while (handler.Available > 0);
Console.WriteLine(DateTime.Now.ToShortTimeString() + ": " + builder.ToString());
// отправляем ответ
string message = "ваше сообщение доставлено";
data = Encoding.Unicode.GetBytes(message);
handler.Send(data);
// закрываем сокет
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
});
try
{
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Но в данном случае при обращении к коллекции "App.ViewModel.Logs.Add("Сервер запущен. Ожидание подключений..."); " Вылетает ошибка, если же запустить метод синхронно, то оператор while не дает запуститься основной программе и интерфейс не загружается.
Так же пробовал обращаться к коллекции через Dispatcher
Dispatcher.CurrentDispatcher.Invoke(() => App.ViewModel.Logs.Add("Сервер запущен. Ожидание подключений..."));
Ошибка не исчезает, что я делаю не так? ") Или может как то по другому сделать? ")
Ответы (2 шт):
Проблема решилась следующим путем, для удобства использовал статический метод:
class Logs
{
public static void Add(string message)
{
App.Current.Dispatcher.BeginInvoke((Action)delegate ()
{
App.ViewModel.Logs.Add(message);
});
}
}
Таким образом диспетчер помещает операцию в связанный с ней поток. Следовательно, элемент всегда будет добавлен в поток пользовательского интерфейса.
App.Current.Dispatcher.BeginInvoke из вашего предыдущего ответа использовать нельзя, так как вы можете просто повредить данные, если вызовете его повторно до того как предыдущий вызов завершится.
Я не знаю точно, почему так происходит, ведь вызов делегата фактически произойдет внутри UI потока, и можно считать его потокобезопасным, но ObservableCollection не так проста как кажется. Помимо добавления элемента в коллекцию, для этого элемента вызывается событие CollectionChanged, чтобы интерфейс мог отследить изменения и обновиться, и здесь кроются сюрпризы, типа вы закидываете одновременно 2 элемента в коллекцию, а собыития вызываются не в том порядке, в котором элементы были добавлены в коллекцию, и в результате получаются неверные данные в интерфейсе.
Чтобы обезопасить себя от подобных сюрпризов, нужно вызвать делегат с блокиованием текущего потока, пока добавление элемента в коллекцию не завершится полностью. Это не занимает много времени, поэтому можно просто подождать:
App.Current.Dispatcher.Invoke(() =>
{
App.ViewModel.Logs.Add(message);
});
Но данный способ не иделаен, представьте себе, у вас 3-4 коллекции, и вы их модифицируете в разных местах кода. И каждый вызов вам придется оборачивать в делегат, и закидывать в Dispatcher.
Есть способ лучше, использовать вот такой потокобезопасный вариант коллекции:
AsyncObservableCollection.cs
public class AsyncObservableCollection<T> : ObservableCollection<T>
{
private readonly SynchronizationContext _context = SynchronizationContext.Current;
private void ExecuteOnSyncContext(Action action)
{
if (_context.Equals(SynchronizationContext.Current))
action();
else
_context.Send(_ => action(), null);
}
protected override void InsertItem(int index, T item) => ExecuteOnSyncContext(() => base.InsertItem(index, item));
protected override void RemoveItem(int index) => ExecuteOnSyncContext(() => base.RemoveItem(index));
protected override void SetItem(int index, T item) => ExecuteOnSyncContext(() => base.SetItem(index, item));
protected override void MoveItem(int oldIndex, int newIndex) => ExecuteOnSyncContext(() => base.MoveItem(oldIndex, newIndex));
protected override void ClearItems() => ExecuteOnSyncContext(() => base.ClearItems());
}
Потокобезопасность проверена, гонял ее на тестах одновременно в 50 потоках с непрерывными модификациями, данные уцелели. Знаю, название грубое, так как здесь асинхронности как таковой нет, но зато коллекцию можно использовать в асинхронном коде и не задумываться о ручной синхронизации потоков при ее модификации.
Класс просто наследует ObservableCollection, и переопределяет в ней вирутальные методы сверяя контекст синхронизации. Класс кросс-платфроменный, то есть будет работать и в Linux, и в Xamarin, но для WPF в Windows под капотом там тоже где-то зашит Dispatcher.Invoke(). В коде это _context.Send().
Чтобы добиться желаемого результата, экземпляр данной коллекции обязательно надо создавать в основном UI потоке, так как при создании он захватывает текущий SynchronizationContext, и в последствии использует его для перенаправления вызовов для модицикации коллекции.
Код написан таким образом, что если вы создадите эту коллекцию не в UI потоке, без контекста синхронизации, вы получите исключение NullReferenceException в строке if (_context.Equals(SynchronizationContext.Current)) при попытке ее модификации. Обработку ошибок я не добавлял сюда специально, чтобы не ухудшать производительность класса, и чтобы ошибки использования было видно сразу.
Использование AsyncObservableCollection предельно простое
private AsyncObservableCollection<string> m_Logs;
public AsyncObservableCollection<string> Logs
{
get
{
return m_Logs;
}
set
{
m_Logs = value;
RaisePropertyChanged(nameof(Logs));
}
}
Logs = new AsyncObservableCollection<string>();
И далее, просто из любого потока, без вызова диспетчера, напрямую.
App.ViewModel.Logs.Add(message);