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

时间:2016-07-30 05:43:46

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

背景

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

  1. 第1层:使用MVP(Model-View-Presenter)设计模式Winforms客户端应用程序。
  2. 第2层:位于Entity Framework和SQL Server之上的WebAPI RESTful服务。
  3. 如果您想了解我正在构建的应用程序的更多详细信息,我提供了一个可能过于彻底的解释here

    当前发展

    目前,我正在Winforms客户端上工作。特别是,我试图在这个客户端中散列出适当的命令模式实现。我很幸运能够偶然发现this出色的博客文章,其中概述了一个坚实的命令架构。为了补充该帖子,作者followed up解释了他如何将查询与命令分开。在阅读这些博客之后,很明显我的第2层(web api服务)将从实现这两个方面大大受益。通用实现允许出色的灵活性,可测试性和可扩展性。

    问题

    对我来说不太清楚的是我如何在winforms客户端(第1层)上实现这些模式。查询和命令在这里继续被认为是独立的吗?考虑一个基本操作,例如登录尝试。这是查询还是命令?最终,您需要从Web服务返回数据(服务器上的用户信息),这样我才会认为这是一个查询。那么另一种情况呢,比如创建新用户的请求。我知道你会创建一个命令对象来存储用户信息并将其发送给服务。命令应该是火上浇油而忘记,但是你不希望从服务中获得命令成功的某种确认吗?此外,如果命令处理程序返回void,您如何告诉演示者用户创建请求是否成功?

    在一天结束时,对于任何给定的UI任务(比如用户创建请求),最终是否最终得到基于winforms客户端的查询/命令,以及最终的web api服务版本命令/查询在那一端处理请求?

1 个答案:

答案 0 :(得分:3)

  

此处的查询和命令是否继续被视为独立?

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

我们假设您要为某个区域指定一名特定的后卫。该命令(仅为DTO)所需的唯一信息是守卫的Id和该区域的Id。关联的CommandHandler将执行处理此操作的所有任务,例如从另一个区域移除该警卫,将他预订为不可用等。

现在您的UI想要显示更改。用户界面可能包含所有警卫及其指定区域的某种列表。此列表将由一个GetActiveGuardsAndAreaQuery填充,该List<GuardWithAreaInformationDto>将返回DTO。此Active Directory可以包含有关所有警卫的各种信息。从命令中返回此信息并不是关注点的清晰分离,因为原子命令处理可以从类似但稍微不同的UI中很好地使用,这将需要略微不同的UI信息更新。

  

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

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

使用Query可以很好地完成您所使用的模式。通过以模态形式询问用户,从中读取它,确保它们是服务于服务的登录凭据配置文件,或其他什么。

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

  

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

虽然IPromptableCommandHandler似乎没有返回任何内容,但事实并非如此。因为如果它没有因某些异常而失败(明确的消息出错了!)它必须成功。

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

总结一下,从失败的命令中抛出明确的异常。您可以添加一个额外的抽象客户端层来很好地处理这个问题。您可以注入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()); } ,而不是将命令处理程序直接注入到不同的演示者中,CommandHandlerProxy在编译时只有一个开放的通用实现:

<Route path="/" component={App}>
  <Route path="/" component={Auth}>
    <Route path="login" component={Login} />
    <Route path="register" component={Register}/>
  </Route>
  <Route path="about" component={About} />

  <Route path="*" component={NoMatch}/>
</Route>
  

在一天结束时,对于任何给定的UI任务(比如用户创建请求),最终是否最终得到基于winforms客户端的查询/命令,以及最终的web api服务版本命令/查询在那一端处理请求?

没有!

客户端你应该创建一个开放的通用MATCH (n:User{user_id:304020})-[r:know]->(m:User) with m MATCH (m)-[s:like|create|share]->(o{is_active:1}) with m, s, o, (toInt(timestamp()/1000)-toInt(o.created_on))/86400 as days, (toInt(timestamp()/1000)-toInt(o.created_on))/3600 as hours, (1- round(o.impression_count_all/20)/50) as low_boost with m,s,o,days,low_boost,hours, CASE WHEN days > 30 THEN 0.05 WHEN days >=20 AND days <=30 THEN 0.1 WHEN days >=10 AND days <=20 THEN 0.2 WHEN days >=5 AND days <=10 THEN 0.4 WHEN days >=2 AND days <=5 THEN 0.5 WHEN days =1 THEN 0.6 WHEN days < 1 THEN CASE WHEN hours <= 2 THEN 1 WHEN hours > 2 AND hours <= 8 THEN 0.9 WHEN hours > 8 AND hours <= 16 THEN 0.8 WHEN hours > 16 AND hours < 23 THEN 0.75 WHEN hours >= 23 AND hours <= 24 THEN 0.7 END END as rs, CASE WHEN low_boost > 0 THEN low_boost WHEN low_boost <= 0 THEN 0 END as lb where has(o.trending_score_all) and has(o.impression_count_all) and not(o.is_featured=2) RETURN distinct o.story_id as story_id, (o.trending_score_all*4) as ts, (o.trending_score_all + rs + lb) as final_score, count(s) as rel_count,max(s.activity_id) as id, toInt(o.created_on) as created_on ORDER BY (CASE WHEN ts > 3 THEN final_score desc, rel_count desc ELSE ts) END) DESC skip 0 limit 10; ,其任务是将命令dto传递给WebApi服务。

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