Простая задача: Обновить возможность выполнения кнопки по событию.
Пишу такой элементарный код при помощи MVVM Toolkit:
public partial class MainViewModel : ObservableObject
{
public MainViewModel()
{
_ = Task.Run(async () =>
{
try
{
while (true)
{
await Task.Delay(1000);
CanTest = !CanTest;
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
});
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(TestCommand))]
private bool _canTest;
[RelayCommand(CanExecute = nameof(CanTest))]
public void Test()
{
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
Как видно, тут создается простой класс, который фоном крутит некую задачу, и раз в секунду обновляет состояние bool свойства, по которому идет обновление CanExecute команды.
Для наглядности, что этот код сгенерирует:
public bool CanTest
{
get => _canTest;
set
{
if (!EqualityComparer<bool>.Default.Equals(_canTest, value))
{
OnCanTestChanging(value);
OnCanTestChanging(default, value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.CanTest);
_canTest = value;
OnCanTestChanged(value);
OnCanTestChanged(default, value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.CanTest);
TestCommand.NotifyCanExecuteChanged();
}
}
}
public IRelayCommand TestCommand => testCommand ??= new RelayCommand(new Action(Test), () => CanTest);
То есть, самая элементарная реализация INotifyPropertyChanged и ICommand, которая знакома многим.
Теперь суть вопроса:
Этот код приводит к ошибке
Вызывающий поток не может получить доступ к данному объекту, так как владельцем этого объекта является другой поток.
Оно и понятно, мы таском выходим из UI потока, из-за чего мы не можем так просто работать с UI, но как вернуться обратно? У нас есть типичные решения данной проблемы, в которых используется Dispatcher. Ок, допустим, только это ViewModel, отдельный класс, который ничего не знает про UI, мы можем написать костыльно такое
public MainViewModel(Dispatcher dispatcher)
{
_ = Task.Run(async () =>
{
try
{
while (true)
{
await Task.Delay(1000);
dispatcher.BeginInvoke(new Action(() => { CanTest = !CanTest; }));
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
}
});
}
и это решит проблему, но только это нарушение MVVM и прочих правил. Используя Dispatcher.CurrentDispatcher мы также избавимся от ошибки, но у нас перестанут обновляться данные, что тоже вроде логично. Собственно, как решить проблему?
А да, само изменение свойства к ошибки не приводит, приводит обновления CanExecute (вызов: TestCommand.NotifyCanExecuteChanged(); ), который генерирует [NotifyCanExecuteChangedFor(nameof(TestCommand))]. Если убрать, обновления в реальном времени у кнопки не будет, но bool свойство значение менять начнет.
P.S. Бесконечный await цикл это для примера, в реальном проекте у меня есть сервис, который отслеживает изменения процесса (запущен или нет), и отсылает событие наружу, это все зарегистрировано в контейнере, из которого и запрашивает ViewModel, где она подписывается и обновляет свойство, получая ошибку.
public class ProcessWatcherService : IProcessWatcher, IDisposable
{
private readonly HashSet<string> _monitoredProcesses = new();
public event EventHandler<ProcessChangedEventArgs>? ProcessChanged;
private readonly ManagementEventWatcher _startWatch;
private readonly ManagementEventWatcher _stopWatch;
private readonly ILogger<ProcessWatcherService> _logger;
public ProcessWatcherService(ILogger<ProcessWatcherService> logger)
{
_logger = logger;
_startWatch = new ManagementEventWatcher(new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace"));
_startWatch.EventArrived += (_, e) => ProcessChangedEvent(e.NewEvent.Properties["ProcessName"].Value, ProcessStatus.Started);
_startWatch.Start();
_stopWatch = new ManagementEventWatcher(new WqlEventQuery("SELECT * FROM Win32_ProcessStopTrace"));
_stopWatch.EventArrived += (_, e) => ProcessChangedEvent(e.NewEvent.Properties["ProcessName"].Value, ProcessStatus.Stopped);
_stopWatch.Start();
}
public bool Verify(string process)
{
var result = Process.GetProcesses()
?.Any(x => x.ProcessName.ToLower() == Path.GetFileNameWithoutExtension(process).ToLower()) == true;
_logger.LogTrace("Verify process status {process} = {result}", process, result);
return result;
}
public void StartWatch(string process)
{
_logger.LogDebug("Process {process} added to watch list", process);
_monitoredProcesses.Add(process.ToLower());
}
public bool AddAndWatch(string process)
{
StartWatch(process);
return Verify(process);
}
void ProcessChangedEvent(object name, ProcessStatus status)
{
if (name is string processName && _monitoredProcesses.Contains(processName.ToLower()))
{
_logger.LogInformation("Process {process} change status: {status}", processName, status);
ProcessChanged?.Invoke(this, new(processName, status));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
_startWatch.Stop();
_stopWatch.Stop();
}
}
ProcessChanged?.Invoke(this, new(processName, status));? Вроде вызываю не асинхронные методы, а из UI вышел... – EvgeniyZ Jun 01 '23 at 22:20P.S.. Где там может быть выход? Простые синхронные вызовы, а ошибку получил, как, почему, не пойму... – EvgeniyZ Jun 01 '23 at 22:23ManagementEventWatcherвызывает свой обработчикEventArrivedв каком-то своём потоке? – aepot Jun 01 '23 at 22:25Debug.WriteLine(Thread.CurrentThread.ManagedThreadId)- посмотреть это для UI потока, а затем для обработчика. Контейнер выходить не будет, он синхронно работает. – aepot Jun 01 '23 at 22:51ManagementEventWatcherдругой, когда сам сервис в UI (1). А безопасно кстати использоватьSynchronizationContextвнутри сервиса, чтоб все подписчики его события сразу были в UI, или чревато проблемами? – EvgeniyZ Jun 01 '23 at 22:56SendилиPostвызвали. А дальше уже детали реализации. Для оптимизации можете проверятьif (_context == SynchronizationContext.Current) { я в контексте } else { я в рабочем потоке }. Тогда валидные вызовы из UI потока к свойству пройдут короткой дорогой, например если сам View к свойству обратится. Ну и осторожней с безусловным вызовомSend, если его дёрнуть из UI, поймаете дедлок. – aepot Jun 01 '23 at 23:00ObservableCollectionна потокобезопасных стеройдах. – aepot Jun 01 '23 at 23:06