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

Entity Factory - 摆脱 ORM 的束缚!

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2020 年 12 月 9 日

CPL

31分钟阅读

viewsIcon

24029

downloadIcon

458

一个直接从您选择的数据库生成模型和视图模型类的工具

引言

Entity Factory 是一款旨在为您的应用程序生成模型类,(可选)视图模型类的工具。在我开始编写它时,并没有“宏伟的设计”,应用程序中的大部分内容都是在我想到的时候添加的。我相信你们中的许多人也曾像我一样,废寝忘食地编写代码,直到某个时候,你开始想:“也许我应该在这个程序中添加一些东西。”

Entity Factory 几乎完全是以这种方式开发的。在当今现代的敏捷/Scrum 范式中,你们中的大多数人可能会惊恐万分,想象着各种混乱,并对缺乏任务评分或 con/bon 图表示近乎愤慨,更不用说抛弃任何可能类似于“冲刺”的东西了。好吧,欢迎来到 70 年代、80 年代和 90 年代的开发风格,在这种风格中,有人有一堆想法,并将这些想法即兴创作到应用程序中,而不受销售纳粹或管理层强加的任何奇怪议程的干扰。我想我是这个垂死之辈的最后一个人了。没有像我这样的开发者,开发世界将会大打折扣。你会看到的。真的。

这个应用程序最初是我为同事们准备的一个工具,当我们开始重写我们大量的 Web 应用程序时。这里每个阅读这篇文章的程序员都渴望有工具来减少模型/视图模型实体创建的单调性,特别是如果他们对使用 ORM 有狂热——尽管可以理解——的厌恶。我个人从未遇到过比更简单、更快速、更灵活的自建 ADO 代码更优越 ORM 的切实理由。为了让这个应用程序更适合在工作之外公开使用,我不得不将整个应用程序重写了几次,这实际上也同时使其更适合在工作中使用。

这篇文章包含大量的截图。为了使文章保持合理的视觉大小,并且因为即使是 CodeProject 的最佳实践建议的最大尺寸也太小而无法真正读取上面的文本,我将它们作为可点击的缩略图提供,这些缩略图将在新标签页中显示全尺寸图像。创新者。这是我的中间名。

注意 - 如果我发布了应用程序的更新,除非有重大更改,否则我可能不会发布新的屏幕截图。懒惰。这也很像我的中间名。

更新已发布 (2021.11.04)

 

对源代码进行了一些重要的更改(下载链接会下载相应的项目版本)。

倦怠?冷漠?牛仔?

为了完全坦诚,我的心并不真的投入到写这篇文章中。我花了一年多的时间来开发这段代码,大约八个月前经历了一次灾难性的代码丢失,最后才设法重新激发了足够大的兴趣来尝试恢复。和我的大多数文章一样,实际写作的过程促使了几项最后的改动,而在写作、修改代码和生成新屏幕截图之间不断往返变得十分繁琐。

老实说,我希望在这个过程中我没有把代码搞得一团糟。如果什么都没有,这个过程加强了我这样一个信念:开发者应该参与最终用户文档的制作,项目进度表应该足够灵活,以便尽可能地吸收后期出现的更改。

最后,这不是一篇教学文章。它不探讨最新的编程理论,不突破任何编码界限,也不展示任何迫使我走出我那舒适区的编码。那些东西我其实从未感兴趣过。这只是我一系列真实世界问题解决文章中的又一篇,所以不要期望任何顿悟、认识、奇妙的启示,甚至最轻微的“啊哈”时刻。我通常没有时间去实验,所以大部分编码都是因为必要而完成的。

下载

下载包括调试版和发布版的编译二进制文件,以及源代码。虽然我在一个私有的 GitHub 存储库中维护这个项目(这是从 2020 年 5 月的大规模源代码丢失中学到的教训),但我从解决方案中移除了 GitHub 源代码控制文件。在工作中,我们使用 TFS,所以 Git 文件很可能会冲突。

为了我的工作

本文或相关的项目文件(集)不包含任何与工作相关的专有代码或数据。

应用程序

此应用程序的目的是根据数据库的内容生成实体模型,以及可选的实体视图模型(名义上用于 WPF 应用程序)的源文件。如果您熟悉内置的 ADO.Net 项目模板,这个应用程序在某种程度上是相似的,除了 a) 它确实有效,以及 b) 我假设您将以某种方式增强生成的源代码,这意味着所有生成的实体都定义为 `partial` 类,并且关键属性被生成为 `virtual`。我还允许对要生成哪些代码以及如何生成代码进行一定程度的配置。

为了确保用户以适当的顺序执行某些先决步骤,应用程序的主窗口以向导的形式呈现(参见上面的“引言”部分中的相关链接)。

免责声明:我不能——更重要的是,我不会——对您的 SQL 格式化很差导致数据库数据损坏负责。我的建议是,在使用此应用程序之前备份您的数据库,或**至少**在尝试生成代码之前检查存储过程。

特点

