c# - 多个 Winforms 项目中的异步等待
问题描述
当线程在后台运行时,我们试图在 UI 中获取状态更新。下面的代码应该允许它,但实际上我们只有在所有线程完成后才能获得更新,而不是在它们运行时。与串行运行任务相比,我们也没有看到显着的性能改进,因此我们可能在这里做错了
该解决方案包括两个带有 winForm 的项目,第一个调用第二个。WinClient 命名空间用于 Winform 客户端。它调用 Services.modMain:
namespace WinClient
{
static class Program
{
[STAThread]
static void Main()
{
//call another winform project and wait for it to complete
Services.modMain.loadObjects().Wait();
//run local form
Application.Run(new Form1());
}
}
}
Service.modMain 是应用程序不断获取数据并在内存中更新数据的地方。当它这样做时,它会将状态消息写入始终保持打开状态的启动表单。一旦 Service.modMain 完成初始数据加载,Form1(本例中为空表单)应该打开,而 splashForm 也保持打开状态
namespace Services
{
public static class modMain
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main()
{
}
public static async Task loadObjects()
{
frmSplash.DefInstance.LoadMe();
Progress<PrintToSplashMessage> messageToWindow = new Progress<PrintToSplashMessage>();
messageToWindow.ProgressChanged += reportProgress;
frmSplash.DefInstance.print_to_window("Starting Services", Color.Black, true);
Task<bool> load1Task = load1(messageToWindow);
Task<bool> load2Task = load2(messageToWindow);
await Task.WhenAll(load1Task, load2Task);
}
private static async Task<bool> load2(IProgress<PrintToSplashMessage> progress)
{
return await Task<bool>.Run(() =>
{
PrintToSplashMessage theMessage = new PrintToSplashMessage("Load2, please wait...", Color.Black, true, false);
progress.Report(theMessage);
for (int i = 0; i != 100; ++i)
{
Thread.Sleep(100); // CPU-bound work
}
return true;
});
}
private static async Task<bool> load1(IProgress<PrintToSplashMessage> progress)
{
return await Task<bool>.Run(() =>
{
PrintToSplashMessage theMessage = new PrintToSplashMessage("Load1, please wait...", Color.Black, true, false);
progress.Report(theMessage);
for (int i = 0; i != 100; ++i)
{
Thread.Sleep(100); // CPU-bound work
}
return true;
});
}
private static void reportProgress(object sender, PrintToSplashMessage e)
{
frmSplash.DefInstance.PrintToSplashWindow(e);
}
}
}
PrintToSplashWindow 只是一个用于存储进度数据的实用程序类:
namespace Services
{
public class PrintToSplashMessage
{
public string Message { get; set; }
public Color MessageColor { get; set; }
public bool OnNewLine { get; set; }
public bool PrintToLog { get; set; }
public PrintToSplashMessage(String theMessage, Color theMessageColor, bool isOnNewLine, bool needPrintToLog)
{
Message = theMessage;
MessageColor = theMessageColor;
OnNewLine = isOnNewLine;
PrintToLog = needPrintToLog;
}
}
}
最后,这里是 frmSplash:
namespace Services
{
public partial class frmSplash : Form
{
public frmSplash() :base()
{
InitializeComponent();
}
public void PrintToSplashWindow(PrintToSplashMessage theMessage)
{
print_to_window(theMessage.Message, theMessage.MessageColor, theMessage.OnNewLine);
}
public void print_to_window(string strShortMsg, Color lngColor, bool blnOnNewLine)
{
string strNewLine = String.Empty;
if (blnOnNewLine)
{
if ( rtbErrorDisplay.Text.Length > 0)
{
strNewLine = Environment.NewLine;
}
else
{
strNewLine = "";
}
}
else
{
strNewLine = "";
}
rtbErrorDisplay.SelectionStart = rtbErrorDisplay.Text.Length;
rtbErrorDisplay.SelectionColor = lngColor;
rtbErrorDisplay.SelectedText = strNewLine + strShortMsg;
rtbErrorDisplay.SelectionStart = rtbErrorDisplay.Text.Length;
rtbErrorDisplay.ScrollToCaret();
Application.DoEvents();
}
}
}
我们期望的是,当任务在后台运行时,frmSplash 会显示进度消息。在实践中,它只在一切都完成后才批量显示。
解决方案
简短版本:在您发布的代码中处理窗口消息的唯一方法是调用Application.DoEvents()
. 但是代码可能永远不会走那么远,或者如果确实如此,则调用发生在错误的线程上。
更长的版本:
您没有包含实际的MCVE,所以我没有费心去测试,但是Progress
该类依赖于同步上下文来工作。由于您还没有调用Application.Run()
,因此可能根本没有同步上下文。在这种情况下Progress
,只会使用线程池来调用订阅它的任何处理程序。
这意味着当您调用 时Application.DoEvents()
,您处于线程池线程中,而不是拥有您的启动窗口的线程。
Windows 由线程拥有,它们的消息进入该线程的消息队列。该Application.DoEvents()
方法将为当前线程的消息队列检索消息,但不为其他线程的队列处理消息。
在最坏的情况下,该线程有一个同步上下文(我不记得了……有可能因为线程是 STA,框架已经为你创建了一个),但是由于你没有消息循环,所以没有任何东西排队永远被派遣。进度报告只是不断堆积,从未处理过。
你应该完全放弃Application.DoEvents()
。跟注DoEvents()
总是很麻烦,总有更好的选择。
在这种情况下,Application.Run()
也可用于第一种形式(初始屏幕)。创建该表单并订阅其FormShown
事件,以便您知道何时调用loadObjects()
。在该方法结束时,关闭表单,这样Application.Run()
将返回并继续下一次Application.Run()
调用。
这是基于您发布的代码的示例,我填写了详细信息(对于这两种形式,只需使用设计器创建一个默认Form
对象……其余的初始化在下面的用户代码中)。
对于闪屏类,我推断出大部分内容,其余部分直接从您的代码中获取。我对您的代码所做的唯一更改是删除对以下的调用Application.DoEvents()
:
partial class SplashScreen : Form
{
public static SplashScreen Instance { get; } = new SplashScreen();
private readonly RichTextBox richTextBox1 = new RichTextBox();
public SplashScreen()
{
InitializeComponent();
//
// richTextBox1
//
richTextBox1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
richTextBox1.Location = new Point(13, 13);
richTextBox1.Name = "richTextBox1";
richTextBox1.Size = new Size(775, 425);
richTextBox1.TabIndex = 0;
richTextBox1.Text = "";
Controls.Add(richTextBox1);
}
public void PrintToSplashWindow(PrintToSplashMessage theMessage)
{
print_to_window(theMessage.Message, theMessage.MessageColor, theMessage.OnNewLine);
}
public void print_to_window(string strShortMsg, Color lngColor, bool blnOnNewLine)
{
string strNewLine = String.Empty;
if (blnOnNewLine)
{
if (richTextBox1.Text.Length > 0)
{
strNewLine = Environment.NewLine;
}
else
{
strNewLine = "";
}
}
else
{
strNewLine = "";
}
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.SelectionColor = lngColor;
richTextBox1.SelectedText = strNewLine + strShortMsg;
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.ScrollToCaret();
}
}
我不清楚为什么你有两个不同的类,这两个类似乎都被设置为程序的入口点。我将它们合并为一个类:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
loadObjects();
Application.Run(new Form1());
}
public static void loadObjects()
{
SplashScreen.Instance.Shown += async (sender, e) =>
{
Progress<PrintToSplashMessage> messageToWindow = new Progress<PrintToSplashMessage>();
messageToWindow.ProgressChanged += reportProgress;
SplashScreen.Instance.print_to_window("Starting Services", Color.Black, true);
Task<bool> load1Task = load1(messageToWindow);
Task<bool> load2Task = load2(messageToWindow);
await Task.WhenAll(load1Task, load2Task);
SplashScreen.Instance.Close();
};
SplashScreen.Instance.ShowDialog();
}
private static async Task<bool> load2(IProgress<PrintToSplashMessage> progress)
{
return await Task.Run(() =>
{
PrintToSplashMessage theMessage = new PrintToSplashMessage("Load2, please wait...", Color.Black, true, false);
progress.Report(theMessage);
for (int i = 0; i < 10; ++i)
{
Thread.Sleep(TimeSpan.FromSeconds(1)); // CPU-bound work
theMessage.Message = $"Load2, i = {i}";
progress.Report(theMessage);
}
return true;
});
}
private static async Task<bool> load1(IProgress<PrintToSplashMessage> progress)
{
return await Task.Run(() =>
{
PrintToSplashMessage theMessage = new PrintToSplashMessage("Load1, please wait...", Color.Black, true, false);
progress.Report(theMessage);
for (int i = 0; i < 10; ++i)
{
Thread.Sleep(TimeSpan.FromSeconds(1)); // CPU-bound work
theMessage.Message = $"Load1, i = {i}";
progress.Report(theMessage);
}
return true;
});
}
private static void reportProgress(object sender, PrintToSplashMessage e)
{
SplashScreen.Instance.PrintToSplashWindow(e);
}
}
推荐阅读
- python - 向rabbitmq队列添加多个队列
- ios - iOS:如何跟踪 Firebase 动态链接
- flutter - NoSuchMethodError:在 null 上调用了 getter 'bloc'
- sql - PostgreSQL:根据分隔符将一个元素的数组拆分为多个元素并取消嵌套的查询
- java - 在android中从前置摄像头和后置摄像头拍摄图像后如何修复图像旋转度数?
- javascript - 如何在 Puppeteer 中等待 ElementHandle 对象单击操作
- php - 修改php中json数组的日期格式
- javascript - Cloudinary with Django - 在页面渲染后使用 javascript 动态增加/减少照片的宽度和高度
- android - Android:如何在不调用 onCreate 方法的情况下从服务后台恢复应用程序?
- api-key - Express-Gateway,提供相同的 API 路径/路由,但在不同的 ServiceEndpoints 下