65.9K
CodeProject 正在变化。 阅读更多。
Home

你真的想敏捷吗?

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (50投票s)

2011年12月29日

CPOL

44分钟阅读

viewsIcon

111710

downloadIcon

761

通过面向对象编程,在野外漫步。

目录

引言

在本系列“面向对象编程”的最新一期中,我将探讨一些可能性,以自动生成关系模型中实体的“用户”界面。其目的是动态生成友好的用户界面,并包含指向已定义关系的导航链接。当然,更改模型应该会立即反映在用户界面的更改中,而无需重新编译代码。

此刻我对“罗密欧与朱丽叶”模型感到有些厌倦,并且偶然发现了Dylan Morley在CodeProject社区发布的关于开源敏捷工具的帖子,我心想,嗯,为什么不制作一个真正可用、也许有一定实用性的敏捷项目管理应用程序呢?

当您阅读这篇冗长的文章时,您会注意到我是一边设计一边实现的。我喜欢这种文章写作方式,因为它能让您(读者)更好地理解我的思维过程,而且我认为,它比讨论一个经过打磨的代码片段“更有活力”。

敏捷项目管理

在浏览了无数关于这个概念的网站后,我可以肯定地说,我完全不知道敏捷项目管理(APM)到底是什么。似乎每个人对实现APM应用程序都有不同的想法,但没有人真正有一个关于APM应用程序(APMA)应该向最终用户提供什么的规范。也许这就是APM的本质,理论上它本身就是以敏捷项目开发方式编写的。对我来说,描述APMA规范的最佳方式似乎是实际使用敏捷实践,但似乎没有人这样做过!所以,就从这里开始吧。

似乎每个APMA都围绕着三件事

  1. 用户故事(也称功能)
  2. 任务(也称实际工作)
  3. 迭代(也称用户故事已更改,或也称“这就是您现在能得到的”)

除此之外,还有一些额外的概念

  1. 项目
  2. 负责执行任务的人
  3. 验收测试(也称单元测试)
  4. Bug(什么,敏捷开发会产生Bug?)