应用程序中实现了许多便捷的功能。(这不是详尽的列表。)

  • 生成的代码使用别名类型,如 `int` 和 `string`。EntityFactory 允许您另外选择使用 System 类型,如 `System.Int32` 或 `System.String`,并且还可以生成可空类型。
     
  • 切换 `#region` 的使用(通常用于字段和属性块)。
     
  • 切换注释的生成(包括 intellisense 注释)。
     
  • 为生成的模型和视图模型指定您自己的命名空间。根据我的经验,模型和视图模型通常都包含在源代码层次结构中具有适当名称的给定文件夹中。
     
  • 为生成的模型和视图模型指定类名前缀。模型的默认前缀是“Entity”,视图模型的默认前缀是“VM”。因此,生成的模型类名将是“EntityClassName”,相关的视图模型(如果生成)的类名将是“VMEntityClassName”。与这些类相关联的文件名将反映生成的类名。
     
  • 切换代表 get、insert、update 和 delete 操作的查询文本的表的 CRUD 属性的生成。
     
  • 切换在模型类中创建 `SqlParameter[]` 属性。此属性旨在与可选的 CRUD 属性一起使用,但也可以在代码的其他地方使用。
     
  • 为视图模型类指定字段名前缀。
     
  • 允许编辑 `INotifyPropertyChanged` 和 `IDataErrorInfo` 的接口实现。
     
  • 支持生成数据注释。目前,唯一可以自动生成的 `StringLength` 数据注释是用于字符串属性的。但是,由于属性可以被选择性地生成为 `virtual`,因此应该很容易在继承类中用适当的注释来覆盖它们。
     

基本向导布局

大部分界面以向导的形式呈现。原因是我想确保在用户开始生成代码之前满足某些标准。**设置**页面包含一个选项卡控件,我甚至确保用户意识到这一点,因为我觉得它在视觉上并不突出。

介绍页面

Intro Page

此页面提供有关应用程序用途和用户期望的简要介绍。除此之外,该页面最有趣的部分是“组织信息”面板。此面板中显示的信息包含在 app.config 文件中,并手动插入到该文件中。这是因为它旨在显示特定于您组织的的代码生成信息。屏幕截图中显示的信息是为我的组织提供的。如果您不想显示此面板,只需在 app.config 文件中的 **CompanyNotice** 字段中不提供任何文本。

设置页面

此页面允许用户设置连接到数据库服务器以及生成类和文件的标准。此页面包含几个选项卡。

您可以随时选择将单个选项卡(或所有选项卡)重置为应用程序的默认值。

Settings Page, Database

数据库服务器选项卡

此设置选项卡允许用户指定数据库服务器登录标准。与 SSMS 一样,用户指定服务器名称(或 IP),是否使用 Windows 登录身份验证,如果使用 SQL Server 身份验证,则指定登录指定服务器实例所需的帐户名和密码。**凭据以未加密的方式保存在设置文件中。**

指定了必要信息后,用户可以通过单击 **测试连接** 按钮来测试数据库连接。

此处指定的连接标准将在应用程序的其余执行过程中使用。

Settings Page, General

常规设置选项卡

此设置选项卡允许用户指定通常在表/视图和存储过程代码生成过程之间通用的设置。

**包含代码生成注释** - 导致代码生成器在生成的代码中包含注释。默认值为勾选。

**在字段和属性周围使用区域** - 导致代码生成器用 #region/#endregion 标记环绕字段和属性块。默认值为 true。

**使用可空类型** - 导致代码生成器为字段和属性使用可空类型。默认值为未勾选。

**使用系统类型** - 导致代码生成器使用系统类型 (`System.String`) 而不是别名类型 (`string`)。默认值为未勾选。

Settings Page, Model

模型设置选项卡

此选项卡允许用户指定模型特定的设置。

**命名空间** - 这是您生成的模型实体将驻留的命名空间。默认值为“Models”。

**类名前缀** - 这是每个模型实体的名称前缀。默认值为“Entity”。

**继承的类** - 这是所有生成的实体模型类都继承的类的逗号分隔列表。默认值为空。

**继承的命名空间** - 这是指定继承类的逗号分隔列表。将为指定的每个命名空间生成 `using` 语句。默认值为空。

**添加 CRUD 属性(用于表)** - 这会导致代码生成器为生成的实体创建 CRUD 属性。生成的 CRUD 属性包括 get、insert、update 和 delete。默认值为未勾选。(CRUD 属性仅为表生成。)

**分离插入和更新 CRUD 属性** - 默认情况下,创建 CRUD 属性将生成一个“upsert”语句,该语句将更新和插入查询合并到一个查询中。选中此框将导致更新和插入查询被分离到它们各自独立的属性中。默认值为未勾选。

**将属性设为 virtual** - 为了使生成的模型代码尽可能灵活,此复选框允许用户将所有已发现的架构属性设为 `virtual`。这将允许您继承生成的类,名义上是用数据注释属性覆盖已发现的属性。默认值为未勾选。

**尽可能添加属性** - 此复选框允许生成的代码包含字符串的数据注释,以便可以将属性值限制为特定长度。如果属性是 `virtual` 的,您甚至可以覆盖数据注释以指定最小长度,尽管这在视图模型端会更有用。默认值为勾选。

**添加 SqlParameter[] 属性** - 这是一个返回 `SqlParameter` 数组的属性,该数组包含所有已发现的属性。这可以省去您自己编写代码的麻烦,并且在使用下面的 CRUD 查询属性时特别有用。默认值为勾选。

**SqlParameter[] 属性名称** - 这是 `SqlParameter[]` 属性所需的名称。默认值为“AsSqlParameters”。

