【问题标题】:How to use command pattern in a WinForms client application?如何在 WinForms 客户端应用程序中使用命令模式?
【发布时间】:2016-07-30 05:43:46
【问题描述】:

背景

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

  1. 第 1 层:使用 MVP(Model-View-Presenter)设计模式的 Winforms 客户端应用程序。
  2. 第 2 层:位于 Entity Framework 和 SQL Server 之上的 WebAPI RESTful 服务。

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

当前发展

目前,我正在开发 Winforms 客户端。特别是,我正在尝试在此客户端中充分实现命令模式。我很幸运地偶然发现了this 优秀的博客文章,它概述了一个可靠的命令架构。为了补充那篇文章,作者followed up 解释了他如何将查询与命令分开。阅读这些博客后,很明显我的第 2 层(Web api 服务)将从实现这两者中受益匪浅。通用实现具有出色的灵活性、可测试性和可扩展性。

问题

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

归根结底,对于任何给定的 UI 任务(比如用户创建请求),您最终是否会拥有一个基于 winforms 客户端的查询/命令,以及一个 web api 服务版本的处理该请求的命令/查询?

【问题讨论】:

  • Ric 的回答很到位。我没有什么要补充的。

标签: c# .net winforms asp.net-web-api simple-injector


【解决方案1】:

查询和命令在这里是否继续被认为是分开的?

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

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

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

例如登录尝试。这是查询还是命令?

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

对于您所指的模式,可以使用AuthenticateToWebApiServiceCommandHandlerDecorator 很好地完成此操作,通过以模式形式询问用户,从配置文件中读取它,或随便。

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

如果命令处理程序返回 void,你将如何告诉演示者用户创建请求是否成功?

虽然void 似乎没有返回任何东西,但这并不是真的。因为如果它没有因某些异常而失败(带有明确的信息,出了什么问题!)它一定是成功的。

在提到的博客文章的follow up 中,@dotnetjunkie 描述了一种从命令返回信息的方法,但请注意帖子顶部添加的评论。

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

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());

}

归根结底,对于任何给定的 UI 任务(比如用户创建请求),您最终是否会拥有一个基于 winforms 客户端的查询/命令,以及一个 web api 服务版本的处理该请求的命令/查询?

不!

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

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

【讨论】:

  • 再次感谢@Ric,感谢您的宝贵时间!为了确保我理解所说的内容,让我重新散列一下:对于一个命令,winforms 客户端将创建一个命令对象(一个 DTO)。此命令对象将作为句柄方法中的参数传递给命令处理程序。但是,winforms 客户端中的命令处理程序只是一个通用代理,它将命令传递给实际命令所在的 web api(即可以将代理注入 PromptableComandHandler)。这听起来正确吗?查询是否也以相同的方式通过代理处理?
  • @Andrew 是的,你理解正确。查询遵循相同的模式。请注意,上述代理实现了 ICommandHandler,但它是一个开放的通用实现,它以相同的方式处理所有命令
  • 它们可以由其他DTO组成,只要最深的级别只有属性而没有行为。他们应该完全没有行为,这是这个设计的核心。
  • @Andrew:WebApiCommandHandlerProxy&lt;T&gt; 实现(SOLIDServices 项目中缺少)通常应按如下方式构造 URL:"api/commands/" + typeof(TCommand).Nameapi/{controller}/{action}/{id} 存在只是因为我从未删除它。您通常不需要它,因为一切都是命令或查询。
  • @Steven:虽然这会有所帮助,但绝不会如此!你的博客已经帮了我很多。 :)
猜你喜欢
  • 1970-01-01
  • 2020-04-03
  • 1970-01-01
  • 2019-05-11
  • 2010-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多