如何在 WinForms 客户端应用程序中使用命令模式?

How to use command pattern in a WinForms client application?

背景

我正在构建一个两层 C# .net 应用程序:

  1. 第 1 层:使用 MVP(模型-视图-呈现器)设计模式的 Winforms 客户端应用程序。
  2. 第 2 层:WebAPI RESTful 服务位于 Entity Framework 和 SQL 服务器之上。

如果您想了解我正在构建的应用程序的更多详细信息,我可能给出了过于详尽的解释

当前发展

目前,我正在开发 Winforms 客户端。特别是,我正在尝试在此客户端中充分实施命令模式。我很幸运地通过解释他如何将查询与命令分开来偶然发现 this excellent blog post that outlines a solid command architecture. To complement that post, the author followed up。阅读这些博客后,很明显我的第 2 层(web api 服务)将从实施这两个方面受益匪浅。通用实现具有出色的灵活性、可测试性和可扩展性。

问题

我不太清楚的是我如何在 winforms 客户端(第 1 层)上实现这些模式。查询和命令在这里继续被认为是分开的吗?考虑一个基本操作,例如登录尝试。那是查询还是命令?最终,您需要从 Web 服务返回数据(服务器上的用户信息),所以这会让我认为这是一个查询。另一种情况呢,比如请求创建一个新用户。我知道您会创建一个命令对象来存储用户信息并将其发送到服务。命令应该是即发即弃的,但是您不想从服务那里得到命令成功的某种确认吗?此外,如果命令处理程序 returns 无效,您将如何告诉演示者用户创建请求是否成功?

归根结底,对于任何给定的 UI 任务(例如用户创建请求),您是否最终也拥有基于 query/command 的 winforms 客户端?作为处理请求的 command/query 的网络 api 服务版本?

Do queries and commands continue to be considered separate here?

是的,通常您会触发命令,如果您需要在执行此操作后更新 UI,您将执行查询以获取新信息。举个例子可以清楚地说明这一点。

假设您要为某个区域分配一个特定的守卫。命令(只有一个DTO)需要的唯一信息是守卫的Id和区域的Id。关联的 CommandHandler 将执行所有任务来处理这个问题,例如将那个守卫从另一个区域移走,将他记为不可用等等

现在您的 UI 想要显示更改。 UI 可能有某种列表,其中包含所有警卫及其分配的区域。此列表将由一个 GetActiveGuardsAndAreaQuery 填充,它将 return 一个 List<GuardWithAreaInformationDto>。这个DTO可以包含所有警卫的各种信息。从命令返回此信息并不是关注点的完全分离,因为原子命令处理可以很好地用于类似但略有不同的 UI,这将需要对 UI 进行略微不同的更新资料。

such as a login attempt. Is that a query or a command?

IMO 登录尝试都不是。这是一个横切关注点,是数据隐藏在安全连接后面的实现细节。然而,应用程序不应该关心这个细节。考虑与另一个客户一起使用该应用程序,您可以在其中托管 WebApi 服务,并且可以在 Active Directory 域中使用 Windows Authentication。在那种情况下,用户只需登录到他的机器,安全由客户端和服务器在通信时处理OS。

使用您所指的模式,使用 AuthenticateToWebApiServiceCommandHandlerDecorator 可以很好地完成此操作,它通过以模态形式询问用户并从配置文件,或其他任何东西。

检查凭据是否有效可以通过执行您的应用程序始终需要的一种标准 Query 来完成,例如 CheckIfUpdateIsAvailableQuery。如果查询成功,则登录尝试成功,否则失败。

if a command handler returns void, how would you tell the presenter whether or not the user creation request was successful?

虽然看起来 void 没有 return 任何东西,但事实并非如此。因为如果它没有因某些异常而失败(并明确显示出了什么问题!),它就一定成功了。

在提到的博客 follow up 中,@dotnetjunkie 描述了一种从命令中获取 return 信息的方法,但请注意 [=57] 顶部添加的评论=].

总而言之,从失败的命令中抛出明显的异常。您可以添加一个额外的抽象客户端层来很好地处理这个问题。您可以注入一个 IPromptableCommandHandler,它在编译时只有一个开放的通用实现,而不是直接将 commandhandler 注入到不同的演示者中:

public interface IPromptableCommandHandler<TCommand>
{
    void Handle(TCommand command, Action succesAction);
}

public class PromptableCommandHandler<TCommand> : IPromptableCommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> commandHandler;

    public PromptableCommandHandler(ICommandHandler<TCommand> commandHandler)
    {
        this.commandHandler = commandHandler;
    }

    public void Handle(TCommand command, Action succesAction)
    {
        try
        {
            this.commandHandler.Handle(command);
            succesAction.Invoke();
        }
        catch (Exception)
        {
            MessageBox.Show("An error occured, please try again.");
            // possible other actions like logging
        }
    }
}
// use as:
public void SetGuardActive(Guid guardId)
{
    this.promptableCommandHandler.Handle(new SetGuardActiveCommand(guardId),() => 
               this.RefreshGuardsList());

}

At the end of the day, for any given UI task (say the user creation request), does it end up that you end up having a winforms client based query/command, as well as a web api service version of the command/query which handles the request on that end?

没有!

客户端你应该创建一个单一的开放通用CommandHandlerProxy,其唯一任务是将命令 dto 传递给 WebApi 服务。

对于服务端架构,您应该阅读另一篇后续文章:Writing Highly Maintainable WCF Services,它描述了一个服务器端架构,可以很好地处理这个问题。链接的项目还包含 WebApi 的实现!