Settings Page, Viewmodel

视图模型设置选项卡

此选项卡允许用户指定视图模型特定的设置。可能需要说明的是,(使用 INotifyPropertyChanged 和 IDataErrorInfo 的)视图模型通常只在 WPF 应用程序中找到,但是如果没有这两个接口,“视图模型”在 Web 应用程序中仍然是一个可行的结构,特别是如果您关注关注点分离,或者想在从数据库获取的内容与在 Web 应用程序中使用/修改它的方式之间划定界限。

**命名空间** - 这是您生成的模型实体将驻留的命名空间。默认值为“Viewmodels”。

**类名前缀** - 这是每个模型实体的名称前缀。默认值为“VM”。此前缀添加到生成的模型类名之前,因此“EntityClassName”将变为“VMEntityClassname”。

**继承的类** - 这是所有实体模型类都继承的类的逗号分隔列表。默认值为空。

**继承的命名空间** - 这是指定继承类的逗号分隔列表。将为指定的每个命名空间生成 `using` 语句。默认值为空。

**字段名前缀** - 这是将用于生成字段名的前缀。通常,您不能指望数据集中返回的所有列名的大小写一致,因此唯一可行的解决方案是允许用户为 MVVM 场景中的字段名指定一个前缀。默认值为“vm_”。

**将属性设为 virtual** - 为了使生成的视图模型代码尽可能灵活,此复选框允许用户将所有已发现的架构属性设为 `virtual`。这将允许您继承生成的类,名义上是用数据注释属性覆盖已发现的属性。默认值为未勾选。

**尽可能添加属性** - 此复选框允许生成的代码包含字符串的数据注释,以便可以将属性值限制为特定长度。如果属性是 `virtual` 的,您甚至可以覆盖数据注释以指定最小长度,尽管这在视图模型端会更有用。默认值为勾选。

Settings Page, Snippets

代码片段选项卡

此选项卡允许您指定代码生成过程中使用的特定代码片段。

**标准模型 using** - 此代码片段包含包含在生成的模型文件中的标准 using 语句。

**标准视图模型 using** - 此代码片段包含包含在生成的视图模型文件中的标准 using 语句。

**`IDataErrorInfo` 实现** - 此代码片段代表 `IDataErrorInfo` 接口的实现。

**`INotifyPropertyChanged` 实现** - 此代码片段代表 `INotifyPropertyChanged` 接口的实现。

选择数据库页面

Settings Page, Snippets
Settings Page, Snippets

此页面允许/要求用户选择将从中生成代码的数据库。用户首先需要做的是单击 **发现数据库** 按钮。这将探测指定的服务器,并将组合框填充。

选择数据库后,**下一步** 按钮将被启用。当您单击 **下一步** 按钮时,用户就可以实际开始生成实体类了。

发现数据库组件

有三种类型的数据库组件可以通过 ADO 生成 DataTable 结果集——表、视图和存储过程。表和视图本质上是相同的,但存储过程可以,而且通常会利用参数来精炼返回的数据集,因此您将有机会在一次生成一个存储过程的代码时指定存储过程参数。在某种程度上,其余的向导页面和窗口显示类似的信息,存储过程有几个额外的控件,以允许合理的处理与其参数有关的问题。下面的屏幕截图和支持性叙述将以存储过程为特色,但为了完整起见,还将显示表/视图版本的向导/窗口。

请记住,这里显示的页面和窗口可能不准确地反映代码的最新版本,但差异应该相当小。


Settings Page, Snippets
Settings Page, Snippets

发现表/视图与发现存储过程之间的唯一真正区别在于显示已发现组件的列表视图的内容。对于表/视图,我认为显示组件类型(表或视图)很重要,而对于存储过程,显示存储过程是否返回数据集以及是否接受参数很重要。无论组件类型如何,都必须显示代码生成状态。

对于两个向导页面,用户都可以使用页面顶部/右角的复选框来切换帮助面板的显示/隐藏。切换状态将一直保持,直到用户重置应用程序设置到默认值,或勾选复选框。


Settings Page, Snippets
Settings Page, Snippets

单击 **发现** 按钮后,列表视图将被填充。一旦填充,列表视图项就会提供一个上下文菜单,允许查看该项目的某些方面。对于所有组件,用户都可以查看生成的代码(一旦在当前会话中生成了代码)。对于存储过程,用户还可以检查该项以查看 **Ret Data** 和 **Has Params** 列的填充方式。

本质上,存储过程的主体被“规范化”,去除了空格、空行和注释,然后应用程序会查找某些选择或以其他方式操作数据的 SQL 命令。如果存在 `DROP`、`DELETE`、`INSERT`、`UPDATE` 或 `MERGE` 命令的任何实例,应用程序将显示该存储过程不返回数据。原因是你可能不希望运行修改数据库的存储过程,其中参数包含假数据。这仅仅是一种保护措施。

为选定组件生成代码

Settings Page, Snippets
Settings Page, Snippets

当用户选择为单个组件生成代码时,将显示此窗口。它允许用户为关联组件同时生成模型和视图模型(如果需要)。模型和视图模型的大部分设置都存在于表单的两侧。

**创建模型类** - 勾选此项时,单击 **生成** 按钮后将生成模型类。默认值为勾选。

