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



我是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();
}

任何帮助/建议/参考不胜感激。

下面的代码有效,但是在我将内部代码提取到另一个方法后taskTimer.Elapsed += delegate{ },它会抛出"无法访问此内容,因为它归另一个线程所有"错误。

使用 System.Windows.Threading.DispatcherTimer 并处理其Tick事件。

不同之处在于,Tick事件将在 UI 线程上引发,UI 线程是可以访问 UI 控件的唯一线程。System.Timers.TimerElapsed事件在后台线程上运行。

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

文本框现在将仅针对错误消息显示

最新更新