封面图:https://www.pixiv.net/artworks/92335014
本文首发于知乎:https://zhuanlan.zhihu.com/p/625789580
找了很久才找到解决方法,特此记录。
网上很多都是简单的一句“可以将异步委托更改成异步任务”的作用,但是很难理解。
这是让我“茅厕顿开”的一个例子:C# dotnet 使用 TaskCompletionSource 让事件转异步方法
下面结合我的实际用途讲一下这玩意有什么用:
假设你在做客户端登录,先检查缓存有没有 jwt token,有就直接用 jwt token 去连接服务器,没有就要弹出个窗口,让用户输入用户名和密码登录以获得 jwt token。
public class ClientService
{
// 这个委托是外面传进来的,调用就会打开登录页面
// ClientService 不关心登录怎么实现,它只想知道登录有没有成功
// 因此这个委托异步返回登录结果
public Func<string, Task<bool>>? LoginMethod { get; set; }
public async Task<bool> ConnectAsync(string url)
{
// 获取 jwt token
string jwtToken = 随便什么方法()
// 我们暂时不关心 jwt token 的有效性
if (jwtToken == null)
{
// 要登录
// 我们希望打开登录页面,用户一通操作,
// 完成登录后返回登录有没有成功
bool loginResult = await LoginMethod(url);
if (loginResult == true)
{
// 登录成功,可以再次获取jwt token
}
else
{
// 用户不登录了
}
}
// 接下来是用jwt token连接服务器
// 我们也不关心
}
}
现在要实现 LoginMethod
里面的具体操作了。这时候就会想到一个问题:不管是 xamarin、WPF 还是 Razor 网页,如果打开新的页面/窗口/网页之类的玩意用来给用户登录,窗口关闭后可没办法返回值去指示登录有没有成功啊!(虽然 WPF 的 ShowDialog() 可以返回值,xamarin 也可以安装第三方 popup ,但是为了统一多平台行为还是不这么做了)
好,想到了异步事件:虽然窗口没办法返回值,但是我可以用事件传值啊!登录窗口声明个事件,ClientService
监听这个事件,登录窗口搞完后就触发这个事件,ClientService
就可以收到登录结果了。于是一通操作猛如虎:
首先实现登录窗口:
// 这里用 WPF 做示范
public partial class LoginWindow : Window
{
// 事件
public event EventHandler<bool> LoginOver;
public LoginWindow(string url)
{
// 初始化
}
public async Task LoginAsync(string url)
{
// 我们不关心具体怎么登录
// 登录成功返回 true
bool loginResult = 随便怎么处理(账号密码之类的);
// 触发事件,把登录结果传回去
LoginOver?.Invoke(loginResult)
// 关闭窗口,不关心
}
}
然后把登录窗口绑上 ClientService
的委托:
// 给 ClientService 绑上实现好的登录方法
ClientService.LoginMethod = async (url) =>
{
LoginWindow loginWindow = new(url);
loginWindow.ShowDialog();
};
然后,你试图修改 ClientService
:
public class ClientService
{
// 这个委托是外面传进来的,调用就会打开登录页面
// ClientService 不关心登录怎么实现,它只想知道登录有没有成功
// 注意这玩意现在不返回结果了
public Action<string>? LoginMethod { get; set; }
public async Task<bool> ConnectAsync(string url)
{
// 获取 jwt token
string jwtToken = 随便什么方法()
// 我们暂时不关心 jwt token 的有效性
if (jwtToken == null)
{
// 要登录
// 我们希望打开登录页面,用户一通操作,
// 完成登录后返回登录有没有成功
// ?????????
// 怎么才能在这里绑上 LoginOver 事件?
LoginMethod(url);
// ?????????
// 怎么在这里获取 LoginOver 传递回来的登录结果?
if (loginResult == true)
{
// 登录成功,可以再次获取jwt token
}
else
{
// 用户不登录了
}
}
// 接下来是用jwt token连接服务器
// 我们也不关心
}
}
这个时候就碰到非常棘手的问题了:
- 既然
LoginMethod()
内部怎么实现的与ClientService
无关,那ClientService
怎么监听LoginMethod()
内部的LoginWindow
的LoginOver
事件???
当然,相信聪明的你一定能想到办法绑上去,即使可能违反各种乱七八糟的编程原则,不管怎样这并不是一个无法解决的问题。但是接下来的这个问题才是致命一击:
2. 怎么让 LoginOver
触发的值恰好传回到调用了 LoginMethod()
之后,以继续进行 ClientService.ConnectAsync()
接下来的流程???
也许大神可以用基于事件的异步或者回调之类的解决这个问题,但反正我是没想到怎么解决,而且我已经习惯了 async/await 语法糖。这时候,就该 TaskCompletionSource
英雄出场了!
首先修改登录窗口:
// 这里用 WPF 做示范
public partial class LoginWindow : Window
{
TaskCompletionSource<bool> _taskCompletionSource;
public LoginWindow(string url, TaskCompletionSource<bool> taskCompletionSource)
{
_taskCompletionSource = taskCompletionSource
// 初始化
}
public async Task LoginAsync(string url)
{
// 我们不关心具体怎么登录
// 登录成功返回 true
bool loginResult = 随便怎么处理(账号密码之类的);
// 把登录结果传回去
_taskCompletionSource.SetResult(_loginResult);
// 关闭窗口,不关心
}
}
然后修改绑定:
// 给 ClientService 绑上实现好的登录方法
ClientService.LoginMethod = async (url, taskCompletionSource) =>
{
LoginWindow loginWindow = new(url, taskCompletionSource);
loginWindow.ShowDialog();
};
最后修改调用:
public class ClientService
{
// 这个委托是外面传进来的,调用就会打开登录页面
// ClientService 不关心登录怎么实现,它只想知道登录有没有成功
// 注意这玩意现在不返回结果了
public Action<string, TaskCompletionSource>? LoginMethod { get; set; }
public async Task<bool> ConnectAsync(string url)
{
// 获取 jwt token
string jwtToken = 随便什么方法()
// 我们暂时不关心 jwt token 的有效性
if (jwtToken == null)
{
// 要登录
// 我们希望打开登录页面,用户一通操作,
// 完成登录后返回登录有没有成功
TaskCompletionSource<bool> source = new();
// 把 TaskCompletionSource 一起传进去
LoginMethod(url, source);
// 这里就一直等待
// 直到 LoginWindow 触发 SetResult(), 就会精准把值传回到这里
bool loginResult = await source.Task;
// 继续流程
if (loginResult == true)
{
// 登录成功,可以再次获取jwt token
}
else
{
// 用户不登录了
}
}
// 接下来是用jwt token连接服务器
// 我们也不关心
}
}
Comments NOTHING