**在此处保存文件** - 用户希望保存生成的模型文件的路径。您可以使用关联的浏览按钮来避免输入路径名。

**覆盖现有模型文件** - 如果勾选,应用程序将覆盖已生成的现有文件。默认值为勾选。

**用属性装饰** - 用指定的属性装饰**所有**已发现的属性。此字段要求您指定语法正确的代码。

**生成文件** 按钮 - 根据上述标准生成并保存选定的实体代码。


Settings Page, Snippets

当用户单击 **设置存储过程参数...** 按钮时,将显示此窗口。您可能已经注意到它与您在 SSMS 的组件树中执行存储过程时看到的窗口相似。它的工作方式也相同。

您可以单独设置每个参数,然后单击 **执行存储过程** 按钮。当存储过程执行完毕后,它返回的数据集的架构将显示在窗口右侧的 **返回数据集架构** 列表视图中。这**不会**导致代码生成,它仅用于信息显示。但是,您在此处指定的参数将在上一个窗口生成代码时使用。


Settings Page, Snippets
Settings Page, Snippets

代码生成时,文件将在指定路径中创建,并且生成的代码显示在 **生成文件** 按钮下方的区域。所示代码可以被高亮显示并复制到剪贴板。两个源列表之间的蓝色条是**分隔符,它允许您调整所需的列表宽度。


Settings Page, Snippets
Settings Page, Snippets

关闭表单后,您将返回到向导页面,列表视图将更新以反映已生成代码的状态。在为给定项生成代码后,您可以右键单击所需的项并单击上下文菜单中的 **检查生成的代码** 来检查生成的代码。此上下文菜单项仅在代码成功生成时才启用。

为所有组件生成代码

Settings Page, Snippets
Settings Page, Snippets

当用户选择为所有已发现的组件生成代码时,将显示此窗口。由于处理的组件很多,因此无法显示与为单个选定组件生成代码时相同的界面。为了使过程尽可能地不干扰和通用,创建的类以模型和视图模型的可查看文件名形式呈现。只需单击所需文件,然后单击关联的 **查看文件** 按钮。

对于存储过程,用户将无法指定参数值,因此如果结果集取决于特定参数值,结果可能有点不尽如人意。当然,这完全取决于存储过程的编写方式。请注意,只有被确定为返回数据的存储过程才包含在要处理的组件列表中。


Settings Page, Snippets
Settings Page, Snippets

代码生成后,最左边的列表框将更新以显示每个组件的生成状态。绿色背景表示成功,红色背景表示失败。


Settings Page, Snippets
Settings Page, Snippets

当用户关闭 **全部生成** 窗口时,向导页面将更新以反映已处理的每个组件的生成状态。

检查项和代码

Settings Page, Snippets

存储过程源代码在代码规范化**之后**进行评估。规范化过程会删除所有前导空格、注释和空行。这意味着此窗口中显示的源代码将保留其格式,因此请准备好以这种方式查看它(不用担心,应用程序不会以任何方式修改数据库中存在的源代码)。为了方便起见,显示的可以直接复制到剪贴板。

请记住,应用程序评估存储过程代码的能力几乎完全取决于存储过程的编写方式。换句话说,在存储过程中使用合理的格式化实践几乎总会带来更好的评估体验。

Settings Page, Snippets

用户还可以检查生成的代码。当所有发现的组件一次性处理时,这尤其有用。只需右键单击所需的组件,然后单击 **检查生成的代码**。将显示此窗口。这仅仅是一种检查生成内容的方法,除了能够将代码复制到剪贴板之外,此窗口没有什么特别之处。

为查询生成代码(新功能,2021.01.01)

Specify query
Specify query

发布文章后,我想到用户可能有时希望为纯文本查询生成实体。为了实现此功能,一个新向导页面被插入为第一个生成页面。除了在其他生成页面上相同的一般特征外,值得注意的特征是

  • **类名** - 指定实体类名。类名将用作文件名(如果用户选择将查询保存到文件),因此指定的类名必须仅包含符合有效文件名规则的字符,以及 C# 命名规则(幸运的是,这两个要求不冲突)。为了方便用户,名称会在他们输入时进行验证。
     
  • **SQL 查询文本** - 此字段是您实际指定查询的地方。输入时没有内置的所见即所得验证,也没有内置的验证所有表、视图或列确实存在于所选数据库中。完全开发和调试需要数月时间,我根本不在乎花那么多时间。M

    我的建议是在 SSMS 中编写查询,因为它拥有您想要的所有语法高亮显示,更不用说确定表/视图名称及其列名称有效性的内置功能了。一旦您在 SSMS 中完成了查询,将其保存为文件,然后将该文件加载到 Entity Factory 中。
     
  • **加载按钮** - 允许用户从 .SQL 文件加载 SQL 查询。
     
  • **保存按钮** - 允许用户将当前指定的查询保存到 .SQL 文件。此按钮在实体名称和 SQL 查询都包含有效文本之前是禁用的。
     
  • **验证 SQL 按钮** - 对指定的 SQL 查询执行语法和数据库验证。在实体名称和 SQL 查询都包含有效文本之前,此按钮是禁用的。
     
  • **生成按钮** - 显示代码生成窗口。在指定的数据经过验证(使用 Vlaidate SQL 按钮)之前,此按钮是禁用的。
     
