Обновление элементов управления из другого потока / класса

Я новичок в 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 13.09.2018 17:00

@Panagiotis Kanavos Это действительно вызывает исключение, о котором я упоминал. Если я сделаю taskTimer.Elapsed += delegate {NewMethod}; и вставлю mainWindow.txtError.Dispatcher.Invoke(new Action(() => { mainWindow.txtError.Text = "Something went wrong"; })); в NewMethod, это выдаст ошибку. @ mm8 объяснил это ниже, но я его не тестировал.

Kevin 14.09.2018 03:31

нет, это вообще не объясняет ошибку. .Dispatcher.Invoke будет маршалировать вызов в поток пользовательского интерфейса. Вы сказали, что извлекли обработчик в другой метод. Опубликовать код что

Panagiotis Kanavos 14.09.2018 08:02

В любом случае попытки получить доступ к элементам управления одной формы из другой формы или потока - плохая идея. В WPF это плохая идея очень - стек был построен для работы через привязку данных, команды, сообщения, агрегаторы событий, MVVM и т. д., А не через прямые манипуляции с пользовательским интерфейсом. Вы можете использовать Progress<T> и интерфейс IProgress<T> для публикации событий из одного потока / модуля без жесткого кодирования ссылки на другой. Различные фреймворки MVVM имеют явную поддержку сообщений или агрегаторов событий (также называемых подписчиками).

Panagiotis Kanavos 14.09.2018 08:06
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
452
2

Ответы 2

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.TimerElapsed выполняется в фоновом потоке.

.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.

Kevin 14.09.2018 18:27

Другие вопросы по теме