Я новичок в WPF и работаю над небольшим личным проектом. Мне интересно, каков наилучший / правильный способ достичь того, чего я хочу. Настройка такова: пользователь щелкает button
, который вызывает класс с именем ProcessManager
. ProcessManager
затем установит таймер, который вызовет другой класс с именем DeviceController
, который будет записывать данные в базу данных. Я хочу, чтобы DeviceController
мог изменить текстовое поле в графическом интерфейсе, чтобы пользователи знали о любых ошибках, возникающих при записи в базу данных.
Приведенный ниже код работает, но после того, как я извлеку код из taskTimer.Elapsed += delegate{ }
в другой метод, он выдает ошибку "cannot access this because it is owned by another thread"
.
public void StartMonitoring()
{
var mainWindow = Application.Current.Windows.Cast<Window>().FirstOrDefault(x => x is MainWindow) as MainWindow;
var _schedule = DateTime.Now;
var _nextTaskSched = _schedule.AddSeconds(10);
var _timerTicks = (_nextTaskSched - DateTime.Now).TotalMilliseconds;
var taskTimer = new Timer(_timerTicks);
taskTimer.Elapsed += delegate
{
//call DeviceController here//
//do stuff//
//something went wrong//
mainWindow.txtError.Dispatcher.Invoke(new Action(() =>
{ mainWindow.txtError.Text = "Something went wrong"; }));
};
taskTimer.Start();
}
Любая помощь / предложения / ссылки приветствуются.
@Panagiotis Kanavos Это действительно вызывает исключение, о котором я упоминал. Если я сделаю taskTimer.Elapsed += delegate {NewMethod};
и вставлю mainWindow.txtError.Dispatcher.Invoke(new Action(() => { mainWindow.txtError.Text = "Something went wrong"; }));
в NewMethod
, это выдаст ошибку. @ mm8 объяснил это ниже, но я его не тестировал.
нет, это вообще не объясняет ошибку. .Dispatcher.Invoke
будет маршалировать вызов в поток пользовательского интерфейса. Вы сказали, что извлекли обработчик в другой метод. Опубликовать код что
В любом случае попытки получить доступ к элементам управления одной формы из другой формы или потока - плохая идея. В WPF это плохая идея очень - стек был построен для работы через привязку данных, команды, сообщения, агрегаторы событий, MVVM и т. д., А не через прямые манипуляции с пользовательским интерфейсом. Вы можете использовать Progress<T>
и интерфейс IProgress<T>
для публикации событий из одного потока / модуля без жесткого кодирования ссылки на другой. Различные фреймворки MVVM имеют явную поддержку сообщений или агрегаторов событий (также называемых подписчиками).
The code below works, but after I extract the code inside
taskTimer.Elapsed += delegate{ }
to another method, it throws a "cannot access this because it is owned by another thread" error.
Используйте System.Windows.Threading.DispatcherTimer и обработайте его событие Tick
.
Разница в том, что событие Tick
будет вызвано в потоке пользовательского интерфейса, который является единственным потоком, в котором вы можете получить доступ к элементам управления пользовательского интерфейса. Событие System.Timers.Timer
Elapsed
выполняется в фоновом потоке.
.Dispatcher.Invoke
отправит вызов потоку пользовательского интерфейса. Вы не должны получать этого исключения. Вместо этого вам следует опубликовать отредактированный код.
В любом случае дизайн плохой, так как он добавляет жесткую зависимость между потоком мониторинга и пользовательским интерфейсом. Формы и модули не должны иметь прямых ссылок друг на друга, особенно в WPF.
WPF добавляет привязку данных, команды, сообщения, поэтому приложения не должны жестко кодировать ссылки между формами. То, что отображается в текстовом поле сегодня, может появиться в текстовом поле панели состояния на следующей неделе. Вам не нужно изменять бизнес-модули или служебные модули для такого незначительного изменения пользовательского интерфейса.
Фреймворки MVVM добавляют явную поддержку для приложений / бизнес-событий через сообщения или агрегаторы событий. Фактические имена зависят от структуры MVVM.
.NET Runtime itsel предоставляет интерфейс IProgress<T>
и класс Progress<T>
для публикации объектов выполнения между потоками. Класс Progress<T>
будет вызывать событие или вызывать обратный вызов в потоке, который он был создан, каждый раз, когда кто-то вызывает IProgress<T>
. Это означает, что вы можете просто передать интерфейс, и код мониторинга / рабочего не должен знать, как и что обрабатывает событие прогресса.
StartMonitoring
можно упростить до этого:
public void StartMonitoring(IProgress<string> progress)
{
var _schedule = DateTime.Now;
var _nextTaskSched = _schedule.AddSeconds(10);
var _timerTicks = (_nextTaskSched - DateTime.Now).TotalMilliseconds;
var taskTimer = new Timer(_timerTicks);
taskTimer.Elapsed += delegate
{
//call DeviceController here//
//do stuff//
//something went wrong//
progress.Report("Something went wrong";);
};
taskTimer.Start();
}
Или вы можете передать интерфейс в конструкторе класса мониторинга
public class MyMonitor
{
IProgress<sring> _progress;
public MyMonitor(IProgress<string> progress,...)
{
....
_progress=progress;
}
public void StartMonitoring(IProgress<string> progress)
{
...
taskTimer.Elapsed += delegate
{
//call DeviceController here//
//do stuff//
//something went wrong//
_progress.Report("Something went wrong";);
};
taskTimer.Start();
}
}
Если метод создается в главном окне, все, что вам нужно сделать, это заранее создать Progress<T>
и передать его методу:
public class MainWindow :...
{
Progress<string> _progress;
public MainWindow()
{
InitializeComponent();
_progress=new Progress<string>(OnProgress);
}
private void OnProgress(string message)
{
txtError.Text = message;
}
public void MethodThatStartsMonitoring()
{
//This could be passed in a constructor too.
myMonitor.StartMonitoring(_progress);
}
}
IProgress<T>
может принимать любой объект, а не только строку. Это, в сочетании с привязкой данных, означает, что вы можете одновременно обновлять элементы управления несколько.
Вместо строки вы можете использовать класс Status
, например:
public class Status
{
public bool IsError{get;set;}
public string Message {get;set;}
public Status(bool isError,string message)
{
IsError=isError;
Message=message;
}
}
Вы можете использовать этот класс с IProgress<T>
:
public void StartMonitoring(IProgress<Status> progress)
{
...
taskTimer.Elapsed += delegate
{
progress.Report(new Status(false,"Starting"));
//call DeviceController here//
//do stuff//
//something went wrong//
progress.Report(new Status(true,"Something went wrong"));
};
...
}
И измените код основной формы на этот:
public class MainWindow:INotifyPropertyChanged,...
{
Progress<Status> _progress;
private Status _status=new Status();
public Status Status
{
get=>_status;
set
{
__status=value;
OnPropertyChanged("Status");
}
}
public MainWindow()
{
InitializeComponent();
_progress=new Progress<Status>(OnProgress);
this.DataContext=this;
}
private void OnProgress(Status status)
{
Status=status;
}
Теперь вы можете добавить привязки из нескольких элементов управления к свойству Status
либо в XAML, либо в коде, например:
<TextBox x:Name = "MyErrorBox" Text = "{Binding Status.Message}"/>
Теперь обработчику прогресса и даже программному обеспечению не нужно знать об элементах, которые будут отображать данные.
Вы также можете привязать другие свойства, например, видимость:
<Window.Resources>
<BooleanToVisibilityConverter x:Key = "BoolToVisConverter" />
</Window.Resources>
...
<TextBox x:Name = "MyErrorBox"
Text = "{Binding Status.Message}"
Visibility = "{Binding Path=Status.IsError, Converter = {StaticResource BoolToVisConverter} }" />
Текстовое поле теперь будет отображаться только для сообщений об ошибках.
Спасибо тебе за это. Первая версия (с использованием string
), которую вы дали, работает отлично. Однако у меня возникают проблемы с тем, чтобы вторая версия работала (я хочу использовать этот подход, выглядит чище). Не могли бы вы взглянуть? У меня возникают ошибки при изменении кода MainWindow.
Опубликуйте метод, который действительно вызывает исключение. Простое извлечение делегата другому методу не вызовет никаких исключений.