Settings Page, Snippets

此窗口与用于表/视图的窗口相同。我将不费心介绍细微差别,因为它们对窗口的功能没有任何影响。

生成的代码示例

以下是应用程序生成的模型和视图模型代码的示例。在以下示例中,注释、区域和 CRUD 属性均已打开。此外,数据注释和虚拟化属性也已打开。

模型

//==================================================================================================
// Source object: AEF2.dbo.Alerts (table)
// File generated: 2020.12.09, 07:32:10

// This file was generated by EntityFactory. Do not modify this file. When regenerated, the file    
// may be overwritten if it already exists and EntityFactory was configured to overwrite existing   
// files). Note that the generated class is "partial", so if you need to augment the generated    
// code, create a new partial extension file and put your custom code in that new file.             

// The class name indicates the model (or viewmodel) class name prefix followed by the name of the  
// original database object from which the class was generated.                                     
//==================================================================================================

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// Auto-incrementing column detected - add the following using to support standard C# attributes.
// If you haven't already, add a reference to System.ComponbentModel.DataAnnotations to the 
// applicable assembly in your application.
using System.ComponentModel.DataAnnotations;

namespace Models
{
	public partial class EntityAlerts
	{
		#region entity properties

		[Key]
		public virtual int      ID               { get; set; }
		[StringLength(32)]
		public virtual string   AlertName        { get; set; }
		[StringLength(128)]
		public virtual string   AlertTitle       { get; set; }
		[StringLength(2147483647)]
		public virtual string   AlertMsg         { get; set; }
		public virtual DateTime AlertStartDate   { get; set; }
		public virtual DateTime AlertExpireDate  { get; set; }
		public virtual int      AlertType        { get; set; }
		public virtual int      AlertClassLevel  { get; set; }
		[StringLength(512)]
		public virtual string   AlertApps        { get; set; }
		[StringLength(64)]
		public virtual string   ActionName       { get; set; }
		[StringLength(64)]
		public virtual string   ControllerName   { get; set; }
		public virtual DateTime DateAdded        { get; set; }
		public virtual int      AddedByUMSUserID { get; set; }

		#endregion entity properties

		public SqlParameter[] SqlParameters
		{
			get
			{
				return new SqlParameter[]
				{
					new SqlParameter("@ID"              , ID              ),
					new SqlParameter("@AlertName"       , AlertName       ),
					new SqlParameter("@AlertTitle"      , AlertTitle      ),
					new SqlParameter("@AlertMsg"        , AlertMsg        ),
					new SqlParameter("@AlertStartDate"  , AlertStartDate  ),
					new SqlParameter("@AlertExpireDate" , AlertExpireDate ),
					new SqlParameter("@AlertType"       , AlertType       ),
					new SqlParameter("@AlertClassLevel" , AlertClassLevel ),
					new SqlParameter("@AlertApps"       , AlertApps       ),
					new SqlParameter("@ActionName"      , ActionName      ),
					new SqlParameter("@ControllerName"  , ControllerName  ),
					new SqlParameter("@DateAdded"       , DateAdded       ),
					new SqlParameter("@AddedByUMSUserID", AddedByUMSUserID),
				};
			}
		}

		#region database properties

		public virtual string CRUDGet
		{
			get
			{
				return "SELECT [ID], [AlertName], [AlertTitle], [AlertMsg], [AlertStartDate],"
						+" [AlertExpireDate], [AlertType], [AlertClassLevel], [AlertApps], [ActionName],"
						+" [ControllerName], [DateAdded], [AddedByUMSUserID] FROM [dbo].[Alerts]"
						+" WITH(NOLOCK);";
			}
		}

		public virtual string CRUDUpsert
		{
			get
			{
				return "UPDATE [dbo].[Alerts] SET [AlertName] = @AlertName , [AlertTitle] ="
						+" @AlertTitle , [AlertMsg] = @AlertMsg , [AlertStartDate] = @AlertStartDate ,"
						+" [AlertExpireDate] = @AlertExpireDate , [AlertType] = @AlertType ,"
						+" [AlertClassLevel] = @AlertClassLevel , [AlertApps] = @AlertApps , [ActionName]"
						+" = @ActionName , [ControllerName] = @ControllerName , [DateAdded] = @DateAdded"
						+" , [AddedByUMSUserID] = @AddedByUMSUserID WHERE [ID] = @ID;"
						+" IF @@ROWCOUNT = 0"
						+" INSERT INTO [dbo].[Alerts] ( [AlertName] , [AlertTitle] , [AlertMsg] ,"
						+" [AlertStartDate] , [AlertExpireDate] , [AlertType] , [AlertClassLevel] ,"
						+" [AlertApps] , [ActionName] , [ControllerName] , [DateAdded] ,"
						+" [AddedByUMSUserID]) VALUES ( @AlertName , @AlertTitle , @AlertMsg ,"
						+" @AlertStartDate , @AlertExpireDate , @AlertType , @AlertClassLevel ,"
						+" @AlertApps , @ActionName , @ControllerName , @DateAdded , @AddedByUMSUserID);";
			}
		}

		public virtual string CRUDDelete
		{
			get
			{
				return "DELETE FROM [dbo].[Alerts] WHERE [ID] = @ID;";
			}
		}