然后还有一些松散的概念

  1. 用户反馈
  2. 在制品(也称看板
  3. 冲刺

最后是报告

  1. 速度
  2. 迭代燃尽图
  3. 发布燃尽图
  4. 范围蔓延
  5. 燃起图

如果我或其他人对进一步的开发感兴趣,我可能会考虑生成报告,但现在还不行。

无处不在的电子表格

电子表格是解决一切问题的终极答案。不是42。Excel。APMA也不例外——基本上,如果你有电子表格,你就能进行APM。坦率地说,如果你有笔和纸,你就能进行APM,但我们保守这个秘密。

如果你看过《火线警告》,请用“迈克尔·韦斯特”的声音阅读以下内容

APM可以在电子表格中建模,这表明我们可以轻松地使用开发人员的万能工具“数据网格”来实现相同的概念。因此,虽然使用数据网格很容易,但也很无聊,而且有时并不是最佳解决方案,特别是当您拥有不适合150像素宽字段的数据时,例如描述。为此,我们需要更聪明,并实现独立控件。

[迈克尔·韦斯特的旁白结束。]

我的APMA用户故事(APMAUS)

这时,我的猫爪子按到了FrontPage上的某个功能键,我丢失了大约30分钟的文本。所以,重新开始,这是我的APMA用户故事

  1. 我同时有许多项目(不一定是所有编程项目),与Microsoft不同,Microsoft的项目是解决方案,解决方案中的项是项目,我喜欢以项目的角度来思考。客户不太可能像《耶稣基督万世巨星》中的该亚法那样对你说:“我们需要一个……解决方案来解决我们的问题”,而更可能是说:“我有一个项目。”
    1. 一个项目,最少,需要有一个名称和一个简短的描述。
    2. 项目由用户故事组成。这是“规格”和“需求”的技术新潮替代品。在某些方面,用户故事实际上更好,因为它们可以捕捉用户希望如何与应用程序交互,而技术规格或应用程序需求通常在较低级别捕获细节,描述功能而非用法。
    3. 我应该能够查看与项目相关的用户故事。
    4. 我应该能够查看与多个项目或所有项目相关的用户故事。
  2. 不幸的是,用户故事存在与良好需求文档相反的问题。它们过于松散,更多地关注如何做,而不是要做什么。
    1. 用户故事应该能够被深入挖掘,将不切实际的幻想提炼成更具体的东西。这需要客户的积极参与,甚至更糟,可能需要多人协调和达成共识。经理对软件应该如何工作的想法与实际使用该应用程序的秘书截然不同。如果你让会计师参与进来,那就别想了!
    2. 深入挖掘用户故事,更重要的是,将故事的“所有权”分配给用户,这是很重要的。
      1. 它回答了“为什么你要这样实现?”这个问题。
      2. 它回答了“为什么成本这么高?”这个问题。
      3. 用户故事需要一个所有者和一个描述字段,并能够有子故事,可能来自不同的所有者。现在就开始学习你的非暴力沟通技巧吧。
    3. 为用户故事带来清晰度,会增加您向客户交付他们所需内容的成功机会。奇怪的是,在我访问过的所有敏捷网站上,我从未读到过这一点。
  3. 一旦用户故事从幻想提炼成可操作的工作(诅咒那个用户故事!),我们就可以创建任务。
    1. 任务实体属于一个用户故事。
    2. 任务不一定是编程任务。它们可能是对于一个模糊的用户故事来说“从客户那里获取更多信息”。
    3. 任务有一个简短的名称、一个描述、一个对用户故事的引用,并且根据每个人所说,任务应该有一个以某种度量单位完成的估算。这是模糊的部分。小时、天、周?
    4. 开发人员并不比客户擅长最初定义用户故事,他们也不擅长定义任务。因此,任务一开始很模糊,估算也很离谱,需要分解成更小的任务。任务越小,其完成的估算就越好(我在所有敏捷网站上都没有读到过这一点)。
    5. 估算的一个奇怪之处在于,模糊任务的估算可能不是子任务估算的总和。这一点非常重要(这也是我在任何敏捷开发网站上都未读到的!)。
      1. 程序不应仅仅将子任务估算相加来自动计算超任务估算。
      2. 它应该允许顶层任务的估算独立存在。
      3. 这是发现一些事情的极其有用的方式
        1. 如果我的高级任务估算高于子任务估算,那么这表明我还没有弄清楚所有的子任务,因为凭直觉,我知道超任务的工作量比我定义的子任务要多。
        2. 如果我的高级任务估算低于子任务估算,那么这表明我正在发现一些在第一次估算超任务时未考虑到的复杂性。哎呀,我们从来没有遇到过这种情况,对吧?
        3. 如果我的高级任务估算与子任务估算匹配,那么我就做错了。我说真的。这表明有人在将子任务估算调整到超任务估算中。在任务估算阶段,犯错没什么关系,尤其是。允许自己犯错,因为它是为任务带来进一步清晰度的有用指标!(你也没读到过这个,对吧?)
        4. 显示估算和计算(汇总)估算都是有用的!
  4. 我提到用户故事处于钟摆的另一端(需求在另一端)。
    1. 我们需要能够识别不属于用户故事的需求、规则、规格等。用户故事不太可能说“在SQL Server中实现”。用户故事可能说“我们需要支持1000个并发用户”,但具体如何实现的技术细节并不存在。
    2. 因此,我们需要能够将需求、规则、技术规格、功能等与用户故事相关联。
    3. 任务也是如此!
  5. 文档!一切都可以关联一个(或两个)文档:项目、用户故事、任务。
    1. 文档本身也可以包含文档(修订版、进一步阅读等)。
    2. 文档可以有多种形式:Word、Excel、图片、电影、URL、书籍、书中的特定段落、文件柜里的纸张(如合同)。
  6. 到此时,您可能已经注意到这听起来更像一个层次结构而不是一个网格,您说得对。树形控件是开发人员工具箱中的另一个“万能工具”,但它实际上是电子表格的补充,因为您可以在Excel中轻松实现分层信息。在网格中并不容易,而且我们绝对不想谈论DataSet,因为我们的关系不像DataSet要求的那样僵化。
    1. 到目前为止,可视化项目、用户故事、任务和需求以树状形式可能很有用,但网格也有其用途,所以我们会看到的。
    2. 这就引出了用户故事的一个有趣观点:有时,在我们实际试驾之前,我们真的不知道最佳实现是什么。
      1. 出于设计目的,拥有“方法”的概念可能很有用。这是我在敏捷开发中从未读过的另一件事。
        1. 方法是一种实现概念,您在尝试之前不知道它是实现某事的好的方式。也许需要几种不同的方法(可视化、算法等)。也许我们只想有一个占位符来评估不同方法的任务和需求。也许我们想做一些原型并展示给客户。卖更贵的那个,那样能赚更多钱。对吧?
        2. 方法是可以附加到用户故事上的。
        3. 方法可以有任务!
  7. 迭代。这些是什么?好吧,一种概念是,它是在现在和下一次“发布”(APM中另一个松散的术语)之间的时间窗口。您和客户可以确定迭代计划,然后找出什么可以包含在该计划中。
    1. 迭代包含任务。
    2. 迭代有一个持续时间。
    3. 它可能还应该有一个名称。
    4. 迭代应该有一个标志来指示它是内部迭代,或者它可能是我们想向客户展示的东西,并且它可能构成官方发布、补丁、修订版等给用户/网站等。
    5. 任务分配给迭代。
      1. 如果我们有来自不同方法的任务,我们需要小心选择任务。我们是否已经决定了某个特定方法,因此其他方法的任务应该被“弃用”?我们是否允许人们处理来自不同方法任务?在迭代中分配任务时需要考虑这一点。
  8. 敏捷开发的一种变体是精益开发,您有时会听到“看板”这个词,它只不过是三个列
    1. 待办工作
    2. 进行中的工作
    3. 已完成的工作
  9. 看板的三个列属于报告的范畴,因为我们应该随时能够查询未分配的任务、已分配但未完成的任务以及已完成的任务。
    1. 除了任务,另一个有用的看板图是用户故事的待办列表、进行中的用户故事以及已完成的用户故事。
    2. 我有没有提到在本期文章系列中我不会生成任何报告?
      1. 这本身就是APM中处理不好的一个概念:未来版本。
      2. 用户故事应该能够规划应用程序的“发展轨迹”。我们都对应用程序应该做什么有一些绝妙的想法,但我们希望能够将所有这些绝妙的想法分解为“立即必须拥有”和“版本10中的一个很棒的功能”等类别。当团队正在处理版本1时,人们不应该从版本10中挑选任务!
      3. 因此,用户故事应该有一个版本与之关联。我推迟的报告故事可能属于版本20。
  10. 谁来做这项工作?嗯,人来做,有时工具也来做。
    1. 人很简单。我们有一个Person实体,带有一个姓名和一些联系信息。
    2. 然而,工具是敏捷所忽略的另一个概念,APM也忽略了这一点。
      1. 一个工具用于一个流程,该流程可能应该被记录。它可以包括“运行混淆器”、“使用NetZ打包所有程序集”、“在SVN中创建分支”等。识别实现任务的自动化流程很重要。重要的是查看任务并考虑任务是否可以自动化!
      2. 一个流程可以分配给任务。
      3. 工具的描述——它们有一个名称,可能还有其他信息。五年后您修改应用程序时,拥有一个重建应用程序所需的工具列表,不是很方便吗?拥有自动化流程以及如何使用它们的列表,不是很方便吗?
      4. 流程不只是指自动化。它还可以指“获得部门主管的批准”。
      5. 因此,工具和流程是您工作方式以及您成功向客户提供产品的重要组成部分。记录这些用于任务的工具和流程很有用!
    3. 人承担任务。我们想知道每个人在做什么任务,以及他们已经完成了什么。
      1. 在某个时候,我们可能想看看一个人的工作量。他们是否有空闲时间?我们需要改变优先级吗?这个人是否因工作而累垮了?
      2. 任务需要能够被优先排序!这就是现实世界——有事情需要立即处理。
  11. 测试。测试驱动开发。验收测试。单元测试。这些是没人喜欢做的东西的流行语,而且做这些事情和写报告或写应用程序本身一样耗时。真的。如果有人告诉你不同,那他们在撒谎。测试对你(开发人员)很重要,因为它是衡量里程碑达成情况的一种方式,而里程碑通常与付款挂钩。测试对客户很重要,因为它是确定正在进行高质量工作以及是否进行里程碑付款的一种方式。但没有人想做。尽管如此
    1. 任务应该有一些测试,通常是单元测试,但也可以是简单的验收测试。
    2. 用户故事应该有一个验收测试,由用户执行,并且用户的AT应该与开发人员的AT不同,因为它们通常测试不同的东西。
    3. 测试可能是自动化的。啊哈。它可能需要一个工具。它可能有一个流程(例如,用测试数据初始化数据库)。
      1. 工具和流程可以与测试相关联!
    4. 测试需要一个名称、一个描述,并且测试运行与任务或用户故事的关联应该有一个通过/失败标准。
  12. Bug。Bug是任何不按我们期望方式工作的東西。
    1. 失败的测试就是Bug。
    2. 但Bug是在正式测试之外发现的。因此,Bug与任务、用户故事,甚至项目相关联。Bug不一定适合用户故事。如果Bug是“当两个人同时使用应用程序时,应用程序会挂起”,那么可能没有“必须处理两个人负载”的用户故事。我们想记录Bug,将其关联到项目,然后在以后确定缺失的用户故事是什么,然后我们就可以将Bug与用户故事关联起来。
    3. Bug有任务。毕竟,我们需要修复Bug,对吧?错了!(敏捷开发中读不到的另一件事!)Bug可能有解决方法(啊,有用的信息)或可能被委托给将来的版本(关联!)。一个人的Bug可能是另一个人的需求,因此任务可能是“用户培训”。想想看:即使您输入了一个错误消息,因为Web服务失败了,用户仍然会说“您的应用程序不起作用”,他们需要的是关于谁(所有权)对失败负责的培训,无论您显示多少有用的消息。我迄今为止最喜欢的消息是Oracle 9中的,它声称死锁条件“不是Oracle的Bug,而是您的应用程序中的Bug”。有点像那样,我不是在开玩笑(Oracle 10中 subsequently 修复了!)。
    4. 用户会将任何问题视为Bug,而开发人员则不会。如果您愿意,保留两套“账本”可能会很有用。

够了!

我可以继续说下去,但我猜您已经跳过了以上大部分内容,即使我努力幽默和讽刺以使您,亲爱的读者,感到娱乐。但是,正如您从以上对我的Gloriosky APMA(GAPMA)的描述中所看到的,用户故事不是整洁的小包。一个用户故事会影响其他用户故事。用户故事应该能够引用其他用户故事(这本身就是一个用户故事——您以为我完了吗!)。

现在,您可能在想,天哪,这需要几周才能实现。荒谬!这只需要几个小时,使用我巧妙的万能开罐器——面向对象编程工具(ROPT)。啊,但是看,我的用户故事中 nowhere 提到了使用这个工具作为构建GAPMA的要求。您立刻就能明白为什么我创建了可以附加到任何东西上的“需求”的概念。使用ROPT是实现GAPMA的要求。快速说十遍。冥想它。相信它。我希望您被洗脑,认为这是唯一的方式、真理的方式、唯一的方式!

模型

ROP的一个要点是,您可以从简单开始,根据需要创建实体、属性和关系,而无需预先架构整个系统。这才叫敏捷!所以,让我们从一个基本模型和一些简单的关系开始。我首先要建模的实体组是

  • 项目
  • 用户故事
  • 任务

关系是

  • 一个项目有用户故事
  • 一个项目有任务
  • 一个用户故事有任务
  • 用户故事可以包含用户故事
  • 任务可以包含任务

基本上,由于许多实体的钻取性质,可递归实体都引用一个项目。使用ROPT

或者,以Schema形式

并且我们得到了一个基本的“超级用户”UI供开始使用(这个显示了Task实体)

然而,这并不是我们想要提供给GAPMA用户的东西。

  1. 我们不想显示ID字段。
  2. 指向其他实体的查找应该有漂亮的标题,而不是“...ID”。
  3. 描述字段分配了超过4000个字符!我们真的想为此使用150像素宽的字段吗?我想不是!
  4. “父项”(对于Task实体,用户故事、任务和项目)应该以某种其他方式显示。

但这并不是我们想要的模型!

此外,我上面创建的是一个物理模型(表、字段、表之间的关系)。上述模型绝对有用,我们应该继续将其作为物理模型的文档维护(实际上,ROPT应该能够随时生成此物理模型,只是我还没有实现可视化),但我们真正想要的是一个元模型,使用我在上一篇文章中创建的ROP物理模型。元模型描述了GAPMA物理模型,但在元级别,它给了我们

  1. 无需修复物理模型即可添加字段和创建新关系的能
  2. 使关系成为实体之间的一等公民的能力。这是全部的要点。
  3. 通过使用元模型,我们可以轻松地跟踪关系更改、在实体之间导航、创建新关系、检查哪些实体与其他实体处于关系中,等等。在“挖掘”日益增长的用户故事、任务、Bug、测试、文档等之间的复杂性时,这些都是有价值的事情!
  4. 这就是元建模所实现的:抽象实体及其关系,以便您不必为每个具有外键的表编写临时的自定义可视化器和报告。
  5. 由于像“Name”这样的属性类型被各种实体使用,您可以做一些疯狂的事情,比如“所有实体的Name是什么,无论它与哪个实体相关?”听起来很傻,但对于“所有名为‘foo’的东西”进行数据挖掘可能很有用。

这才是我们想要的

这一切都在ROPT中使用*rop.model*文件创建,这是我们的元建模模式(听起来比“我们的元建模模型”更好)。

首先,我们有一组简短的属性类型(请注意,没有ID*属性*。在元模型级别,我们不再需要这个——外键已经通过关系实例抽象出来了。

我们的三种实体类型

实体属性描述

以及关系类型的描述

我们现在可以使用ROP内置的超级用户界面用实例信息填充元模型——这是一个定义实体属性实例的物理值的屏幕示例

但这比我们想要的最终用户看到的还要远!所以,我们需要创建一个用户界面(类似于上面的),不是从物理模型,而是从元模型。是的,这就是难点。

顺便说一句,这里有一个有趣的观点:请注意,元模型如何在同一个DataSet中描述模式(...Type元素)和实例(...Instance元素)。这一点很重要,模式(描述类型和关系)和数据在同一个“包”中。这一点为什么重要有点难以描述,但听起来不错!

从元模型动态生成用户界面

因此,我们终于到达了本文的重点,即从元模型动态生成用户友好的UI。为物理模型创建UI(毕竟,第一个截图显示的就是这个)相对直接,所以这是如何为元模型创建管理实体实例UI的线索。让我们先从Project实体开始。对于所有实体,我们想收集两样东西

  1. 实体的属性类型(用于查看和编辑数据)
  2. 实体的关系类型(用于导航关系)

我们还希望对“数据管理”部分的UI行为进行一些控制——用户可以在其中查看和管理(编辑、删除、创建)数据。我认为有三种基本选择

  1. 仅网格
  2. 网格和独立控件
  3. 仅独立控件

描述属性的顺序很重要,因此我在ROP模型中的EntityTypeAttributes定义中添加了“Ordinality”,这将帮助我们按顺序排列表单中的字段。

此外,我们需要关于AttributeType本身的更多信息,因此我向AttributeType实体添加了DataType和Length属性。

另外,我添加了以下菜单项

这会打开一个基本表单,用于选择实体类型以及一些初始的简单选项,说明我们希望如何渲染UI。

项目UI

让我们从创建简单的Project实体UI开始。我也将从一个基本网格开始,因为有几个部分需要实现

  1. 创建UI本身
  2. 创建底层的DataTable。这需要将实体-属性(它们是行)旋转为列
  3. 用数据填充底层DataTable。类似地,这需要将实体实例数据(表示为离散的属性-值对)旋转为列,其中实体实例的所有属性代表底层表中的一行。
  4. 反过来用于管理实体实例的ROP表示。为此,我将使用我几年前写过的DataTable事务记录器。
  5. 创建通常的组件:DataViewBindingSource(它也将用于处理独立控件)

最后,问题是,如何生成UI?我是通过在空白表单上运行时创建控件来完成的吗?我应该像Visual Studio的表单设计器那样生成代码吗?我应该创建一个UI的XML对象图吗?是的,我就是我,这似乎是最有用的方法!我可以创建XML,用MyXaml生成UI,这是一个易于持久化和扩展的文档。理想情况下,生成的XML对象图应该包含与ROP工具无关的UI工作所需的所有组件。听起来像用户故事,不是吗?

但是,存在一些复杂性,最好是通过先构建一个非常简单的概念验证来发现这些复杂性,将一些核心底层代码弄对,弄清楚我们可能想要暴露哪些规则来控制UI生成的行为(在某个时候甚至可能能够使用模板来实现某些外观和感觉),并识别出需要解决的依赖项,如果我们要创建一个真正独立的UI功能。这听起来就像迭代和大量的重构,一旦我对自己在做什么有了更好的了解。

创建基本表单

首先,我们需要一个表单,像这样创建

XDocument xdoc = new XDocument();
XNamespace nsswf = "System.Windows.Forms, System.Windows.Forms, " + 
   "Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
XElement controls;
xdoc.Add(
  root = new XElement(nsswf + "MyXaml", 
    new XAttribute(XNamespace.Xmlns+"def", "Definition"),
    new XAttribute(XNamespace.Xmlns + "ref", "Reference"),
    new XElement(nsswf + "Form", 
      new XAttribute("Text", "Project"),
      new XAttribute("Size", "800, 600"),
      controls = new XElement(nsswf + "Controls"))));
    string xml = xdoc.ToString();

Clipboard.SetText(xml); // for debugging and viewing the generated XML.

Parser p = new Parser();
Form form = (Form)p.InstantiateFromString(xml, "*");
form.Show();

这就得到一个空白表单

XML看起来像这样(别担心,我不会每次都给您看XML)

<MyXaml xmlns:def="Definition" 
        xmlns:ref="Reference" 
        xmlns="System.Windows.Forms, System.Windows.Forms, Version=2.0.0.0, 
               Culture=neutral, PublicKeyToken=b77a5c561934e089">
  <Form Text="Project" Size="800, 600">
    <Controls />
  </Form>
</MyXaml>

创建基本DataGridView控件

这仅仅是创建了一个网格控件

protected void RopGrid(XNamespace ns, XElement controls, 
          List<AttributeInfo> attributes, ref int x, ref int y)
{
  XNamespace nsdef="Definition";
  controls.Add(new XElement(ns + "DataGridView",
    new XAttribute(nsdef + "Name", "grid1"),
    new XAttribute("Location", x + "," + y),
    new XAttribute("Size", "600, 400"),
    new XAttribute("Anchor", "Top, Bottom, Left, Right")));

  y += 150;
}

这就得到了一个基本锚定的网格控件

创建底层的DataTable列

底层DataTable的列由分配给实体类型的属性类型决定

protected List<AttributeInfo> GetEntityTypeAttributes(int entityTypeID)
{ 
  // Get the AttributeType records for the EntityTypeAttributes
  // records where the EntityTypeID = [entityTypeID]
  List<AttributeInfo> attributes = 
    (from eta in dataSet.Tables["EntityTypeAttributes"].AsEnumerable()
        where eta.Field<int>("EntityTypeID") == entityTypeID
        join at in dataSet.Tables["AttributeType"].AsEnumerable()
        on eta.Field<int>("AttributeTypeID") equals at.Field<int>("ID")
        orderby eta.Field<int>("Ordinality")
        select new AttributeInfo
        {
          ID=at.Field<int>("ID"),
          Name = at.Field<string>("Name"),
          DataType = at.Field<string>("DataType"),
          Length = at.Field<int>("Length"),
        }).ToList();

  return attributes;
}

然后,用一些非常粗暴的代码

DataTable dt = new DataTable("mydt");
int cnum=0;

foreach (AttributeInfo ai in attributes)
{
  DataColumn dc = new DataColumn("col" + cnum++);
  dc.Caption = ai.Name;
  dc.MaxLength = ai.Length;
  dt.Columns.Add(dc);
}

// We need an internal ID for the entity.
dt.Columns.Add(new DataColumn("ID", typeof(int)));

DataView dv = new DataView(dt);
BindingSource bs = new BindingSource();
bs.DataSource = dv;
dgv.DataSource = bs;

// Stupid fixup required because the DataGridView
// doesn't pay attention to the column Caption property.
for (int i = 0; i < dt.Columns.Count; i++)
{
  dgv.Columns[i].HeaderText = dt.Columns[i].Caption; 
}

// Hide the ID column
dgv.Columns[dt.Columns.Count - 1].Visible = false;

我们得到这个

有进展了!

记录事务

如上所述,我将使用DataTableTransactionLogger(DTTL)记录事务,然后将更改批量持久化到ROP模型,这需要将具体的DataTable实现转换回元模型,操作EntityInstance和EntityAttributeInstance表。

protected DataTableTransactionLog CreateTransactionLogger(DataTable dt)
{
  DataTableTransactionLog dttl = new DataTableTransactionLog(dt);

  return dttl;
}

由于事务不会立即持久化,我们需要实现一个“保存”按钮。

protected void RopSaveButton(XNamespace ns, XElement controls)
{
  controls.Add(new XElement(ns + "Button",
    new XAttribute("Location", "220, 10"),
    new XAttribute("Size", "70, 25"),
    new XAttribute("Text", "&Save"),
    new XAttribute("Anchor", "Top, Right"),
    new XAttribute("Click", "{App.OnSave}")));
}

在未来的某个版本(抱歉,是*迭代*)中,我们可以添加撤销/重做功能,DTTL支持这些功能,这也是我没有使用.NET本机记录更改记录的原因之一(其他原因包括能够将更改从我的记录器发送到中间层,而不是被困在2层范式中。请不要提到Entity Framework——如果我再听到ORM这个缩写,我就会……)

以下代码(记住,这是概念验证)将在元模型中保存事务。这包括将水平物理结构(行和字段)转换为垂直结构(实体实例和实体实例属性值)。

protected void OnSave(object sender, EventArgs eventArgs)
{
  // The log.
  List<DataTableTransactionRecord> log = gridInfo.Logger.Log;

  // The tables we're going to be manipulating.
  DataTable dtEntityInstance = dataSet.Tables["EntityInstance"];
  DataTable dtEntityAttributeInstance = 
                dataSet.Tables["EntityAttributeInstance"];
  int entityID = -1;

  foreach (DataTableTransactionRecord dttr in log)
  {
    switch (dttr.TransactionType)
    {
      case DataTableTransactionRecord.RecordType.NewRow:
      {
        DataRow newEntityRow = dtEntityInstance.NewRow();
        newEntityRow["EntityTypeID"] = entityTypeID;
        dtEntityInstance.Rows.Add(newEntityRow);

        entityID = newEntityRow.Field<int>("ID");

        // Don't log setting the ID field.
        gridInfo.Logger.SuspendLogging();
        dttr.Row["ID"] = entityID;
        gridInfo.Logger.ResumeLogging();

        break;
      }

      case DataTableTransactionRecord.RecordType.ChangeField:
      {
        // A bizarre piece of code that needs to abstract the column
        // caption from the model's field name which isn't 
        // the table's column name.
        string fieldName = gridInfo.DataTable.Columns[dttr.ColumnName].Caption;
        int attributeTypeID = dataSet.Tables["AttributeType"].Select(
            "Name='" + fieldName + "'")[0].Field<int>("ID");
        DataRow[] rows = dtEntityAttributeInstance.Select(
            "EntityID=" + entityID + " and AttributeTypeID=" + attributeTypeID);
        bool recordExists = (rows.Length > 0);

        // If the entity attribute instance record exists,
        // then change the existing value...
        if (recordExists)
        {
          rows[0]["Value"] = dttr.NewValue;
        }
        else
        {
          // ... otherwise create a new record.
          DataRow newAttrInst = dtEntityAttributeInstance.NewRow();
          newAttrInst["AttributeTypeID"] = attributeTypeID;
          newAttrInst["EntityID"] = entityID;
          newAttrInst["Value"] = dttr.NewValue;
          dtEntityAttributeInstance.Rows.Add(newAttrInst);
        }

        break;
      }

      case DataTableTransactionRecord.RecordType.DeleteRow:
      {
    foreach(DataRow row in dtEntityInstance.Select("ID = " + 
                           dttr.ColumnValues["ID"]))
        {
          row.Delete();
        }

        break;
      }
    }
  }

  gridInfo.Logger.ClearLog();
  dtEntityInstance.AcceptChanges();
  dtEntityAttributeInstance.AcceptChanges();
}

现在,当我们创建一些用户友好UI中的条目时

我们可以在超级用户元模型数据集中看到这些条目

更进一步,我们可以钻入每个“Project”实体实例以查看实例属性值,方法是选择一行并导航到详细记录。

揭示(第一个ID为0的实体实例)

这样,我们可以随时检查和验证我们的用户友好具体支持表、事务记录和元模型更新。由于此对话框是非模态的,您可以实时看到实例实体发生的更改。

加载具体的DataTable

用所选类型的实体实例加载具体的支持DataTable很简单。这是保存过程的反向过程,这里将垂直实体实例和实体实例属性值转换为水平行-字段物理表。请注意填充隐藏的ID列值,以便我们支持更新和删除现有记录。以后我发现需要保留EntityID值!

protected void LoadTable()
{
  DataTable dtEntityInstance = dataSet.Tables["EntityInstance"];
  DataTable dtEntityAttributeInstance = dataSet.Tables["EntityAttributeInstance"];
  DataTable dtAttributeType = dataSet.Tables["AttributeType"];

  DataRow[] entityInstances = 
    dtEntityInstance.Select("EntityTypeID=" + entityTypeID);

  foreach (DataRow entityInstance in entityInstances)
  {
    DataRow row = gridInfo.DataTable.NewRow();
    int entityID = entityInstance.Field<int>("ID");
    row["ID"] = entityID;

    DataRow[] attributeValues = 
       dtEntityAttributeInstance.Select("EntityID=" + entityID);

    foreach (DataRow attrVal in attributeValues)
    {
      // TODO: Decouple the association of the attribute name to the column header.
      int attributeTypeID = attrVal.Field<int>("AttributeTypeID");
      string name = dtAttributeType.Select("ID=" + 
                       attributeTypeID)[0].Field<string>("Name");
      row[nameColumnMap[name]] = attrVal["Value"];
    }

    gridInfo.DataTable.Rows.Add(row);
  }
}

项目UI作为混合屏幕

动态创建独立控件是一门艺术,而非科学。我说的不是技术,而是创建智能布局的规则。这(而且事实上,理想情况下,我认为用户应该能够根据自己的意愿调整布局)不是我想在这里探索的,所以您将看到一些概念验证实现(这是委婉地说“暴力且丑陋无比”)。首先

case Mode.Hybrid:
{
  RopGrid(nsswf, controls, attributes, ref x, ref y, "Top, Left, Right");
  CreateDiscreteControls(nsswf, controls, attributes, ref x, ref y);

  break;
}

使用一个非常简单的实现

protected void CreateDiscreteControls(XNamespace nsswf, XElement controls, 
          List<AttributeInfo> attributes, ref int x, ref int y)
{
  int n = 0;

  foreach (AttributeInfo attrInfo in attributes)
  {
    RopLabel(nsswf, controls, attrInfo, ref x, ref y);
    RopTextBox(nsswf, controls, attrInfo, ref x, ref y, "ctrl" + n++);
    x = 10;
    y += 5;
  }
}

是的,目前一切都是TextBox。就像我说的,概念验证。

当然,控件需要绑定到binding source

protected void BindControls(Parser p, BindingSource bs, List<AttributeInfo> attributes)
{
  int n = 0;

  foreach (AttributeInfo attrInfo in attributes)
  {
    string ctrlName = "ctrl" + n;
    string colName="col"+n;
    n++;
    Control ctrl = (Control)p.GetReference(ctrlName);
    ctrl.DataBindings.Add("Text", bs, colName);
  }
}

请注意硬编码的控件名称和列名。在待办事项列表中需要清理这些。一旦GAPMA基本功能实现,我将把这些待办事项作为任务添加!总之,初始结果是

哇哦。别太 impressed。

然而,一个问题是,添加新记录。如果我在网格中的新行上单击,然后在离散文本框之一上单击,网格就会退出其“新行”状态,并恢复到前一个现有行。这不是我们想要的。这就需要一个“新建”按钮,它只需创建一个空白行(这就引出了“如果某个值是必需的怎么办?”这样的问题,啊,未来的迭代任务!)

protected void OnNew(object sender, EventArgs eventArgs)
{
  DataRow row = gridInfo.DataTable.NewRow();
  gridInfo.DataTable.Rows.Add(row);
  int id = row.Field<int>("ID");
  int pos = -1;

  // What a lame way of finding a row. Can't use sort,
  // because the view might be sorted by the grid.
  // This means we have to iterate through all the records???
  foreach (DataRowView drv in gridInfo.DataView)
  {
    ++pos;

    if (drv.Row.Field<int>("ID") == id)
    {
      break;
    }
  }

  gridInfo.BindingSource.Position = pos;
}

结果是正确的行为(注意排序的Name列)

项目UI作为独立控件

在这种情况下,我们需要“保存”和“新建”按钮以及一个BindingNavigator来帮助在非网格显示中导航记录。

case Mode.Discrete:
{
  bindingNavigatorOffset = 25;
  RopBindingNavigator(nsswf, controls, ref x, ref y);
  CreateDiscreteControls(nsswf, controls, attributes, ref x, ref y);

  break;
}

并初始化BindingNavigator

if (renderMode == Mode.Discrete)
{
  BindingNavigator nav = (BindingNavigator)p.GetReference("nav1");
  nav.AddStandardItems();
  nav.BindingSource = bs;
}

这就得到了独立控件版本的可用初始尝试

足够简单。然而,有一次,在添加记录后,我遇到了一个异常,我还没有能够重现。

顺便说一句,您可以做的一件有趣的事情是同时打开“Model -> View Data”表单和“Model -> Render Entity Type UI”表单。当您在实体UI中保存更改时,您可以同时看到元模型数据(例如,如果您选择了“EntityInstanceAttributes”)在更新。这对我发现多个删除不起作用很有帮助。这导致了一个恼人的发现,即新添加的记录在用户导航离开记录之前不会退出“编辑”模式,因此在保存例程中需要调用EndEdit。

protected void OnSave(object sender, EventArgs eventArgs)
{
  // Finish edits to get all transactions into the logger.
  gridInfo.BindingSource.EndEdit();

现在我们可以创建所有实体类型的UI了

上面的代码并不特限于Project实体类型——它适用于我们所有的实体类型(当然,在支持的控件程度上)。只需要对代码进行一个小小的修改,就可以在表单标题中显示适当的实体类型

string entityTypeName = dataSet.Tables["EntityType"].Select("ID=" + entityTypeID)[0].Field<string>("Name");
...
new XAttribute("Text", entityTypeName),

现在我们可以开始使用GAPMA输入我们的敏捷项目数据了!

处理关系

我在我的元模型中注意到,RelationshipType可以真正受益于引用实体A和B类型。在我之前的文章中,RelationshipType实例定义为

并且我们通过命名它们来定义关系类型

这些信息不足以提供UI的导航支持。要解决这个问题

我创建了两个新属性:EntityATypeIDEntityBTypeID

然后,我将这些属性添加到RelationshipType实体定义中

最后,定义了这两个字段与EntityType实体的关系

这现在允许我定义关系类型中的实体类型

简单吧?这很好地演示了修改模型、创建新关系以及立即更新模型数据以反映新属性和关系。我们必须感谢DataSet反序列化器,它在模式XML与正在加载的模式不匹配时不会抛出异常。上述过程依赖于这样一个事实:反序列化现有的XML数据集,当数据集中存在新的字段而XML中没有时,是可以工作的。反之(数据集模式缺少XML模式中定义的字段)当然不支持,但我认为这很好。

到目前为止,我有以下实体类型关系

关联实体实例

所以,既然我们已经解决了这个问题,我们就需要让用户能够将一个实体与一个“允许的”父项关联——类似于外键引用另一个(或同一)表中的主键记录。使用ROP,任何东西都可以与任何东西关联,但我们限制关联列表仅限于关系类型定义的关联,这就是上面练习的目的。

此时,我还添加了“Bug”和“Resolution”实体类型,并向系统中输入了一些目前未关联的数据

我实际上开始使用GAPMA了!

我们可以通过三种基本方式进行关联

  1. 在网格中提供一个字段,或者作为一个独立控件,从中选择父记录
  2. 有了父记录,显示零个或全部子记录,当用户添加新子记录时,自动将其关联到父项
  3. 提供一个独立的允许的父子实体列表,并允许用户创建子到父的关联或父到子的关联。

技术上来说,这三种版本都应该得到支持,但目前,为了保持简单,我将只实现第三种版本,原因如下

当存在大量可能的关联时,第一种选项开始变得丑陋。此外,它无法捕获某项可以与同一父项类型的不同实例有多个关联的可能性。在我处理我的产品套件时,我经常有一个任务需要对产品套件的几个组件执行。使用第一种选项,我如何指定任务与多个项目或用户故事相关联?

第二种选项是可行的,但将用户限制在“自上而下”的思维方式。显然,这有一些优点,例如数据输入——例如,发票上的明细行。另一方面,这可能是一种人为的限制,降低了用户使用应用程序的灵活性。

第三种选项消除了这些限制,允许用户以自上而下和自下而上的方式工作。我更喜欢这种方法来演示我试图演示的内容,然而,在实际操作中,它有点难以适应。

规则

显示父/子记录的一些规则

  1. 这种方法的一个基本规则是,一个实体不能与自身建立关系,因此,我们总是排除所选实体实例的父/子实例列表。
  2. 第二条规则是,一个实体实例只能与另一个实体实例建立一次关系。因此,已经与所选实体建立关系的实体实例被排除。
  3. 一个实体实例不能成为同一类型实体实例的父项和子项。例如,子任务(作为任务的子项)不能成为父任务的父项,这将使父任务成为其子任务的子任务。这会造成循环。
  4. 同样,一个实体实例不能成为不同类型实体实例的父项和子项。例如,任务不能是用户故事的子项,也不能是同一个用户故事的父项。
  5. 某些关系实际上是必需的。例如,由于用户故事和任务的层次结构性质,这些实体应该始终与项目相关联。这并不意味着用户被强制立即建立关联——相反,应该执行一个“检查”来告知用户关系要求的违规情况。

在写作时,我将不会在本期中实现这些规则!

选择父/子实体和实体记录

我们需要几个组合框来选择允许的父子实体类型。概念是,在RelationshipType实体中,EntityA始终是子项,EntityB始终是父项。“A”和“B”是如此糟糕的名称!最好添加一个任务

首先,让我们获取需要用来填充组合框的列表

/// <summary>
/// Returns a list of parent entity types given an entity type ID.
/// </summary>
protected List<Association> GetParentEntityTypes(int entityTypeID)
{
  List<Association> ret =
  (from rt in dataSet.Tables["RelationshipType"].AsEnumerable()
    // Qualify by the child (EntityA)
    where rt.Field<int>("EntityATypeID") == entityTypeID
    join et in dataSet.Tables["EntityType"].AsEnumerable()
    on rt.Field<int>("EntityBTypeID") equals et.Field<int>("ID")
    orderby rt.Field<string>("Name")
    select new Association
    {
      // Populate with the parent (EntityB)
      EntityTypeID = rt.Field<int>("EntityBTypeID"),
      EntityTypeName = et.Field<string>("Name"),
    }).ToList();

  return ret;
}

/// <summary>
/// Returns a list of child entity types given an entity type ID.
/// </summary>
protected List<Association> GetChildEntityTypes(int entityTypeID)
{
  List<Association> ret =
  (from rt in dataSet.Tables["RelationshipType"].AsEnumerable()
    // Qualify by the parent (EntityB)
    where rt.Field<int>("EntityBTypeID") == entityTypeID
    join et in dataSet.Tables["EntityType"].AsEnumerable()
    on rt.Field<int>("EntityATypeID") equals et.Field<int>("ID")
    orderby rt.Field<string>("Name")
    select new Association
    {
      // Populate with the child (EntityA)
      EntityTypeID = rt.Field<int>("EntityATypeID"),
      EntityTypeName = et.Field<string>("Name"),
    }).ToList();

  return ret;
}

其次,创建组合框(是的,我知道,糟糕的硬编码值,记住,概念验证!!!)

// The part of the UI for associating an entity instance with another instance:
List<Association> parentEntityTypes = GetParentEntityTypes(entityTypeID);
List<Association> childEntityTypes = GetChildEntityTypes(entityTypeID);
p.AddReference("parentEntityTypes", parentEntityTypes);
p.AddReference("childEntityTypes", childEntityTypes);
RenderParentEntityTypePickList(nsswf, controls, 10, y+25);
RenderChildEntityTypePickList(nsswf, controls, 400, y+25);
protected void RenderParentEntityTypePickList(XNamespace nsswf, XElement controls, int x, int y)
{
  RopCombobox(nsswf, controls, "Child Of:", "parentEntityTypes", 
              "EntityTypeID", "EntityTypeName", x, y);
} 

protected void RenderChildEntityTypePickList(XNamespace nsswf, XElement controls, int x, int y)
{
  RopCombobox(nsswf, controls, "Parent Of:", "childEntityTypes", 
              "EntityTypeID", "EntityTypeName", x, y);
}

以及在XML中渲染组合框的代码

protected void RopCombobox(XNamespace nsswf, XElement controls, string label, 
          string listName, string valueField, string displayField, int x, int y)
{
  RopLabel(nsswf, controls, label, x, y);

  controls.Add(new XElement(nsswf + "ComboBox",
    new XAttribute("Location", (x + 60) + ", " + y),
    new XAttribute("Width", "150"),
    new XAttribute("ValueMember", valueField),
    new XAttribute("DisplayMember", displayField),
    new XAttribute("DataSource", "{" + listName + "}")));
}

生成了一些可用的东西,例如渲染User Story实体类型,它允许的父项是Project和另一个User Story,并且它可以是Task或其他User Story的父项。

显示父/子记录

在下一期待办事项列表中,将根据某些全局标准(例如项目)过滤父项和子项记录。此外,这也是上述规则适用的地方,但目前我还没有实现它们!

给定三种方法(其中两种用于我上面未显示的按钮的事件),一种用于填充网格,我们现在可以显示选定实体类型的父项和子项记录。

protected void OnShowParentData(object sender, EventArgs eventArgs)
{
  int entityTypeID = (int)cbParentEntities.SelectedValue;
  ShowEntityInstances(entityTypeID);
}

protected void OnShowChildData(object sender, EventArgs eventArgs)
{
  int entityTypeID = (int)cbChildEntities.SelectedValue;
  ShowEntityInstances(entityTypeID);
}

protected void ShowEntityInstances(int entityTypeID)
{
  List<AttributeInfo> attributes = GetEntityTypeAttributes(entityTypeID);
  BindingSource bs;
  DataView dv;
  DataTable dt = CreateUnderlyingDataTable(attributes, out bs, out dv);
  grid2.DataSource = bs;
  Fixup(grid2, dt);
  LoadTable(dt, entityTypeID);
  grid2.AllowUserToAddRows = false;
  grid2.AllowUserToDeleteRows = false;
  grid2.ReadOnly = true;
}

结果是

一个描述网格显示内容的标签会很好。目前,我必须解释下面的网格显示了用户故事的可能父项目。

建立关联!

现在我们即将实现一个重要的里程碑——在顶部网格中选定的记录和“关联到”网格中选定的记录之间创建关系。请注意,我使用了复数——没有理由我们不能同时进行多个关联。代码很简单——这是执行子到父关联所完成的工作

foreach (DataGridViewRow child in gridInfo.Grid.SelectedRows)
{
  foreach (DataGridViewRow parent in grid2.SelectedRows)
  {
    int childID = (int)((DataRowView)child.DataBoundItem)["EntityID"];
    int parentID = (int)((DataRowView)parent.DataBoundItem)["EntityID"];
    int relationshipTypeID = GetRelationshipTypeID(childID, parentID);
    DataRow row = dtRelationshipInstance.NewRow();
    row["EntityAID"] = childID;
    row["EntityBID"] = parentID;
    row["RelationshipTypeID"] = relationshipTypeID;
    dtRelationshipInstance.Rows.Add(row);
  }
}

我们还需要关系类型ID,它使用了这个精美的查询

/// <summary>
/// Returns the relationship type ID given two entity type instance ID's.
/// </summary>
protected int GetRelationshipTypeID(int entityAID, int entityBID)
{
  var id = from row in dataSet.Tables["RelationshipType"].AsEnumerable()
    join entityA in dataSet.Tables["EntityInstance"].AsEnumerable()
      on row.Field<int>("EntityATypeID") equals entityA.Field<int>("EntityTypeID")
      where entityA.Field<int>("ID") == entityAID
    join entityB in dataSet.Tables["EntityInstance"].AsEnumerable()
      on row.Field<int>("EntityBTypeID") equals entityB.Field<int>("EntityTypeID")
      where entityB.Field<int>("ID") == entityBID
    select row.Field<int>("ID");

  return (int)id.First();
}

结果——例如,将“显示父/子数据的规则”关联到GAPA项目——是(在超级用户模式下)

我想将实际的规则故事关联到我刚刚创建的更高级别的规则故事,以及将无关联的任务暂时关联到项目,因为这些任务没有用户故事,也没有我到目前为止记录的Bug以及如何解决它们。结果,如果我检查与“规则显示…”实体相关的实体,看起来像这样(使用ROPT可视化关系实例)。

而现在整个Gloriosky关系图看起来像这样(水平压缩)。

疯狂!能够以这种方式可视化我们的动态关系!

用户反馈

有趣的是,点击这个按钮真的没有任何反馈。我添加了一个消息框来提供确认。理想情况下,一旦上述规则实现,列表将不再显示已经存在关联的父/子记录,这将为用户提供更有意义的反馈。

一对一关系

1:1关系是一种特殊情况,通常情况下,底层模式在物理上分开了两个概念(如TaskCreateInfo),但逻辑上,信息应该以联接状态显示。这留给下一期!

物理实体与逻辑实体

ROP的一个有趣的产物是,实体属性甚至没有物理分隔。例如,如果有一个CreateInfo实体,其属性如CreatedOn和CreatedBy,这些属性在每个实体中都属于同一组属性。在典型的模式中,我们可能会将这些字段包含在每个实体中,就像Task、Project、UserStory实体一样。搜索“特定日期范围内的所有任务和用户故事”将不是一件容易的事。创建一个单独的CreateInfo表是可能的,但这需要每个实体表都有一个外键,这使得跨实体类型搜索日期范围变得困难。此外,在传统数据库中不可能说CreateInfo表有一个指向Task、Project和User Story实体之一的外键字段。这意味着外键必须在父表中,这有点本末倒置(在1:1关系中是可以接受的,但在1:n关系中则不行)。ROP模型克服了这些限制。

在ROP模型中,我们可以创建一个逻辑实体“CreateInfo”,但这甚至不是必需的。我们可能会出于清晰的目的这样做,但从技术上讲,CreatedOn和CreatedBy属性可以包含在所有必需的实体中,我们仍然可以跨实体搜索,因为这些值被持久化到一个单一的“属性值池”中,可以轻松地映射回实体实例和类型。

那么,为什么创建一个单独的实体呢?最好的理由是可维护性。如果我们有一个由CreatedOn和CreatedBy属性组成的CreateInfo实体,后来我们想添加另一个属性,比如“ApprovedBy”(技术上来说,审查和批准应该是单独的实体,但请遵循这个例子),那么所有与CreateInfo有关联的实体都会自动获得这个新属性的使用权。如果我们已将CreatedOn和CreatedBy属性添加到每个Task、User Story和Project实体中,那么我们就必须回去向每个实体添加ApprovedBy,这将需要更多的工作。最终,这将导致属性数量庞大,类似于我在传统模式中看到的灾难,这些模式在多年来为了适应新需求而被调整,以至于以前表示“A”的字段现在被用来表示“B”!

无关联实体

ROP的一个独特功能是,实体不一定需要建立关系。例如,我们可以有一个Task,它没有与Project或User Story相关联。我们可以专门查询未关联的实体,无论其类型如何。这放宽了用户必须遵守预定义关系规则的限制,这些规则要求实体必须相关联,允许用户稍后修复这些无关联实体。

关系状态

我还没有实现管理关系状态的规则。主要规则之一是,关系类型是否允许该类型有多个活动关系(婚姻就是一个例子,尽管即使是“只与一个人结婚”的规则也因文化和宗教信仰而异。)此外,如何将关系指定为已完成——例如,一项工作何时开始,何时结束。这些都是需要在下一期(原谅我,是*迭代*!)中探讨的问题。

删除关系

关系可以通过超级用户UI删除。从用户的角度来看,正如我在上一篇文章中提到的,关系通常不会被删除,而是被“过期”、“终止”、“完成”等。在GAPMA中,关系信息通常不是时间状态敏感的,不像人与住所的关系那样。尽管如此,用户应该能够删除关系,因为毕竟,每个人都会犯错。而且,由于ROP模型非常灵活,这意味着错误的发生率更高。

过滤记录

显然,我们通常希望按我们感兴趣的项目过滤用户故事、任务等。但要注意一个有趣的怪癖——“Resolution”实体仅与Task相关联,而不是与Project相关联。这限制了我们的过滤能力——我们只能查看“所有分辨率”(跨所有项目)或“与任务相关联的分辨率”。理想情况下,我们希望能够自动连接到父记录并利用它们的过滤能力——例如,通过与用户故事关联的任务来过滤分辨率。这留给下一个迭代!

导航现有关系

导航关系涉及隐式过滤。当从子项导航到父项时,我们隐式地用子项的外键值限定父项的主键值。当从父项导航到子项时,我们隐式地用父项的主键值限定子项的外键。您会注意到,我们提供了与超级用户相同的功能,但这次是以“用户友好”模式。

一个最基本的方法看起来像这样(代码够多了,这里只是一个UI示例,展示了如何从项目导航到用户故事,并钻入特定用户故事以显示子故事。)

由于生成了一个新的非模态表单,因此没有特定“返回”按钮的意义,因为用户只需关闭表单或选择之前的表单。还要注意,对于没有目标实体实例的关系导航,没有考虑(即用户反馈)。这是下一期的一个功能!

同样,也没有明确的反馈表明用户正在查看数据的过滤视图。这将很有用!最后,用户可能会认为,如果他在/她在此过滤视图中添加记录,它会自动与父实体建立关系。事实并非如此,这可能会导致混淆,因此在某个时候也需要解决这个问题。

使用ROPT和GAPMA:演练

这是一个添加新实体类型、创建关系和在GAPMA中输入数据的示例。我们将向模型添加一个“Iteration”实体,以便我们也能按迭代对任务和用户故事进行分类。我还想添加一个“Sprint”实体,任务可以与之关联,最终想法是可以通过衡量每个冲刺中任务的完成情况来跟踪敏捷开发的“速度”。

首先,从“文件 -> 打开”菜单中打开“*rop.model*”文件。

接下来,从“模型 -> 打开 DataSet”菜单中打开“gapma - save.data”文件。

这是不包含Iteration和Sprint实体的GAPMA版本。

接下来,通过选择“模型 -> 查看模型数据(超级用户模式)”菜单项,在超级用户模式下查看模型。选择EntityType。

输入2个新实体类型:Iteration和Sprint。

接下来,在Current Entity组合框中选择EntityTypeAttributes实体,并为新创建的Iteration和Sprint实体类型输入“Name”和“Short Description”以及序数性。

接下来,在Current Entity组合框中选择RelationshipType实体,并输入将Iteration和Sprint实体与Project实体关联的关系类型。

此外,将Task与Sprint关联,将User Story与Iteration关联。

关闭模型数据查看器,并从菜单中选择“模型 -> 保存 DataSet”。

现在,您可以从“模型 -> 查看模型数据(普通用户模式)”菜单项打开“用户”模型数据查看器,并选择,例如,网格视图模式下的Iteration。

  1. 输入几个迭代。
  2. 单击“保存”按钮将数据导入ROP数据集模型。
  3. 单击父“Project”实体类型的“显示数据”按钮。
  4. 选择您创建的迭代,选择一个项目,然后单击“关联”。

您现在已经将迭代与项目关联起来了。

  1. 返回主应用程序,然后选择“模型 -> 查看模型数据(普通用户模式)”菜单项,这次选择网格视图模式下的Project。
  2. 选择GAPMA项目。
  3. 从“To Child”组合框中选择Iteration,然后单击Go。

您将获得与项目关联的迭代。

别忘了在完成后保存数据集!现在,例如,与迭代关联的用户故事中的所有任务都可以被钻取!如果我们支持报告,我们可以生成关于迭代中的任务、冲刺中的任务或迭代中未分配到冲刺的任务等的报告。这为报告工具需要支持的功能提供了很多思考。

可视化我们的实体类型关系

通过从“模型 -> 图形实体类型关系”中绘制实体类型关系,验证您是否正确创建了关系。您应该得到

注意我们不能将任务与Bug关联。这很好,因为首先,我们从来没有Bug,其次,如果我们有,我们从不修复它们!

可视化处于关系中的实体实例

而真正有趣的部分是,既然我已经创建了GAPMA中各种实体之间的关系,我就可以可视化它们,这是查看我的敏捷项目是否按正确方式组合起来的绝佳方式。上面给出了一个例子,我查看了与我的“规则”用户故事相关的实体实例。

其他应用 - Bug跟踪器呢?

另一种非常适合此方法的应用是Bug跟踪,与敏捷开发的概念分开。我一直抱怨Bug跟踪器的一点是无法添加我想要的字段(最多,Bug跟踪器只能在显示Bug时减少字段数量),也无法建立与特定于我的应用程序域的其他实体类型的关系。使用ROPT,这些问题完全消除了。作为超级用户,我可以创建新的实体和关系,作为用户,我可以填充数据并创建我想要的关系,而不是Bug跟踪器设计者认为我想要的关系。一个明显的情况是将一个复杂的Bug跟踪器与GAPMA联系起来。

互联数据集

这当然会引发一个问题,我是否可以将Bug跟踪器的数据模型连接到,比如说,我的GAPMA数据模型?我不知道,但我知道这个想法值得探索!

结论

还有很多工作要做,但在很大程度上,繁重的工作已经完成……其余的都是打磨和抛光。这将在本系列的下一篇文章中讨论,使其成为一个更完善的应用程序开发工具。如果您运行程序,您会注意到“用户友好”的UI非常糟糕。还有很多工作要做,但我将把这些推迟到进行大规模的代码清理,并使表单行为更加自主。我还不知道这意味着什么,但我以后会弄清楚的。显然,需要支持更多的数据类型,而且我真的想摆脱丑陋的.NET控件,所以在下一篇文章中,您可能会看到转向DevExpress控件。

识别了许多任务

如果您阅读了文本,您会注意到我将许多事情留给了未来的迭代。更多文章!

有人在吗?

有没有人有兴趣为这个项目做出贡献?最终,我想把它变成一个用于开发此类应用程序的RAD产品,包括一个n层后端和多用户支持。如果是这样,我可以为您提供SVN存储库的访问权限,我们可以创建自己的冲刺!

参考文献

© . All rights reserved.