从另一个线程/类更新控件

时间:2018-09-13 14:58:31

标签: c# wpf

我是WPF的初学者,正在从事一个小型个人项目。我想知道什么是实现我想要的最好/正确的方法。设置是这样的:用户单击button,这将调用名为ProcessManager的类。 ProcessManager然后将设置一个计时器,该计时器将调用另一个名为DeviceController的类,该类会将数据写入数据库。我想要的是一种DeviceController更改GUI上的文本框的方式,以使用户知道在数据库中写入遇到的任何错误。

下面的代码有效,但是我将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();
}

感谢任何帮助/建议/参考。

1 个答案:

答案 0 :(得分:1)

.Dispatcher.Invoke将调用分配到UI线程。您不应该得到该例外。您应该改为发布重构的代码。

在任何情况下,该设计都是不好的,因为它在监视线程和UI之间增加了硬性依赖性。表单和模块之间不应直接相互引用,尤其是在WPF中。

WPF添加了数据绑定,命令,消息,因此应用程序不必不必对表单之间的引用进行硬编码。今天在文本框中显示的内容可能在下周显示在“状态”面板文本框中。您无需为如此小的UI更改而修改业务或服务模块。

MVVM框架通过消息或事件聚合器添加了对应用程序/业务事件的显式支持。实际名称取决于MVVM框架。

.NET运行时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;
    }

您现在可以使用XAML或代码(例如:

)将来自多个控件的绑定添加到Status属性中。
    <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} }" />

该文本框现在仅显示错误消息