		#endregion database properties

	} // end class

} // end namespace

视图模型

//==================================================================================================
// Source object: AEF2.dbo.Alerts (table)
// File generated: 2020.12.09, 07:39:31

// This file was generated by EntityFactory. Do not modify this file. When regenerated, the file    
// may be overwritten if it already exists and EntityFactory was configured to overwrite existing   
// files). Note that the generated class is "partial", so if you need to augment the generated    
// code, create a new partial extension file and put your custom code in that new file.             

// The class name indicates the model (or viewmodel) class name prefix followed by the name of the  
// original database object from which the class was generated.                                     
//==================================================================================================

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// The following using was added to support the INotifyPropertyChanged and/or IDataErrorInfo interfaces.
using System.ComponentModel;

// The following using was specified in the Model Namespace setting.
using Models;

// The following usings were specified in the Inerited Namespaces setting.
using WpfCommon;

//----------- Generated from database object dbo.Alerts --------------//

namespace ViewModels
{

	public partial class VMEntityAlerts : INotifyPropertyChanged, IDataErrorInfo
	{
		#region INotifyPropertyChanged implementation

		/// <summary>
		/// Occurs when a property value changes.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;
		
		/// <summary>
		/// Notifies that the property changed, and sets IsModified to true.
		/// </summary>
		/// <param name="propertyName">Name of the property.</param>
		protected void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
		{
			if (this.PropertyChanged != null)
			{
				this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
				if (propertyName != "IsModified")
				{
					this.IsModified = true;
				}
			}
		}
		

		#endregion INotifyPropertyChanged implementation

		#region fields

		private int      vm_ID;
		private string   vm_AlertName;
		private string   vm_AlertTitle;
		private string   vm_AlertMsg;
		private DateTime vm_AlertStartDate;
		private DateTime vm_AlertExpireDate;
		private int      vm_AlertType;
		private int      vm_AlertClassLevel;
		private string   vm_AlertApps;
		private string   vm_ActionName;
		private string   vm_ControllerName;
		private DateTime vm_DateAdded;
		private int      vm_AddedByUMSUserID;

		#endregion fields

		#region properties

		/// <summary>
		/// Model (generated from database source object)
		/// </summary>
		protected EntityAlerts Model { get; set; }

