我知道通常不应该从UI线程以外的线程触摸UI元素,但我是WPF的新手,我想知道我当前的工作实现是否可以改进。
我的应用程序仅包含通知托盘图标,我想从后台线程更新该图标。
这是我的Program.cs入口点:
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (IconHandler notify = new IconHandler())
{
notify.Display();
Application.Run();
}
}
}
这是我的IconHandler.cs通知图标处理程序类:
class IconHandler : IDisposable
{
NotifyIcon ni;
public IconHandler()
{
ni = new NotifyIcon();
}
public void Display()
{
ni.MouseClick += new MouseEventHandler(ni_MouseClick);
ni.Icon = Resources.icon1;
ni.Visible = true;
new Thread(new ThreadStart(UpdateIcon)).Start();
}
public void UpdateIcon()
{
while (true)
{
// reference ni directly, it updates fine
}
}
public void Dispose()
{
ni.Dispose();
}
void ni_MouseClick(object sender, MouseEventArgs e)
{
// something useful
}
}
这有什么明显不正确的吗?这对我来说似乎有点可疑 - 这只是我的第一次尝试。它似乎适合我想做的事情,有没有人有任何更好的实施建议?我会在这个设置中遇到生命周期问题吗?
答案 0 :(得分:3)
这有什么明显不正确的吗?这对我来说似乎有点可疑 - 这只是我的第一次尝试。它似乎适合我想做的事情,有没有人有任何更好的实施建议?我会在这个设置中遇到生命周期问题吗?
首先,NotifyIcon
不是WPF控件,而是来自Windows窗体命名空间。因此,它具有正常的C#属性(例如Icon
,Visible
),这意味着您可以在非UI线程中更改图标属性,而不会引发异常。如果您使用了WPF控件,那么它们具有依赖项属性,并且在UI线程之外直接操作依赖项属性将导致异常被引发。
我会在这个设置中遇到生命周期问题吗?
您目前 NOT 创建了WPF窗口或WPF控件。如果您的应用程序开发使得您开始使用WPF并且UpdateIcon
方法扩展为比您当前执行的更多并访问这些WPF对象,那么您将需要一种策略来处理来自非UI线程的更新。
您可以使用一些辅助方法隐藏一些此跨线程访问。
示例1 如果您的策略从后台线程以编程方式引用WPF控件,那么您可以使用诸如此类的辅助方法。
它首先检查调用是否在UI线程上,如果是,则它直接更新控件,否则它将安排稍后从UI线程调用方法(本身)时间点。
我在这里使用了BeginInvoke
,以便后台线程可以在UI线程实际调用该方法之前继续。如果要阻止后台线程,请改用Invoke
。
public void UpdateLabel(Label control, string text)
{
if (Application.Current.Dispatcher.CheckAccess())
control.Content = text;
else
Application.Current.Dispatcher.BeginInvoke(new System.Action(() => UpdateLabel(control, text)), DispatcherPriority.Normal);
}
示例2
如果您的策略使用后台线程上引发的Events
以编程方式更新WPF控件,那么您可以隐藏一些跨线程调用作为引发事件的一部分,使WPF更新例程非常干净且易于阅读。
可以编码此事件的任何事件处理程序,因为它将从UI线程进行调用,因此不会出现线程问题。
public void OnRaiseEvent(EventHandler handler, EventArgs args)
{
if (handler != null)
{
if (Application.Current.Dispatcher.CheckAccess())
handler(sender, new PropertyChangedEventArgs(propName));
else
Application.Current.Dispatcher.BeginInvoke(new System.Action(() => handler(sender, args)), DispatcherPriority.Normal);
}
}
示例3
如果您的未来策略充分利用WPF与Binding的优势(而不是以编程方式更新WPF控件),那么您可以将交叉线程代码嵌入到数据绑定对象中。
例如,如果您的XAML
数据绑定到MyProperty
类实例的MyDataClass
属性,并且该类实现了INotifyPropertyChanged接口,则可以将交叉线程代码放入数据中class使得可以从任何线程更新数据。以下是该类的示例: -
public class MyDataClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _myProperty;
public string MyProperty { get { return _myProperty;} set { PropertyChanged.SetValueAndNotify(this, ref _myProperty, value); } }
}
此课程在SetValueAndNotify
事件中使用PropertyChanged
扩展名方法。在这里我们隐藏了交叉线程代码以简化代码的其他部分。这是此扩展方法的定义。
public static class PropertyChangedExtension
{
public static void SetValueAndNotify<T>(this PropertyChangedEventHandler handler, object sender, ref T destination, T source, [CallerMemberName] string propName = "notset")
{
// Is the new value different from the previous value? If there is no difference then there is nothing more to do
if (Equals(destination, source))
return;
// If we got to this point then the new value is different from the old value, so lets make the assignemnt and raise the property changed event
destination = source;
if (handler != null)
{
if (Application.Current.Dispatcher.CheckAccess())
handler(sender, new PropertyChangedEventArgs(propName));
else
Application.Current.Dispatcher.BeginInvoke(new System.Action(() => handler(sender, new PropertyChangedEventArgs(propName))), DispatcherPriority.Normal);
}
}
}
上面的示例使用C#5中的[CallerMemberName]
属性来删除为INotifyPropertyChanged参数提供属性名称时的任何输入错误。如果您没有使用最新版本,则需要按如下方式修改getter和setter: -
public string MyProperty { get { return _myProperty;} set { PropertyChanged.SetValueAndNotify(this, ref _myProperty, value, "MyProperty"); } }
答案 1 :(得分:0)
代码中的while(true)
循环将导致大量CPU /资源使用。也许添加例如Thread.Sleep(1000)
进入循环以允许更新之间的中断。
后台线程的最佳用法是在后台线程上执行长时间运行的工作(例如与服务器/ DB的通信),一旦线程完成,让UI线程更新UI。
使用BackgroundWorker:
var worker = new BackgroundWorker();
worker.DoWork += (sender, args) =>
{
// long running work
};
worker.RunWorkerCompleted += (sender, args) =>
{
// Update UI
};
worker.RunWorkerAsync();
async / await pattern:
public async void DoWork()
{
// Do long running task
var data = await Task.Run(() => new object());
// Update UI here
}
TaskFactory:
Task.Factory.StartNew(() => new Object()).ContinueWith(task => MessageBox.Show(task.Result.ToString()), TaskScheduler.FromCurrentSynchronizationContext());
如果UI需要在常量循环上更新,可以使用计时器定期重启该过程。这样可以避免CPU受到冲击。
答案 2 :(得分:0)
您必须始终只从UI线程更新UI,但是,您可以使用调度程序从后台线程安排UI线程的一些工作
public void Display()
{
ni.MouseClick += new MouseEventHandler(ni_MouseClick);
ni.Icon = Resources.icon1;
ni.Visible = true;
new Thread(new ThreadStart(UpdateIcon)).Start();
}
public void UpdateIcon()
{
while (true)
{
//do some long running work
Application.Current.Dispatcher.Invoke(()=>{
//update ui
});
}
}
但是如果你没有长时间的工作并且你只想定期做某事,你应该在后台线程中使用DispatcherTimer而不是循环。