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

Я новичок в 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();
}

Любая помощь / предложения / ссылки приветствуются.

Ответы [ 2 ]

0 голосов
/ 14 сентября 2018

.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} }" />

Текстовое поле теперь будет отображаться только для сообщений об ошибках

0 голосов
/ 13 сентября 2018

Приведенный ниже код работает, но после того, как я извлек код из taskTimer.Elapsed += delegate{ } в другой метод, он выдает ошибку «невозможно получить доступ к этому, потому что он принадлежит другому потоку».

Используйте System.Windows.Threading.DispatcherTimer и обрабатывайте его Tick событие.

Разница в том, что событие Tick будет вызвано в потоке пользовательского интерфейса, который является единственным потоком, в котором вы можете получить доступ к элементам управления пользовательского интерфейса. Событие System.Timers.Timer Elapsed выполняется в фоновом потоке.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...