		/// <summary>
		/// Get/set ID
		/// </summary>
		public virtual int ID
		{
			get { return this.vm_ID; }
			set
			{
				if (value != this.vm_ID)
				{
					this.vm_ID = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertName
		/// </summary>
		[StringLength(32)]
		public virtual string AlertName
		{
			get { return this.vm_AlertName; }
			set
			{
				if (value != this.vm_AlertName)
				{
					this.vm_AlertName = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertTitle
		/// </summary>
		[StringLength(128)]
		public virtual string AlertTitle
		{
			get { return this.vm_AlertTitle; }
			set
			{
				if (value != this.vm_AlertTitle)
				{
					this.vm_AlertTitle = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertMsg
		/// </summary>
		[StringLength(2147483647)]
		public virtual string AlertMsg
		{
			get { return this.vm_AlertMsg; }
			set
			{
				if (value != this.vm_AlertMsg)
				{
					this.vm_AlertMsg = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertStartDate
		/// </summary>
		public virtual DateTime AlertStartDate
		{
			get { return this.vm_AlertStartDate; }
			set
			{
				if (value != this.vm_AlertStartDate)
				{
					this.vm_AlertStartDate = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertExpireDate
		/// </summary>
		public virtual DateTime AlertExpireDate
		{
			get { return this.vm_AlertExpireDate; }
			set
			{
				if (value != this.vm_AlertExpireDate)
				{
					this.vm_AlertExpireDate = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertType
		/// </summary>
		public virtual int AlertType
		{
			get { return this.vm_AlertType; }
			set
			{
				if (value != this.vm_AlertType)
				{
					this.vm_AlertType = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertClassLevel
		/// </summary>
		public virtual int AlertClassLevel
		{
			get { return this.vm_AlertClassLevel; }
			set
			{
				if (value != this.vm_AlertClassLevel)
				{
					this.vm_AlertClassLevel = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AlertApps
		/// </summary>
		[StringLength(512)]
		public virtual string AlertApps
		{
			get { return this.vm_AlertApps; }
			set
			{
				if (value != this.vm_AlertApps)
				{
					this.vm_AlertApps = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set ActionName
		/// </summary>
		[StringLength(64)]
		public virtual string ActionName
		{
			get { return this.vm_ActionName; }
			set
			{
				if (value != this.vm_ActionName)
				{
					this.vm_ActionName = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set ControllerName
		/// </summary>
		[StringLength(64)]
		public virtual string ControllerName
		{
			get { return this.vm_ControllerName; }
			set
			{
				if (value != this.vm_ControllerName)
				{
					this.vm_ControllerName = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set DateAdded
		/// </summary>
		public virtual DateTime DateAdded
		{
			get { return this.vm_DateAdded; }
			set
			{
				if (value != this.vm_DateAdded)
				{
					this.vm_DateAdded = value;
					this.NotifyPropertyChanged();
				}
			}
		}

		/// <summary>
		/// Get/set AddedByUMSUserID
		/// </summary>
		public virtual int AddedByUMSUserID
		{
			get { return this.vm_AddedByUMSUserID; }
			set
			{
				if (value != this.vm_AddedByUMSUserID)
				{
					this.vm_AddedByUMSUserID = value;
					this.NotifyPropertyChanged();
				}
			}
		}


		#endregion properties

		#region constructors

		/// <summary>
		/// Default constructor
		/// </summary>
		public VMEntityAlerts()
		{
		}

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <summary>
		/// Default constructor
		/// </summary>
		public VMEntityAlerts(EntityAlerts model)
		{
			this.Model = model;
		}

		#endregion constructors

	} // end class

} // end namespace

代码,就这样吧

最初,我打算将其作为一篇分为两部分的文章,但说实话,谁想阅读关于普通桌面应用程序中可以找到的代码的细节?毕竟,它就在那里,橙色和白色,以链接的形式呈现。但是,我将继续谈论技术栈,以便您决定是否要自己查看源代码。

就 .Net、WPF 和 ADO 的实现而言,这不一定是一个复杂的应用程序,但我确实希望您对这些有一定程度的经验。请注意,本文**不会**努力教授基本概念。它仅描述所使用的技术,并要求您在需要进一步帮助理解这些技术时自学。(如果我试图为初学者详细解释一切,这可能最终会成为一篇 20 部分的文章系列,而我根本不感兴趣。)

我认为代码已经相当注释了,但可能存在覆盖遗漏,因为文件很多,我可能遗漏了一些(或很多)。

就代码质量而言,我承认组织可能可以更好,或者实现方式不同,并且我没有特别努力地遵循“最佳实践”。再说一遍,大部分代码是我对这个程序应该包含什么的即时反应,而且我的技术在编写代码的过程中有了很大的发展。再加上源代码丢失的灾难,实现可能从一天到另一天发生巨大变化。

技术栈

  • **Visual Studio 2017** - 当我开始这个项目的代码时,VS2017 是当时可用的最新发行版。VS2019 很快作为发布候选版本推出,但我们都知道不要将代码库提交给微软的新软件。目前,VS2019 是最新最好的(实际上已经正式发布),如果您想将此应用程序转换为 .Net Core,您应该使用 VS2019 并直接迁移到 .Net 5.0。
     
  • **.Net Framework 4.72** - 我不是 .Net Core 的粉丝,而且在我开始编写代码时,WPF 支持很糟糕。据我所知,WPF 支持在 .Net 5 下已经稳定,但我在这方面没有经验。还没有。
     
  • **ADO.Net** - 我使用 ADO 是因为我喜欢它,而且我理解它。我有很多代码围绕它编写,我不想放弃它来换取僵化的通用便利代码。对 ORM 说不
     
  • **Windows Presentation Foundation (WPF)** - 当 WPF 于 2009 年发布时,我曾抵制在其自己的代码中采用它,但现在,在 2020 年,结合 MVVM 的采用,如果您需要一个有吸引力且健壮的 UI,我无法想象其他桌面开发解决方案。
     
  • **Model View Viewmodel (MVVM) 模式** - 虽然在应用程序中实现起来更繁琐,但它肯定能强制关注点分离。这是一个好主意。
     
  • **单例模式** - 单例是实例化对象,它们在整个应用程序中静态可用。它们被称为单例,因为它们在应用程序会话的生命周期中只实例化一次。为了方便访问,我在需要访问这些单例对象的任何对象中定义了属性。这允许我如下引用它们
    if (this.AppSettings.SomeProperty == someValue){}
  • **反射** - 反射用于检查和操作应用程序中定义的对象的属性。通过反射,您可以确定属性名称、类型、可访问性,甚至可以在运行时将功能注入对象(此应用程序不进行任何注入)。这项技术在代码中得到了广泛的应用。要看到它的实际效果,请在解决方案中查找 `PropertyInfo` 的实例。
     
  • ADO.Net 通用 DAL - 重访  [ ^ ] - 本文讨论了我的通用 DAL 对象以及如何使用它。
     
  • WPF 向导的另一种方法  [ ^ ] - 本文介绍了用于创建本文中向导的代码。
     
  • 可自定义的 WPF MessageBox  [ ^ ] - 本文介绍了一个可自定义的标准 WPF `MessageBox` 版本。
     

代码组织

代码被组织成包含模型对象、视图模型对象、窗口和向导页面的文件夹。根项目文件夹中实际存在的文件很少,所以一切都相当整洁。

WPF

我坚信,仅仅因为你能做某事,并不意味着你必须,甚至不应该。XAML 中有很多事情可以做,但在我看来,在取证模式下,它只会让事情更难弄清楚,因为你无法在 XAML 代码块中停止调试器。出于这个原因,我喜欢在 C# 代码中做很多事情。

所有窗口都派生自基类 `NotifiableWindow`(位于 WPFCommon 程序集中)。这是一个继承了 `INotifyPropertyChanged` 接口的 Window 类,这使得实现窗口可以用来更新 UI 的属性变得容易。我在 WPF 应用程序中经常这样做。当您从 WPF 中的基类继承时,您必须更改 XAML 以适应它。

<wpfctrl:WizardWindowBase x:Class="EntityFactory.MainWindow"
    xmlns...
    xmlns:wpfctrl="clr-namespace:WpfCommon.Controls;assembly=WpfCommon"

您必须添加一个 xml 命名空间 (xmlns) 条目,声明程序集(在您的应用程序中引用),并更改主标签以反映自定义基类(由您分配的 xml 命名空间限定)。

其他 WPF 技术包括自定义模板、静态和动态资源的用法、转换器,以及将一个元素中的绑定属性与另一个(命名)元素中的属性进行绑定。例如,我经常将按钮的 `Width` 绑定到将最宽的按钮(基于按钮 `Content` 中的文本)的 `ActualWidth`。这种技术可以在 `WizPgTablesViews` 类中看到。

生成实体的过程

这是应用程序存在的理由——从表、视图、存储过程或用户查询返回的数据集中生成代码。在生成任何代码之前,我们必须知道将从数据库中检索什么数据。这样做的机制是 ADO `DAL`(有关此工作原理的更多信息,请参阅上面引用的 DAL 文章),并且通过项目数据库文件夹中的 `BLL` 对象来处理。

第一步是发现数据库中包含的数据库组件。对于表和视图,发现过程就到此结束,但存储过程需要更多工作。首先,我们必须发现为给定存储过程可能指定的任何参数。接下来,我们必须确定存储过程是否实际返回数据集。

本质上,我们查询数据库以创建/返回表示返回数据集的 `DataTable`。对于表和视图,使用简单的 `SELECT TOP(0)* FROM [table/view]` 查询从表或视图中检索数据。存储过程需要更精细的方法。

存储过程可以在数据库中执行许多操作。如果某个存储过程有可能以任何方式修改表(使用 `DROP`、`DELETE`、`INSERT`、`UPDATE`、`MERGE` 或 `TRUNCATE`),它将被标记为不返回数据。这是为了保护数据库中的数据免受错误数据或完全删除数据的侵害。如果发现的存储过程被确定为返回数据,则实际上会执行它以创建必要的 `DataTable`,然后可以对其进行解析。

一旦我们有了 `DataTable`,应用程序就可以使用该 `DataTable` 来确定列名、数据类型以及关于该列的其他数据。它使用这些信息来创建生成代码中的属性定义。

为了完全披露,对表和视图的支持是事后考虑,因为在我工作的地方(以及在我自己的数据库访问方法中),我们只使用存储过程来检索数据,因为我们的选择标准非常细微且针对特定需求。我们没有直接查询表或视图的代码。为 CodeProject 写文章的决定促使我重新考虑了整个应用程序的设计,您今天看到的许多功能都是这一设计方向改变的直接结果。不客气。

**更新 2021.01.02** - 为了更全面地披露,对用户查询的支持是事后考虑的。我想有些人可能只是想为特定结果手工编写查询。再说一遍,不客气。

关注点

再次重申,此应用程序旨在供不使用 ORM 框架的程序员使用。但是,这并不排除它用于增强此类系统。我想不到任何会从中受益的用例,但我只是一个拥有非常狭窄(且有偏见)世界观的人。谁知道呢?这个应用程序甚至可能促使更多的程序员使用 ORM。我猜更奇怪的事情都发生过。

如果您看到我做的某件事有问题,请在评论区告诉我。我会尽快以某种方式解决它。请注意,我一定会注意到拼写错误,并会在文章发布后快速更新。

更新 2021.01.02

我曾考虑将 AvalonEdit 添加到应用程序中以提供语法高亮显示,但这增加了太多的工作量,我断定得不偿失。如果您想尝试这项任务,请随意。注意 - 如果您不打算将代码转换为 .Net Core,您将仅限于使用 6.0 之前的某个版本的控件。

我花了一周时间试图让 `IDataErrorInfo` 在新的用户查询向导页面上生效,但我的向导页面代码中存在一些隐藏(且非常奇怪)的交互,导致它无法可靠工作,而且我不想去追踪它,所以我绕过了问题并取得了一些合理的效果。如果您有兴趣,代码在 WizPgQuery.* 中。

历史

  • **2021.11.04** - 修复了以下问题
    • 设置向导页面上的“保存”按钮在保存设置更改后保持红色。
    • 发现存储过程和发现表/视图向导页面 - 当您选择一个或多个要生成代码的项目,然后单击 **生成选定项...** 按钮时,将显示适当的窗体(生成一个或生成所有),具体取决于选定的项目数量。这允许您批量生成,而无需处理所有已发现的项目。如果您只想为所有已发现的项目快速生成一些代码,**生成全部**按钮仍然在那里。
    • 设置向导页面 | 常规选项卡 - 为针对 .Net Core 3.0/更新项目的实体添加了使字符串可空的支持。
    • 视图模型代码生成 - 修复了一个问题,即所有属性/字段都会被设为可空(如果指定)。正确行为是不使 .net Core 2.n 及更早版本(包括 .Net Framework)的字符串可空。
    • 视图模型代码生成 - 修复了一个问题,即可空的 DateTime 字段在类型声明和字段名称之间没有空格。
    • 视图模型代码生成 - 修复了一个问题,即模型的“usings”代码片段被用于视图模型。
    • 杂项控件大小/对齐问题。
     
  • **2021.01.02** - 更新以添加用户查询生成以及文章中的大量拼写更正。
     
  • **2020.12.09** - 首次发布
     
© . All rights reserved.