在异步按钮单击时重用UI处理代码

本文关键字:UI 处理 代码 异步 按钮 单击 | 更新日期: 2023-09-27 18:09:50

情况

在客户端/服务器体系结构中,在长时间运行的操作(如服务器端通信(期间停用UI控件是很常见的。这个答案显示了一个专门针对特定情况的解决方案。

// condensed code listing from linked answer
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    DisableUI(); 
    try { await ViewModel.CreateMessageCommand(); }
    finally { EnableUI(); }
}

有没有一种方法可以概括/抽象这种操作并重用它?在某些应用程序中,此代码往往会重复多次。然而,它遵循了Stephen Cleary的文章中指出的最佳实践,喜欢一直使用async,除了事件处理之外,更喜欢async Task而不是async void,例如这个按钮点击处理程序。异步等待最佳实践

假设具有以下可能长时间运行的代码部分:

// somewhere in an appropriate scope
async Task TheWorkWeGotToDo() { Task.Delay(2000); }

测试了一些尝试,以集中围绕这个潜在的长期运行操作启用和禁用UI代码的包装:

尝试1

class OurButton extends SomeKnownButton {
  Action theWork;
  public async void HandleClick(object sender, EventArgs args) {
    DisableUI();
    try { await theWork(); }
    finally { EnableUI(); }
}

由于误用了非async Action实例,此尝试不允许await声明的操作并引发编译错误。

尝试2

class OurButton extends SomeKnownButton {
  Task theWork;
  public async void HandleClick(object sender, EventArgs args) {
    DisableUI();
    try { await theWork; }
    finally { EnableUI(); }
}

这种尝试允许await指定为Task的工作,并允许将任务的创建与其执行分离,正如微软的async await初学者所指出的那样。Async Await初学者文章来自Microsoft

// Signature specifies Task<TResult>
async Task<int> TaskOfTResult_MethodAsync()
{
    int hours;
    // . . .
    // Return statement specifies an integer result.
    return hours;
}
// Calls to TaskOfTResult_MethodAsync
Task<int> returnedTaskTResult = TaskOfTResult_MethodAsync();
int intResult = await returnedTaskTResult;
// or, in a single statement
int intResult = await TaskOfTResult_MethodAsync();

这会编译,但会使定义需要封装的工作变得困难。对于这种情况,一些不令人满意的尝试是:

尝试2a

OurButton b = /* init */;
b.theWork = TheWorkWeGotToDo; // compile error

尝试2b

OurButton b = /* init */;
b.theWork = new Task(() => TheWorkWeGotToDo());

这会编译,但是它的行为并不像预期的那样。

尝试3

另一种可能性是在按钮类中声明一个virtual成员。子类可以使用async关键字来实现此操作。

class T {
    async void Test() { await AsyncWork(); }  
    public virtual async Task AsyncWork() {} // warning: will run synchronously
}
class WrappedOperation : T {
  public override async Task AsyncWork() { await TheWorkWeGotToDo(); }
}

从按钮类派生每一种情况似乎都非常不合适。

问题

谈到一些函数和面向方面的编程背景:是否可以以某种方式装饰函数以异步截取部分代码?

OurButton b = /* init */;
Action someWork = /* some long running task */;
Action guardedWork = someWork.SurroundWithUiGuards(b);
b.Clicked += guardedWork;
// in some appropriate scope
static class Extension {
  public static Action SurroundWithUiGuards(this Action work, SomeKnownButton b) {
    return () => {
      b.Disable();
      try { work(); }
      finally { b.Enable(); }
    };
  }
}

因此,在这种情况下:是否可以以某种方式使用类似someWorkAction并包装拦截代码来保护ui元素并返回一个新的"Action"实例?

备注

这是一个有点开放的问题,解决具体问题就足够了。然而,任何回答抽象问题的建议都非常感谢!当前的目标环境是单线程ui框架Xamarin。

我认为主要的问题是,一方面似乎不可能将异步操作声明为字段。另一方面,可以声明异步lambda表达式,但如何等待这些表达式呢?我也希望我正在监督一些非常明显的事情。

我对c#async await编程真的很陌生,但我已经读了很多asyncawait的文章。

在异步按钮单击时重用UI处理代码

实际实现的解决方案是使用Task对象定义所有内容。可以等待Task,从而指出一种用拦截代码包裹长时间运行的操作的解决方案:

static class Ext {
  public static async Task SurroundWithUIGuard(this Task task, SomeButton button) {
    button.Enabled = false;
    try { await task; }
    finally { button.Enabled = true; }
  }
}

这是一种功能性的方法,可以将任务创建与其执行分离,并通过修改Task实例在创建之前和之后包装代码。要定义事件处理程序,只需要再引入一些异步和等待表达式。

SomeButton b = /* init */;
b.Clicked += async (s,e) => await Task.Delay(5000).SurroundWithUIGuard(b);

此外,async lambda表达式是这一系列async Task运算中唯一的async void,因此遵循最佳实践。由于async await关键字的大量使用,它看起来仍然有点冗长,但允许重用这部分代码。