Tripous介绍






4.45/5 (10投票s)
一个开源的桌面数据库(数据录入)应用程序框架。
引言
Tripous 是一个开源应用程序框架,用 C# 编写,用于 **快速** 实现 WinForms 数据录入应用程序。
官方网站可在 http://tripous-net.com 找到。SourceForge 上的项目页面可在 http://sourceforge.net/projects/tripous 找到。
要使用 Tripous,您至少需要 .NET 3.5 Service Pack 1 和 Visual Studio 2008。
本文是一篇关于如何编码基本 Tripous 应用程序的入门教程。您可以将此示例应用程序作为编写自己的 Tripous 应用程序(具有自己的数据和表单)的基础。
Tripous 提供什么
- 快速应用程序开发。
- 统一的数据访问;也就是说,数据库服务器中立的 SQL 语句,以及最重要的,数据库服务器中立的 SQL 参数。
- 一个强大且灵活的业务类,其尊称为 Broker,完全声明式、可继承或两者兼有。
- 基本的数据录入表单类及其自己的用户界面控件,可用于加速用户界面的构建。
- 用户访问控制,易于(确实)扩展以覆盖任何类型的对象。
- 自动日志记录和异常处理。
- XML 序列化(Tripous 大量使用 XML)。
- 自动 SQL 语句生成。
- 统一的应用程序资源访问。
- 可插入式应用程序选项子系统。
- 插件。
- 可设计报表(使用您选择的外部报表库)。
- 一个 TCP 服务器和客户端,它们使用简单的 XML 命令进行通信(简易版的 SOAP),完全可插入,且可扩展。
- SQLite 集成。
- PDA 框架,具有桌面框架的所有优点(包括控件)。
- ...等等。
Tripous 教程
我已经在 CodeProject 上写了一些关于如何使用 Tripous 子系统的教程,并计划写更多。您可以在我在 CodeProject 上的 个人资料页面 上找到那些教程。
这是什么名字?
Tripous 是一个古希腊词,意思是三足支架。Tripod 是同义词。德尔菲的 德尔菲 皮提亚 在做出神谕时就坐在一个 三脚架 上。与数据库服务器的对话类似于与神谕的对话。您发出一个查询,但您不知道会得到什么答案。由于 Tripous 框架是关于数据的,我认为 Tripous 是一个合适的名字。
为什么要有一个框架?
Tripous 是一个用于快速开发数据录入应用程序的框架。
框架是一个代码库,它提供构建应用程序的服务。.NET 就是这样一个框架。大多数编程环境也属于这一类。那么为什么要费力构建这样一个框架呢?难道 .NET 和所有这些 .NET 类还不够吗?是的,它们是够的。
如果需要框架
- 如果您认为您必须将各种数据库连接技术隐藏在统一的数据访问层后面。
- 如果您打算提供基础的业务逻辑父类。
- 如果您认为您的数据录入表单应具有一定程度的统一性。
背景
Tripous 是一个传统的系统,在构建应用程序的许多方面都有自己的视角。让我们检查最重要的领域。
P/Invoke
无 P/Invoke。Tripous 的虚荣心是有一天能在 Mono 世界 中漂泊。以及在 DotGNU 大陆中。因此,它只在没有其他解决方案时使用 P/Invoke。例如,它使用 P/Invoke 来调用远程 API(RAPI)。P/Invoke 会将框架与特定系统绑定。而这对于框架来说不是一个好习惯。友谊已经足够了。
Tripous 不是 ORM
Tripous 的主要任务是处理存储在关系数据库中的数据。所以我们可以说 Tripous 主要是一个数据库应用程序框架。
有两种数据库应用程序框架:我们称之为传统框架,以及对象关系映射框架(ORM)或对象持久化框架(OPF)。
在 ORM/OPF 框架中,数据库表被 映射 到类。该框架的用户处理的是对象和对象列表。而不是数据集或数据库表。一个类建立在一个或多个数据库表之上,而该类的属性或多或少地映射到数据库字段。尽管这是一个简化的描述。无论如何,这种映射技术有一些局限性,特别是当底层数据库模式不会永远保持不变时(在我看来,模式更改非常频繁)。如果模式发生更改,则需要重新映射对象并重新编译应用程序。
除了 ORM,一些编程环境还提供所谓的类型化数据集,即一个类似数据集的对象,其中底层数据库表的字段名已经是数据表的属性。.NET 和 PowerBuilder 是这类环境的例子。
ORM 和类型化数据集的整个想法是,程序员了解类,并且只处理类和对象。仅此而已。这种观念还认为,任何接触数据库表和 SQL 语句(如 SELECT
、INSERT
等)的行为都可能损害程序员的态度,并导致他们某种程度上的失望。
所以 ORM 框架就像魔术师一样,营造了数据位于应用程序本地而不是数据库中的错觉。正如 Anders Hejlsberg 所说:“您可能希望数据不在数据库中的这种错觉。您可以拥有这种错觉,但这需要付出代价。”
Tripous **不是**一个 ORM 框架。(事实是,曾几何时,当 Tripous 还年轻的时候,它有另一种语言,即 Delphi Pascal,并且它是一个 ORM 的潜在追求者。但那已经是历史了。)相反,Tripous 是一个“传统”框架。尽管它有一个业务对象 `Broker` 类,但它提供了对数据库表和字段的完全访问。因此,Tripous 为您提供了对 `DataSet`、`DataTable` 和 `DataRow` 对象的直接访问。它已尽力不打扰您,但迟早您必须处理那些 DataSomething 的东西。
无 N 层
有时会混淆什么是层或什么是什么。让我们再做一次徒劳的尝试来澄清这个问题。
一个 设计良好 的数据库应用程序应该提供三个独立的 层。数据访问、业务逻辑以及表示或用户界面。将它们想象成组织中的部门。它们必须具有明确定义的目标,并且没有重叠。实际上,这很困难。这些层就像为了同一件玩具(数据)而争吵的孩子。
简而言之
- **数据访问层** 隐藏了各种数据库类型的复杂性,并通过执行来自业务逻辑层的
SELECT
、INSERT
、UPDATE
等语句,在访问数据库数据方面提供了统一性。 - **业务逻辑层** 解决业务问题,并执行业务操作,同时考虑所谓的业务规则(“任何新客户在头三个月内,除非他是老板的表弟,否则不能享受超过 10% 的折扣”),并将数据提供给表示层。
- **表示层** 或 **用户界面层** 控制用户在与应用程序交互时使用的所有视觉元素,如窗口、菜单和按钮,将数据呈现给用户,并将任何输入返回给业务逻辑层。
设计良好的应用程序会将业务逻辑与表示解耦。尽管如此,这种分层大多是**逻辑**上的分层。也就是说,在大多数情况下,这三个层存在于同一个物理可执行文件中。
关于**物理分层**,即不同的可执行模块,存在两个相互冲突的派别:
- 传统的**客户端-服务器**派别
- 以及现代的**多层**或**N 层**派别
在客户端-服务器设计中,有两个物理层:应用程序是其中之一。数据库和数据库服务器(MS SQL、Oracle 等)是另一个。
在多层设计中,有时称为 N 层,在应用程序和数据库之间还有一个层:**应用程序服务器**(Application Server),或简称为 AppServer。该应用程序服务器使用各种技术(如 DCOM、CORBA 等)为客户端应用程序提供业务对象(客户、供应商、订单等)。在多层设计中,客户端应用程序与应用程序服务器通信,并远离数据库服务器。
Tripous 不是一把 瑞士军刀。它是一个谦虚的系统,知道它的局限性。它面向拥有小型内网或 VPN 的中小型企业。在这些条件下,传统的客户端-服务器系统就足够了。
表分类
关于业务逻辑,数据库应用程序可以逻辑上划分为业务模块。每个模块,例如客户、商店或销售模块,都使用一组相关表。例如,一个假设的销售模块可能使用 CUSTOMER、MATERIAL、TRADE 和 TRADE_LINES 表。
Tripous 以特定的方式看待数据表。表可能属于以下类别之一,具体取决于数据的性质和可能存在的行数:主表、查找表、事务表和关联表。
- **主表**(Master)是指其他表具有外键指向它的表。主表通常有三个以上的字段,并且有数千行。CUSTOMER 或 MATERIAL 表被认为是主表。主表可能包含指向其他主表或查找表的 असतात。
- **查找表**(Lookup)是指有几个字段,最多只有一百行数据的表。查找表是维护指向它的外键的其他表的**字面值提供者**。它通常有 ID、CODE 和 NAME 字段。OCCUPATION、MEASURE_UNIT 或 COUNTRY 表被认为是查找表。一个完美的查找表不包含指向其他表的 असतात。
- **事务表**(Transaction)是指记录主表事务的表。有时它们被称为历史表,甚至交易表。事务表很容易有数百万行。ORDERS 或 TRADE 表被认为是事务表。事务数据通常需要两个甚至更多表形成主从关系,构成一个表树。例如,ORDERS 和 ORDER_LINES;其中 ORDERS(主表)包含关于客户、交易日期等信息,而 ORDER_LINES(从表)记录关于商品、数量和价格的信息,并有一个指向 ORDERS 的 ID。
- **关联表**(Correlation)是指关联两个或多个主表的表。通常,关联表只记录那些主表的 ID,而没有其他信息。关联表通常记录多对多关系。例如,CAR 和 DRIVER 主表可能需要 CAR_DRIVER 关联表。一个司机可能负责多辆车。
关于表分类和表树的构成,Tripous 依赖您,程序员。Tripous 提供了 `Broker` 类,它代表一个业务模块。`Broker` 是 Tripous 中的**业务对象**。在内部,`Broker` 使用 `TableSet` 类来处理表树。
`TableSet` 类处理上述业务模块的表集合。每个表由一个 `DataTable` 实例表示。`TableSet.TopTable` 属性指向顶层 `DataTable`。`TopTable` 是 `DataTable` 对象树中的顶层 `DataTable`,其中低层级的 `DataTable` 是下一层级 `DataTable` 的主表,处于主从关系中。
规范化
规范化是数据库编程中的一个术语,用于描述设计表以最小化信息重复的技术。
例如,在 CUSTOMER 表中,有一个 OCCUPATION_NAME 字符串字段是不明智的。相反,您可以使用一个 OCCUPATION_ID 外键字段指向 OCCUPATION 表的 ID 主键字段。对于 ORDER_LINES 事务表和 MATERIAL 主表也同样适用。ORDER_LINES 应该有一个 MATERIAL_ID,而不是 MATERIAL_CODE 或 MATERIAL_NAME 字段。
简而言之,一点信息只存储一次,且存储在一个特定的表中。任何想要访问该信息点的其他表都只需维护一个指向它的引用。将表和字段安排如上所述的过程称为规范化。
规范化并非万能药。它用于所谓的“生产数据库”,即用于日常事务处理的数据库。规范化不用于“数据仓库数据库”,后者主要用于“数据挖掘”操作。
您可以查看 数据库规范化、数据仓库 和 数据挖掘 在 wikipedia 上的条目。但关于规范化,请不要花太多时间阅读这些文本,除非您是正在寻找下学期参考资料的学生。常识就足够了。至少对于 Tripous 试图涵盖的领域来说。
主键
Tripous 严格遵循一条规则:数据库中的每个表都应该有一个名为 ID 的字段(好吧,如果您坚持,可以随意命名)。该字段
- 是一个整数字段或 GUID 字符串字段,
- 它是主键,即
- 它**唯一**标识一行,最重要的是,
- 它**没有任何业务含义**。
应用程序用户几乎从不看到该字段。
这些唯一 ID 称为 OID(对象标识符)。一些 SQL 服务器提供生成这些整数唯一 ID 的内置机制。有两种形式:自动递增字段和唯一编号生成器。MS SQL Server 和 Informix 属于第一类,而 Interbase/Firebird 和 Oracle 属于第二类。
数据录入表单
数据库应用程序主要由数据录入表单组成。但是数据录入表单应该是什么样的?嗯,对此有几百种不同的看法。您只能用数据做四件事,相信我,不多不少:SELECT、INSERT、UPDATE 和 DELETE。一个非常有限的集合。(承认吧。我们作为数据库应用程序程序员的工作并不难,但最好把它保密。)
Tripous 数据录入表单有两个部分:
- 浏览(或列表)部分,以及
- 编辑(或条目)部分。
列表部分的数据通过发出“select * from TableName
”语句获得(但这不完全准确,因为该 SELECT
会尽可能复杂)。应用程序用户可以为该 SELECT
语句配置一个可选的 WHERE
子句,每次执行该语句时都会生效。列表部分的数据显示在一个只读的浏览器网格中。
编辑部分的数据,在编辑时,通过执行“select * from TableName where Id = x
”语句获得。`Id` 值来自列表部分,由用户选择后得到。插入时,编辑部分只显示空控件。
以上是 Tripous 主表单的简要描述。主表单用于主表和事务表。
第二种 Tripous 数据录入表单是列表表单。列表表单没有单独的编辑部分。所有数据录入都在浏览器网格中进行,并且更改一次性提交。列表表单用于查找表。
为报表或任何其他 BI 目的 SELECT 数据与为数据录入目的 SELECT 数据是完全不同的。所以现在我们可以安全地忽略它。
存储过程
无存储过程。Tripous 对存储过程过敏。存储过程将应用程序绑定到特定的数据库服务器。Tripous 面向中小型企业。其中一些(更富有的)可能已经拥有商业数据库服务器。另一些(更明智的)可能没有,因为他们想降低成本并使用 开源服务器。因此,任何将应用程序绑定到特定数据库服务器的东西都不是一个好的选择。
但有一个传言说存储过程的执行速度比普通 SQL 语句快一倍,这是因为数据库服务器预编译了存储过程代码。我对这个传言不太确定,但我不得不说,一个体面的数据库服务器应该在其内部缓存中保留所有 SQL 语句的执行计划,而不仅仅是存储过程的执行计划。如果它没有,那么我就认为这是一个主要缺陷。我承认我的观点缺乏足够的有效性。
秘诀在于使用**参数化** SQL。Tripous 就是这样做的。类似于“select * from CUSTOMER where ID = :ID
”。从数据库服务器的角度来看,这个语句比普通的“select * from CUSTOMER where ID = 1234
”看起来要好得多。
安装和设置
本节提供有关如何安装和设置 Tripous 的信息和说明。
Tripous 源代码
Tripous 库的源代码是一个压缩文件,其中仅包含一个 Visual Studio 解决方案。
该解决方案包含以下文件夹:
- Compact:与 Compact Framework (PDA) 相关的程序集。
- Desktop:普通的 .NET Framework 相关程序集。
- Plugins:Tripous 已使用的一些插件程序集。
- Printing:一个正在进行中的创建报表库的项目。
Compact 文件夹包含:
- TripousPPC5 项目,这是用于处理 PDA 的 Tripous 库程序集。
- DevAppPPC5 项目,这是使用 Tripous 创建 Compact Framework (PDA) 应用程序的模板项目。
- TripousPPC5.Design 项目,包含 TripousPPC5 程序集的一些设计器。请阅读随附的 *ReadMe.txt* 文件以获得进一步的说明。
- TripousPPC5.DesignAttributesHack 项目,这实际上是我能找到的解决 CF 设计时功能相关的 *genasm.exe* 问题的最佳方法。
Desktop 文件夹包含:
- Tripous 项目,这是用于处理 PC 的 Tripous 库。
- DevAppDesktop 项目,这是使用 Tripous 创建桌面应用程序的模板项目。
Plugins 文件夹包含:
- em_FastReport 项目,这是一个插件程序集(*em_* 代表外部模块),用于动态加载优秀的 FastReport 库。
- em_ScriptNet 项目,这是一个插件程序集,用于优秀的 ScriptDotNet 脚本库。
- em_SyntaxBox 项目,这是一个插件程序集,用于优秀的 Puzzle.SyntaxBox.NET3.5 语法高亮编辑器。
(Tripous 试图遵循一种可插入式架构,其中许多代码元素(例如表单和其他类)而不仅仅是外部模块插件。这意味着您可以提供自己的代码,在任何情况下都可以覆盖 Tripous 的默认解决方案。)
Printing 文件夹包含一个项目,该项目将成为 Tripous 报表库,如果时间允许的话。
安装和编译
只需将源代码解压缩到一个目录。无需在 GAC 中安装任何内容。找到解决方案文件(*TripousLib.sln*)并双击它。VS 会打开并加载解决方案。只需重新生成解决方案,一切都会准备就绪。这里没什么可说的,除了我还是个 Jedi 骑士 时学到的一句古老的祝福:“愿原力与你同在”。
如果您打算使用 Tripous 进行**Compact Framework** 开发,那么您最好仔细阅读 TripousPPC5.Design 和 TripousPPC5.DesignAttributesHack 项目中的两个 *ReadMe.txt* 文件。
编译过程将生成您需要在任何 Tripous 项目中引用的两个程序集:
- RootFolder\Compact\TripousPPC5\bin\Debug\TripousPPC5.dll,用于 Compact Framework 应用程序。
- RootFolder\Lib\Desktop\Tripous\bin\Debug\Tripous.dll,用于普通桌面应用程序。
安装 Tripous 控件
- 关闭所有 MS VS 实例。
- 重新打开 MS VS 并创建一个包含 Windows Forms Application 项目的解决方案。
- 使 `Form1` 可见。
- 转到 Toolbox 并创建一个新选项卡(右键单击然后添加选项卡)。
- 将新选项卡命名为“Tripous Desktop”。
- 右键单击该选项卡,然后选择“选择项”。
- 在“选择工具箱项”对话框中,单击“浏览”并导航到 *RootFolder\Lib\Desktop\Tripous\bin\Debug\Tripous.dll*。
- 选择 `Tripous.dll` 程序集中包含的所有控件,然后单击“确定”。
第一个 Tripous 应用程序
本节提供了关于如何编码您的第一个 Tripous 应用程序的说明。
创建一个包含 Windows Forms Application 项目的新解决方案。将项目命名为 DevAppDesktop。将项目默认的命名空间(解决方案资源管理器 | 右键单击项目 | 属性 | 应用程序 | 默认命名空间)重命名为 Project。添加对 *Tripous.dll* 程序集的引用。
生成项目(Shift + F6)。
MainForm
将 `Form1` 重命名为 `MainForm`。将其 `IsMdiContainer` 属性设置为 true
。您的 `MainForm` 必须继承自 `Tripous.Forms.SysMainForm`。此外,您还需要向 *MainForm.cs* 文件添加一些 using
指令。这是代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using Tripous;
using Tripous.Forms;
using Tripous.Data;
namespace Project
{
public partial class MainForm : SysMainForm
{
public MainForm()
{
InitializeComponent();
}
}
}
`SysMainForm` 类是用于派生主窗体的基类。我们将在后续教程中深入探讨它。
ApplicationManager
向项目中添加一个新的代码文件,并将其命名为 *ApplicationManager.cs*。这是代码:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using System.Data;
using Tripous;
using Tripous.Forms;
using Tripous.Data;
using Tripous.BusinessModel;
using Tripous.Sockets;
namespace Project
{
class ApplicationManager : ApplicationManagerDesktop
{
/* construction */
public ApplicationManager()
{
Db.ErrorsVisible = true;
Variables["User.Enabled"] = true;
Variables["CompanyId"] = 1;
}
}
}
`ApplicationManagerDesktop` 类继承自 `ApplicationManagerBase` 类。任何 Tripous 应用程序中都必须只有一个应用程序管理器。应用程序管理器就像一个想成为硬摇滚乐队领袖的交通警察。权威与无政府主义并存。
`Db` 类是 `Tripous.Data` 命名空间中的核心类。实际上只是一系列辅助静态方法。`Tripous.Variables` 类是一组命名变量,可以持久化到 XML。这里,`ApplicationManager.Variables` 属性是 `Variables` 类的一个实例,它维护了一些有用的设置。
再次提及 MainForm
回到 `MainForm`。向 `MainForm` 添加一个 `System.Windows.Forms.MenuStrip` 并将其命名为 `mnuMain`。向 `MainForm` 添加一个 `System.Windows.Forms.ToolStrip` 并将其命名为 `ToolBar`。
向 `MainForm` 添加一个 `System.Windows.Forms.StatusStrip` 并将其命名为 `StatusBar`。向 `StatusBar` 添加七个 `System.Windows.Forms.ToolStripStatusLabel` 项,并按以下方式命名它们:
lblLight
lblHint
lblAppName
lblUser
lblProfile
lblDate
lblTime
向 `MainForm` 添加一个 `Tripous.Forms.SideArea` 并将其命名为 `LeftSide`。`SideArea` 是一个 Tripous 控件,它的秘密野心是有一天能媲美 MS VS 的工具箱。
这是我 *MainForm.Designer.cs* 文件中这些对象的声明:
private System.Windows.Forms.MenuStrip mnuMain;
private System.Windows.Forms.ToolStrip ToolBar;
private System.Windows.Forms.StatusStrip StatusBar;
private System.Windows.Forms.ToolStripStatusLabel lblHint;
private System.Windows.Forms.ToolStripStatusLabel lblLight;
private System.Windows.Forms.ToolStripStatusLabel lblUser;
private System.Windows.Forms.ToolStripStatusLabel lblAppName;
private System.Windows.Forms.ToolStripStatusLabel lblProfile;
private System.Windows.Forms.ToolStripStatusLabel lblDate;
private System.Windows.Forms.ToolStripStatusLabel lblTime;
private Tripous.Forms.SideArea LeftSide;
这是我 *MainForm.Designer.cs* 文件中完整的 `IntializeComponent()` 方法:
private void InitializeComponent()
{
this.mnuMain = new System.Windows.Forms.MenuStrip();
this.ToolBar = new System.Windows.Forms.ToolStrip();
this.StatusBar = new System.Windows.Forms.StatusStrip();
this.lblLight = new System.Windows.Forms.ToolStripStatusLabel();
this.lblHint = new System.Windows.Forms.ToolStripStatusLabel();
this.lblAppName = new System.Windows.Forms.ToolStripStatusLabel();
this.lblUser = new System.Windows.Forms.ToolStripStatusLabel();
this.lblProfile = new System.Windows.Forms.ToolStripStatusLabel();
this.lblDate = new System.Windows.Forms.ToolStripStatusLabel();
this.lblTime = new System.Windows.Forms.ToolStripStatusLabel();
this.LeftSide = new Tripous.Forms.SideArea();
this.StatusBar.SuspendLayout();
this.SuspendLayout();
//
// mnuMain
//
this.mnuMain.Location = new System.Drawing.Point(0, 0);
this.mnuMain.Name = "mnuMain";
this.mnuMain.Size = new System.Drawing.Size(570, 24);
this.mnuMain.TabIndex = 0;
this.mnuMain.Text = "menuStrip1";
//
// ToolBar
//
this.ToolBar.Location = new System.Drawing.Point(0, 24);
this.ToolBar.Name = "ToolBar";
this.ToolBar.Size = new System.Drawing.Size(570, 25);
this.ToolBar.TabIndex = 1;
this.ToolBar.Text = "toolStrip1";
//
// StatusBar
//
this.StatusBar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.lblLight,
this.lblHint,
this.lblAppName,
this.lblUser,
this.lblProfile,
this.lblDate,
this.lblTime});
this.StatusBar.Location = new System.Drawing.Point(0, 389);
this.StatusBar.Name = "StatusBar";
this.StatusBar.Size = new System.Drawing.Size(570, 22);
this.StatusBar.TabIndex = 0;
this.StatusBar.Text = "statusStrip1";
//
// lblLight
//
this.lblLight.AutoSize = false;
this.lblLight.BackColor = System.Drawing.Color.Green;
this.lblLight.BorderSides = ((System.Windows.Forms.ToolStripStatusLabelBorderSides)(
(((System.Windows.Forms.ToolStripStatusLabelBorderSides.Left |
System.Windows.Forms.ToolStripStatusLabelBorderSides.Top)
| System.Windows.Forms.ToolStripStatusLabelBorderSides.Right)
| System.Windows.Forms.ToolStripStatusLabelBorderSides.Bottom)));
this.lblLight.BorderStyle = System.Windows.Forms.Border3DStyle.Etched;
this.lblLight.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.lblLight.Name = "lblLight";
this.lblLight.Size = new System.Drawing.Size(10, 17);
//
// lblHint
//
this.lblHint.AutoSize = false;
this.lblHint.Name = "lblHint";
this.lblHint.Size = new System.Drawing.Size(340, 17);
//
// lblAppName
//
this.lblAppName.AutoSize = false;
this.lblAppName.Name = "lblAppName";
this.lblAppName.Size = new System.Drawing.Size(90, 17);
//
// lblUser
//
this.lblUser.AutoSize = false;
this.lblUser.Name = "lblUser";
this.lblUser.Size = new System.Drawing.Size(90, 17);
//
// lblProfile
//
this.lblProfile.AutoSize = false;
this.lblProfile.Name = "lblProfile";
this.lblProfile.Size = new System.Drawing.Size(100, 17);
//
// lblDate
//
this.lblDate.AutoSize = false;
this.lblDate.Name = "lblDate";
this.lblDate.Size = new System.Drawing.Size(60, 17);
//
// lblTime
//
this.lblTime.AutoSize = false;
this.lblTime.Name = "lblTime";
this.lblTime.Size = new System.Drawing.Size(40, 17);
//
// LeftSide
//
//
// LeftSide.Body
//
this.LeftSide.Body.Cursor = System.Windows.Forms.Cursors.Default;
this.LeftSide.Body.Dock = System.Windows.Forms.DockStyle.Fill;
this.LeftSide.Body.Location = new System.Drawing.Point(0, 0);
this.LeftSide.Body.Name = "Body";
this.LeftSide.Body.Size = new System.Drawing.Size(9, 502);
this.LeftSide.Body.TabIndex = 3;
this.LeftSide.Dock = System.Windows.Forms.DockStyle.Left;
this.LeftSide.IsExpanded = false;
this.LeftSide.Location = new System.Drawing.Point(0, 49);
this.LeftSide.Name = "LeftSide";
this.LeftSide.IsPinned = false;
this.LeftSide.Size = new System.Drawing.Size(14, 502);
this.LeftSide.TabIndex = 3;
this.LeftSide.Title = "Tools";
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(570, 411);
this.Controls.Add(this.LeftSide);
this.Controls.Add(this.StatusBar);
this.Controls.Add(this.ToolBar);
this.Controls.Add(this.mnuMain);
this.IsMdiContainer = true;
this.Location = new System.Drawing.Point(0, 0);
this.MainMenuStrip = this.mnuMain;
this.Name = "MainForm";
this.Text = "MainForm";
this.StatusBar.ResumeLayout(false);
this.StatusBar.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
我们的 `MainForm` 的用户界面已经准备好了。现在我们可以回到 *MainForm.cs* 代码文件。
将 `Tripous.Forms.ICommandHost` 接口添加到 `MainForm` 类的祖先列表中,并实现其属性。
public partial class MainForm : SysMainForm, ICommandHost
{
#region ICommandHost Members
MenuStrip ICommandHost.MenuStrip
{
get { return this.MainMenuStrip; }
}
ToolStrip ICommandHost.ToolBar
{
get { return this.ToolBar; }
}
SideArea ICommandHost.SideBar
{
get { return this.LeftSide; }
}
#endregion
public MainForm()
{
InitializeComponent();
}
}
`ICommandHost` 接口代表一个可以显示命令和其他信息的对象。通常,此对象是主窗体。`ApplicationManager` 使用 `ICommandHost` 对象作为我们添加到其中的命令的命令显示器。
在 `MainForm` 类中添加以下两个私有成员:
/* private methods */
private void Application_Idle(object sender, EventArgs e)
{
if (!DesignMode)
{
lblDate.Text = DateTime.Today.ToString("yyyy-MM-dd");
lblTime.Text = DateTime.Now.ToString("HH:mm");
lblAppName.Text = Sys.ApplicationTitle;
lblUser.Text = User.UserName;
lblProfile.Text = !string.IsNullOrEmpty(Manager.Profiles.Active) ?
Manager.Profiles.Active : Sys.None;
}
}
/* private properties */
private ApplicationManager Manager {
get { return ApplicationManager.Instance as ApplicationManager; } }
然后调整构造函数如下:
public MainForm()
{
InitializeComponent();
if (!DesignMode)
{
appManagerType = typeof(ApplicationManager);
Application.Idle += new EventHandler(Application_Idle);
}
}
`appManagerType` 是 `SysMainForm` 类中 `System.Type` 类型的 `protected` 字段。`appManagerType` 提供了用于创建实际应用程序管理器对象的类型。该应用程序管理器实例由 `SysMainForm.ApplicationInitialize()` 虚拟方法创建。我们重写该方法和另外两个方法:`ApplicationFinalize()` 和 `HandleEvent()`。这是完整的 *MainForm.cs* 源代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using Tripous;
using Tripous.Forms;
using Tripous.Data;
namespace Project
{
public partial class MainForm : SysMainForm, ICommandHost
{
#region ICommandHost Members
MenuStrip ICommandHost.MenuStrip
{
get { return this.MainMenuStrip; }
}
ToolStrip ICommandHost.ToolBar
{
get { return this.ToolBar; }
}
LeftSide ICommandHost.SideBar
{
get { return this.LeftSide; }
}
#endregion
/* private methods */
private void Application_Idle(object sender, EventArgs e)
{
if (!DesignMode)
{
lblDate.Text = DateTime.Today.ToString("yyyy-MM-dd");
lblTime.Text = DateTime.Now.ToString("HH:mm");
lblAppName.Text = Sys.ApplicationTitle;
lblUser.Text = User.UserName;
lblProfile.Text = !string.IsNullOrEmpty(Manager.Profiles.Active) ?
Manager.Profiles.Active : Sys.None;
}
}
/* private properties */
private ApplicationManager Manager { get {
return ApplicationManager.Instance as ApplicationManager; } }
/* overrides */
protected override void ApplicationInitialize()
{
base.ApplicationInitialize();
Ini Ini = new Ini();
ToolBar.Visible = Ini.ReadBool("ToolBar.Visible", false);
LeftSide.Visible = Ini.ReadBool("SideBar.Visible", false);
LeftSide.LastSize = Ini.ReadInteger("SideBar.LastSize", LeftSide.LastSize);
if (LeftSide.Visible)
{
LeftSide.IsExpanded = Ini.ReadBool("SideBar.IsExpanded", false);
if (LeftSide.IsExpanded)
LeftSide.IsPinned = Ini.ReadBool("SideBar.IsPinned", false);
if (LeftSide.IsExpanded || LeftSide.IsPinned)
LeftSide.Width = Ini.ReadInteger("SideBar.Width", LeftSide.Width);
}
}
protected override void ApplicationFinalize()
{
Ini Ini = new Ini();
Ini.WriteBool("SideBar.Visible", LeftSide.Visible);
Ini.WriteBool("SideBar.IsExpanded", LeftSide.IsExpanded);
Ini.WriteBool("SideBar.IsPinned", LeftSide.IsPinned);
Ini.WriteInteger("SideBar.Width", LeftSide.Width);
Ini.WriteInteger("SideBar.LastSize", LeftSide.LastSize);
Ini.WriteBool("ToolBar.Visible", ToolBar.Visible);
base.ApplicationFinalize();
}
protected override void HandleEvent(ArgList Args)
{
if (!DesignMode && !IsDisposed)
{
base.HandleEvent(Args);
switch (Args.ValueOf("EventName", string.Empty).ToString())
{
case "Application.Executing":
case "Application.Waiting":
if (Args.ValueOf("Value", false))
lblLight.BackColor = Color.Red;
else
lblLight.BackColor = Color.Green;
break;
}
}
}
/* construction */
public MainForm()
{
InitializeComponent();
if (!DesignMode)
{
appManagerType = typeof(ApplicationManager);
Application.Idle += new EventHandler(Application_Idle);
}
}
}
}
在 `ApplicationInitialize()` 和 `ApplicationFinalize()` 方法中使用的 `Tripous.Ini` 类使用 XML 文件模拟了一个经典的 Windows *.ini* 文件。
重写的 `HandleEvent()` 方法会接收由 `Tripous.Broadcaster` 对象发送的通知。`Broadcaster` 类代表一个向其订阅的侦听器发送事件通知的对象。`MainForm` 就是这样一个侦听器,因为它的基类 `SysMainForm` 实现了 `Tripous.IListener` 接口。任何代码都可以使用 `Broadcaster` 实例来发送通知给该 `Broadcaster` 的订阅者。
将 `Broadcaster` 想象成一个邮件列表。有人向列表发送一条消息,列表中的每个成员都会收到该消息的通知。收到此类消息的接收者可以在收到消息后采取行动,或者完全忽略它。
Program 类
现在让我们关注 *Program.cs* 文件。代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Threading;
using Tripous;
using Tripous.Forms;
namespace Project
{
static class Program
{
private const string AppUniqueId = "{Put a Guid string here}";
static private InstanceManager im;
[STAThread]
static void Main(string[] args)
{
Sys.SetApplicationCulture(args);
using (im = new InstanceManager(AppUniqueId))
{
if (!im.IsSingleInstance)
{
Sys.ErrorBox("This application is already running!");
Application.Exit();
return;
}
Res.Add(Project.Properties.Resources.ResourceManager);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
static Program()
{
Application.ThreadException +=
new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}
static void CurrentDomain_UnhandledException(object sender,
UnhandledExceptionEventArgs e)
{
if ((e.ExceptionObject is Exception) && !e.IsTerminating)
{
try
{
Logger.Log(e.ExceptionObject as Exception);
}
catch
{
}
}
}
static void Application_ThreadException(object sender,
ThreadExceptionEventArgs e)
{
try
{
Logger.Log(e.Exception);
}
catch
{
}
}
}
}
让我们从 `Main()` 开始,谈谈 `InstanceManager`。当您的应用程序只允许运行一个实例时,请使用 `InstanceManager`。`InstanceManager` 确保只有一个应用程序实例正在运行。为了实现这一点,它使用一个唯一的字符串来唯一标识应用程序。这就是 `AppUniqueId` 私有字段的原因。为其分配一个 GUID 以确保万无一失(菜单工具 | 创建 GUID)。
`Tripous.Res` 类是一个静态类。通过调用其 `Add()` 方法,它可以访问任何注册给它的 `ResourceManager` 的资源。在这里,本地的 `Project.Properties.Resources.ResourceManager` 被添加到 `Res` 类的资源管理器中。
`Program` 类的静态构造函数只是链接了一些事件,这些事件使我们能够集中处理任何未处理的异常。这两个事件的代码都使用静态的 `Tripous.Logger` 类来记录有关抛出异常的信息。`Logger` 类用于记录任何类型的信息,不仅仅是异常。`Logger` 类还以一种类似 `Broadcaster` 的方式,通知任何订阅的侦听器任何日志事件。`Logger` 本身不持久化日志信息。这是特殊日志侦听器的任务。Tripous 已经提供了一个将日志信息保存到数据库表的日志侦听器。但是,还有其他日志侦听器,它们只是将日志信息显示给用户。
配置文件
在 Tripous 中,配置文件是一个普通的 XML 文件,最初位于应用程序可执行文件所在的文件夹中。它始终命名为 *Profiles.XML*。将 此 文件复制到应用程序的 *...\\bin\\Debug* 文件夹,并将其重命名为 *Profiles.XML*。Tripous 应用程序会将此文件移动到 `Environment.SpecialFolder.CommonApplicationData` 指定的文件夹。在 Windows Vista 中,这是 *C:\\ProgramData\\* 文件夹。
配置文件是配置文件项的列表。其中一个配置文件项是活动配置文件。活动配置文件由根 `Profiles` 标签的 `Active` 属性表示。Tripous 应用程序在启动时,如果未定义活动配置文件,会要求用户选择活动配置文件。此外,如果用户启动应用程序并按住 Ctrl 键,Tripous 将显示相同的“选择活动配置文件”对话框供用户选择或创建新配置文件。
这些配置文件项中的每一个都包含一个 **Datastore
** 项列表。数据存储项包含数据库的连接设置。一个配置文件可能包含多个数据存储连接,因为一个应用程序可能使用多个数据库。名为 `MAIN` 的数据存储项被认为是应用程序的主数据库。主数据库是 Tripous 系统表所在的地方。Tripous 使用几个数据库表来满足其自身需求。这些表的命名遵循 *SYS_TABLENAME* 的模式。
这是 *Profiles.XML* 文件中名为 *Sqlite* 的配置文件内容:
<item Provider="Sqlite" ConnectionString="Alias=Sqlite;
Initial Catalog=[Data]\DevApp.db3;" Name="MAIN" />
`Provider` 属性指定了提供程序。Tripous 在 .NET 2 之前就使用“提供程序”来提供数据库服务器中立的访问。之后是 `ConnectionString` 属性。它几乎与 ADO.NET 中使用的常规连接字符串相同。`Alias` 子属性复制了 `Provider` 属性。`[Data]` 字面量是一个占位符。在运行时,Tripous 会将占位符替换为实际路径。SQLite 和 Firebird 需要文件名。当然,并非所有服务器都这样做。`Name` 属性指定了数据存储对象(一个 Tripous 类)的 `Name` 属性的值。它**不是**数据库的名称。
这是同一文件中名为 *MsSql* 的配置文件内容:
<item Provider="MsSql" ConnectionString="Alias=MsSql;
Data Source=localhost; Integrated Security=SSPI; Initial Catalog=DevApp" Name="MAIN" />
不用费心创建这些数据库。Tripous 知道如何为 *Profiles.XML* 文件中指定的这三个服务器中的任何一个创建空的数据库文件。
ApplicationManager 初始化和最终化
将您的 `ApplicationManager` 类代码调整如下:
class ApplicationManager : ApplicationManagerDesktop
{
private CommandSetsToolForm commandSetsToolForm;
/* other overrides */
protected override void RealizeCommandSets()
{
base.RealizeCommandSets();
if (commandSetsToolForm != null)
commandSetsToolForm.RefreshToolForm(commandSets);
}
/* initialization-finalization */
protected override void DoInitialize()
{
base.DoInitialize();
if (SideBar != null)
{
commandSetsToolForm = new CommandSetsToolForm();
SideBar.AddForm(commandSetsToolForm);
commandSetsToolForm.RefreshToolForm(commandSets);
}
}
protected override void DoFinalize()
{
base.DoFinalize();
}
/* construction */
public ApplicationManager()
{
Db.ErrorsVisible = true;
Variables["User.Enabled"] = true;
Variables["CompanyId"] = 1;
}
}
您可能还记得,实际的 `ApplicationManager` 实例是由 `SysMainForm` 的初始化代码创建的,具体来说是由 `SysMainForm.ApplicationInitialize()` 方法创建的。该方法在创建管理器后,会调用应用程序管理器的 `ApplicationInitialize()` 方法。`ApplicationManager.DoInitialize()` 是作为该调用序列的结果而调用的。
`DoInitialize()` 为命令集创建了一个工具窗体。命令集是一组命令。`CommandSetsToolForm` 实例被创建,作为 `MainForm` 的 `LeftSide` 控件的子窗体。应用程序管理器指示 `CommandSetsToolForm` 显示其命令。我们稍后会讨论命令和 `Command` 类。
`DoFinalize()` 这里什么都不做。目前只是为了对称。
`RealizeCommandSets()` 在初始化期间以及每次命令集发生更改时被调用。在 Tripous 中,最终用户可以使用 `Command` 对象作为这些集合的成员来创建命令集。用户为每个命令集指定一个唯一的名称。`CommandSetsToolForm` 工具窗体是向用户显示命令集的一种方式。另一种方式是 `MainForm` 的 `ToolBar`。
数据库架构注册和创建
Tripous 使您能够即时创建版本化的数据库架构。`SchemaDatastores`、`SchemaDatastore` 和 `SchemaExecutor` 是相关的类,都位于 `Tripous.Data` 命名空间中。
您将一个 `SchemaDatastore` 实例注册到 `SchemaDatastores`,用于任何主要的架构更改。
一个 `SchemaDatastore` 具有 `Name` 和 `Version` 属性。最初,您在 `Name` 为 MAIN 和 `Version` = 1 的情况下创建一个 `SchemaDatastore`,假设这是您应用程序版本 1 的架构。在您的应用程序成功销售一年后,出现了修改初始架构的需求。然后您在同一个 `Name` MAIN 下创建另一个 `SchemaDatastore`,这次 `Version` 为 2。仅此而已。
Tripous 对其自身的系统表也做同样的事情。检查 `ApplicationManagerBase.RegisterSystemSchema()` 方法和 `SystemSchema` 类。
`ApplicationManager.RegisterSchemas()` 是注册数据存储架构的地方。这是我们示例应用程序的该方法的代码。将以下方法添加到 `ApplicationManager` 类中:
/* registration */
protected override void RegisterSchemas()
{
base.RegisterSchemas();
SchemaDatastore schema = schemaDatastores.FindForce(Sys.MAIN, 1);
SchemaTable Table;
/* Company */
string cCOMPANY =
@"
create table Company (
Id @PRIMARY_KEY
,Name varchar(56) not null
);
";
Table = schema.AddTable("Company", cCOMPANY);
/* Trader */
string cTRADER =
@"
create table Trader (
Id @PRIMARY_KEY
,Code varchar(32) not null
,Name varchar(48) not null
,IsCustomer integer default 1 not null
,IsSupplier integer default 0 not null
,@COMPANY_ID integer default -1 not null
,constraint FK_Trader_00 foreign key (@COMPANY_ID) references Company (Id)
);
";
Table = schema.AddTable("Trader", cTRADER);
/* TradeItem */
string cTRADE_ITEM =
@"
create table TradeItem (
Id @PRIMARY_KEY
,Code varchar(32) not null
,Name varchar(48) not null
,Price float default 0 not null
,@COMPANY_ID integer default -1 not null
,constraint FK_TradeItem_00 foreign key (@COMPANY_ID) references Company (Id)
);
";
Table = schema.AddTable("TradeItem", cTRADE_ITEM);
/* Trade */
string cTRADE =
@"
create table Trade (
Id @PRIMARY_KEY
,Code varchar(32) not null
,TraderId integer not null
,TradeDate @DATE not null
,@COMPANY_ID integer default -1 not null
,constraint FK_Trade_00 foreign key (@COMPANY_ID) references Company (Id)
,constraint FK_Trade_01 foreign key (TraderId) references Trader (Id)
);
";
Table = schema.AddTable("Trade", cTRADE);
/* TradeLines */
string cTRADE_LINES =
@"
create table TradeLines (
Id @PRIMARY_KEY
,TradeId integer not null
,TradeItemId integer not null
,Qty float default 1 not null
,Price float not null
,constraint FK_TradeLines_00 foreign key (TradeId) references Trade (Id)
,constraint FK_TradeLines_01 foreign key (TradeItemId) references TradeItem (Id)
);
";
Table = schema.AddTable("TradeLines", cTRADE_LINES);
}
根据 Tripous 的术语,Trader、TradeItem 和 Trade 是我们的主表。TradeLines 是 Trade 表的详细表。
@PRIMARY_KEY
、@DATE
等仅仅是占位符。Tripous 会将这些占位符替换为 *Profiles.XML* 文件中指定的数据库服务器使用的实际数据类型。这就是您获得自动创建架构所需的所有内容。同一数据库未来的架构更改将在相同的 `Name` 和更高的 `Version` 下注册。这样,当您发布下一版应用程序时,您的服务和支持人员就不可能忘记(哦,告诉我吧)应用所需的架构更改。
当 Tripous 创建上述架构时,它会在 SYS_INI 系统表中写入一个条目,以了解架构的当前版本。下一次应用程序运行时,它会比较这两个版本来决定如何操作。如果 `RegisterSchemas()` 方法在 `SchemaDatastores` 中注册的架构比 SYS_INI 中存在的架构旧,它将什么也不做。因此,上述代码将只执行一次,前提是尚不存在 SYS_INI 表,或者 SYS_INI 中的条目版本小于或等于该行中的版本。
SchemaDatastore schema = schemaDatastores.FindForce(Sys.MAIN, 1);
业务模型注册表
`Tripous.BusinessModel.Registry` 类是 tripous 业务模型的注册表。有大量的**描述符类**描述其他类和操作给 Tripous 系统。这些描述符中的大多数都注册到 `Registry` 类,它是关于业务模型类的中央注册表。
应用程序管理器的大多数 `RegisterXXXX()` 方法都涉及 `Registry` 类及其描述符。主要的描述符类是:
BrokerDescriptor
FormDescriptor
Command
,本身不是描述符LocatorDescriptor
CodeDescriptor
所有这些描述符在其组中都有唯一的名称(`Name` 属性)。
Broker 注册
`Tripous.BusinessModel.Broker` 类是 Tripous 的业务对象。它代表一个业务逻辑域或业务逻辑模块。
Broker 实际上是一组相关表(由 `DataTable` 对象表示)和代码。可以使用已注册的 `BrokerDescriptor` 以声明式方式定义 Broker。`BrokerDescriptor` 类是 Tripous 的描述符类,它描述了 `Broker` 类的结构和属性。稍后,系统可以使用 `Registry` 中找到的 `BrokerDescriptor` 提供的描述来创建实际的 `Broker` 类实例。
这是 `BrokerDescriptors.Add()` 方法的头部:
public BrokerDescriptor Add(string DatastoreName, string Name, string MainTableName,
string Title, string BrokerClassName, string CodeProducerName);
`ApplicationManager.RegisterBrokers()` 方法是进行 Broker 注册的地方。这是我们小型应用程序的该方法的代码。将以下方法添加到 `ApplicationManager` 类中:
protected override void RegisterBrokers()
{
base.RegisterBrokers();
BrokerDescriptor Broker;
TableDescriptor Table;
JoinTableDescriptor JoinTable;
/* Company */
Broker = Registry.Brokers.Add(Sys.MAIN, "Company", "Company",
"Company", "SqlBroker", string.Empty);
/* Trader */
Broker = Registry.Brokers.Add(Sys.MAIN, "Trader", "Trader",
"Trader", "SqlBroker", "SIMPLE XXXXXX");
Table = Broker.Tables.Add("Trader", "Trader");
Table.Fields.Add("Id", SimpleType.Integer, 0, "Id", FieldFlags.None);
Table.Fields.Add("Code", SimpleType.String, 32, "Code",
FieldFlags.Visible | FieldFlags.Searchable |
FieldFlags.Required | FieldFlags.ReadOnlyUI);
Table.Fields.Add("Name", SimpleType.String, 48, "Name",
FieldFlags.Visible | FieldFlags.Searchable | FieldFlags.Required);
Table.Fields.Add("IsCustomer", SimpleType.Integer, 0, "Is customer",
FieldFlags.Visible | FieldFlags.Required |
FieldFlags.Boolean).DefaultValue = "1";
Table.Fields.Add("IsSupplier", SimpleType.Integer, 0, "Is supplier",
FieldFlags.Visible | FieldFlags.Required | FieldFlags.Boolean);
Table.Fields.Add(Sys.CompanyFieldName, SimpleType.Integer, 0,
"Company", FieldFlags.Required);
/* TradeItem */
Broker = Registry.Brokers.Add(Sys.MAIN, "TradeItem", "TradeItem",
"TradeItem", "SqlBroker",
"SIMPLE XXX-XXX");
Table = Broker.Tables.Add("TradeItem", "TradeItem");
Table.Fields.Add("Id", SimpleType.Integer, 0, "Id", FieldFlags.None);
Table.Fields.Add("Code", SimpleType.String, 32, "Code",
FieldFlags.Visible | FieldFlags.Searchable |
FieldFlags.Required | FieldFlags.ReadOnlyUI);
Table.Fields.Add("Name", SimpleType.String, 48, "Name",
FieldFlags.Visible | FieldFlags.Searchable | FieldFlags.Required);
Table.Fields.Add("Price", SimpleType.Float, 0, "Price",
FieldFlags.Visible | FieldFlags.Required).DefaultValue = "0";
Table.Fields.Add(Sys.CompanyFieldName, SimpleType.Integer, 0,
"Company", FieldFlags.Required);
/* Trade */
Broker = Registry.Brokers.Add(Sys.MAIN, "Trade", "Trade",
"Trade", "SqlBroker",
"SIMPLE XXX-XXX");
Broker.BrowserSql.Text =
@"
select
Trade.Id as Id
,Trade.Code as Code
,Trade.TradeDate as TradeDate
,Trader.Name as Trader__Name
from
Trade
left join Trader on Trader.Id = Trade.TraderId
";
Broker.BrowserSql.DisplayLabels.Add("Trader__Name=Trader");
Table = Broker.Tables.Add("Trade", "Trade");
Table.Fields.Add("Id", SimpleType.Integer, 0, "Id", FieldFlags.None);
Table.Fields.Add("Code", SimpleType.String, 32, "Code",
FieldFlags.Visible | FieldFlags.Searchable |
FieldFlags.Required | FieldFlags.ReadOnlyUI);
Table.Fields.Add("TraderId", SimpleType.Integer, 0,
"TraderId", FieldFlags.Required);
Table.Fields.Add("TradeDate", SimpleType.Date, 0, "Date",
FieldFlags.Visible | FieldFlags.Searchable |
FieldFlags.Required).DefaultValue = "AppDate";
Table.Fields.Add(Sys.CompanyFieldName, SimpleType.Integer, 0,
"Company", FieldFlags.Required);
Broker.LinesTableName = "TradeLines";
Table = Broker.Tables.Add(Broker.LinesTableName, Broker.LinesTableName);
Table.MasterTableName = "Trade";
Table.MasterKeyField = "Id";
Table.DetailKeyField = "TradeId";
Table.Fields.Add("Id", SimpleType.Integer, 0,
"Id", FieldFlags.None);
Table.Fields.Add("TradeId", SimpleType.Integer, 0,
"TradeId", FieldFlags.Required);
Table.Fields.Add("TradeItemId", SimpleType.Integer, 0,
"TradeItemId", FieldFlags.Required);
{
JoinTable = Table.JoinTables.Add("TradeItem", "TradeItemId");
JoinTable.Fields.Add("Id", SimpleType.Integer, 0, "Id", FieldFlags.None);
JoinTable.Fields.Add("Code", SimpleType.String, 32,
"Item Code", FieldFlags.Visible | FieldFlags.Searchable);
JoinTable.Fields.Add("Name", SimpleType.String, 48,
"Item", FieldFlags.Visible | FieldFlags.Searchable);
//JoinTable.Fields.Add("Price", SimpleType.Float, 0,
// "Item Price", FieldFlags.None);
}
Table.Fields.Add("Qty", SimpleType.Float, 0, "Qty",
FieldFlags.Visible | FieldFlags.Required).DefaultValue = "1";
Table.Fields.Add("Price", SimpleType.Float, 0, "Price",
FieldFlags.Visible | FieldFlags.Required).DefaultValue = "0";
}
有些 Broker 描述得非常详细,而有些则不。这取决于应用程序的需求和业务逻辑的复杂性。在任何情况下,当构建实际的 `Broker` 实例时,Tripous 会使用数据库表信息检查其描述,并可能进行许多更正和补充。
`Add()` 方法的 `BrokerClassName` 参数对应于 `BrokerDescriptor.TypeClassName` 属性,并且非常重要。该参数/属性的值用于获取实际的 `Broker` 类类型。
所有上述 `BrokerDescriptor` 项都将 `SqlBroker` 类定义为 Broker 的 `BrokerClassName`。`Tripous.BusinessModel.SqlBroker` 是处理数据库数据的 Broker 的基类。该 `SqlBroker` 类知道如何处理大多数常见的数据录入场景,只需提供其描述符信息即可。当标准的 `SqlBroker` 类不够用时,程序员可以从它派生一个新的 Broker 类,以满足其特定需求。
在 Tripous 业务模型描述符系统中,所有事物都有自己的描述符类。`TableDescriptor` 描述 `BrokerDescriptor` 的表,而 `FieldDescriptor` 描述表的字段。有一个 `JoinTableDescriptor` 描述连接到另一个表的表,甚至是递归的方式。您可以看到 Tripous 使用您在注册 Broker 时提供的信息来为 Broker 的“编辑部分”构建 SELECT
、INSERT
、UPDATE
和 DELETE
SQL 语句。
`BrokerDescriptor` 提供了 `BrokerDescriptor.BrowserSql` 属性,您可以在其中定义 Broker 的“浏览部分”的 SELECT
语句。
主从关系可以轻松定义,正如您通过观察上面的 Trade-TradeLines 表所示。上面的示例还包含一个 `JoinTableDescriptor`。
**重要提示**:当描述符注册需要类名时(如上面的 `SqlBroker`),您应提供完整的名称(包括命名空间),除非它是 Tripous 类。
定位器?什么是定位器?
**定位器**(Locator)是 Tripous 的一个概念,它指的是一段知道如何在特定 WHERE
条件下定位数据库记录的代码。有一个定位器控件(`LocatorBox`)、`DataGridView` 的定位器列(`LocatorColumn`)、一个包含实际定位器逻辑的定位器组件(`Locator`),当然,还有一个 `LocatorDescriptor`。
`ApplicationManager.RegisterLocators()` 方法是进行定位器注册的地方。这是该方法的代码。将以下方法添加到 `ApplicationManager` 类中。
protected override void RegisterLocators()
{
base.RegisterLocators();
LocatorDescriptor Locator;
/* Customer */
Locator = Registry.Locators.Add("Customer", "TraderId",
"Trader", "Id");
Locator.Fields.Add(SimpleType.Integer, "TraderId", "Id", "Trader");
Locator.Fields.Add(SimpleType.String, "Trader__Code",
"Code", "Trader", "Customer Code");
Locator.Fields.Add(SimpleType.String, "Trader__Name",
"Name", "Trader", "Customer");
Locator.SelectSql.Text = @"select Id, Code, Name from Trader where IsCustomer = 1";
/* TradeItem */
Locator = Registry.Locators.Add("TradeItem", "TradeItemId",
"TradeItem", "Id");
Locator.Fields.Add(SimpleType.Integer, "TradeItemId", "Id", "TradeItem");
Locator.Fields.Add(SimpleType.String, "TradeItem__Code",
"Code", "TradeItem", "TradeItem Code");
Locator.Fields.Add(SimpleType.String, "TradeItem__Name",
"Name", "TradeItem", "TradeItem");
Locator.Fields.Add(SimpleType.Float, "Price", "Price",
"TradeItem", "Price").Searchable = false;
}
定位器是一个非常高级的主题,我们暂时忽略它。
命令注册
一个 `Command` 类似于一个按钮或菜单。但 `Command` 本身没有任何视觉表示。相反,它可以附加到一个按钮、菜单项或其他东西上。基本上,命令代表应用程序执行的某个操作。要么是用户操作的结果,要么是某些客户端代码的请求。
与 Tripous 中所有可注册对象一样,`Command` 也有一个唯一的名称。命令形成一个树结构,以模拟菜单、树视图等,这些都可以用来显示命令。命令还用于组合前面描述的 `CommandSet`。
一个 `Processor` 是一个特殊的命令,它是命令树的根。只要一个 `Command` 没有 `Parent`,它就认为自己是一个 `Processor`。一个 `Command` 有一个 `Kind`,指示命令的类型。它可以是:
容器
,分隔符
,Mdi
,Modal
,或者Procedure
.
`ApplicationManager.RegisterCommands()` 方法是进行命令注册的地方。这是该方法的代码。将以下方法添加到 `ApplicationManager` 类中。
protected override void RegisterCommands()
{
base.RegisterCommands();
Command P = mainProcessor;
Command Container;
/* Admin */
Container = Command.CreateContainer("ADMIN",
Res.GetString("Administration", "Administration"));
P.InsertAfter("FILE", Container);
Container.AddMdi("Trader",
Res.GetString("cmdTrader", "Traders"),
DataMode.Browse);
Container.AddMdi("TradeItem",
Res.GetString("cmdTradeItem", "Trade Items"),
DataMode.Browse);
Container.AddMdi("Trade",
Res.GetString("cmdTrade", "Trades"),
DataMode.Browse);
}
应用程序管理器已经有了自己的命令处理器。实际上是 `Registry.MainProcessor` 对象。`ApplicationManager.mainProcessor` 受保护字段返回 `Registry.MainProcessor`。这是 Tripous 本身用于注册其系统命令的处理器。FILE 命令就是这些系统命令之一。
我们的 `RegisterCommands()` 方法在系统 FILE 命令之后插入一个名为 ADMIN 的容器命令。然后它注册三个 MDI 表单命令项。`Form` 类型的命令在执行时必须创建一个 Windows Form 对象。
Tripous 将使用主处理器 `Registry.MainProcessor` 中的命令,以动态创建 `MainForm` 的主菜单项和其他命令表示。在 Tripous 中,您**不**创建主菜单项。只创建命令项。
表单注册
当执行一个名为“Trader”的表单命令时,它会尝试查找一个同名的 `FormDescriptor`。`FormDescriptor` 是一个非常简单的描述符,它只提供 `Form` 类的名称和其他一些设置。
`ApplicationManager.RegisterForms()` 方法是进行表单注册的地方。这是该方法的代码。将以下方法添加到 `ApplicationManager` 类中。
protected override void RegisterForms()
{
base.RegisterForms();
FormDescriptor FormDes;
FormDes = Registry.Forms.Add("Trader", "Traders",
"Project.TraderForm", "Trader", FormFlags.SingleInstance);
FormDes = Registry.Forms.Add("TradeItem", "Trade Items",
"Project.TradeItemForm", "TradeItem", FormFlags.SingleInstance);
FormDes = Registry.Forms.Add("Trade", "Trades",
"Project.TradeForm", "Trade", FormFlags.SingleInstance);
}
Tripous 也以同样的方式注册其系统表单。
正如您所看到的,有三个表单描述符项,其名称与相应的命令项相同。每个 `FormDescriptor` 项定义了自己的 `Form` 类名:`TraderForm`、`TradeItemForm` 和 `TradeForm`。这些表单对象尚不存在。我们的下一个任务是创建它们。
**重要提示**:当描述符注册需要类名时(如上面的 `TraderForm`),您应提供完整的名称(包括命名空间),`Project.TradeForm`,除非它是 Tripous 类。
创建表单
我们将创建 `TradeForm`。使用 Visual Studio 向导添加一个新表单(解决方案资源管理器 | 右键单击项目 | 添加 | Windows 窗体)。
将源代码文档命名为 *TradeForm.cs*,并将表单类命名为 `TradeForm`。
请确保新表单属于 `Project` 命名空间。同时检查 *TradeForm.cs* 和 *TradeForm.designer.cs* 文件以确保。
生成项目(Shift + F6)。
转到 *TradeForm.cs* 并向 `Tripous.Forms` 命名空间添加一个 using
。
将 `TradeForm` 的基类从 `Form` 更改为 `DataEntryBrokerForm`。这是完整代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Tripous.Forms;
namespace Project
{
public partial class TradeForm : DataEntryBrokerForm
{
public TradeForm()
{
InitializeComponent();
}
}
}
生成项目(Shift + F6)。现在,如果您在解决方案资源管理器中双击 `TradeForm`,您将看到表单的浏览部分,它类似于以下内容:
切换到“Data”选项卡。
添加一个 `Panel` 并将其 `Dock` 属性设置为 `Top`。
从 Toolbox 的 Tripous 选项卡中,添加一个 `DataGridViewEx` 并将其 `Dock` 属性设置为 `Fill`。
从 Toolbox 的 Tripous 选项卡中,向面板添加两个 `TextBoxEx` 文本框和一个 `LocatorBox`。在这些控件前面添加 `LabelEx` 标签。
Tripous.Forms.TextBoxEx 提供了额外的属性:`DataField` 和 `DataSourceName`。这两个属性用于将控件绑定到数据库数据。`DataField` 是必需的。所以将一个设置为 `Code`,另一个设置为 `TradeDate`。这些是 `Trade` 数据库表的字段。将 `DataSourceName` 留空。`TextBoxEx` 文本框的空 `DataSourceName` 属性指示 Tripous 将控件绑定到 `Broker.tblItem DataTable`,即 `Broker` 的顶层表。
您可以使用 `Item` 一词作为 `DataSourceName`,这与将其留空效果相同。当然,您可以显式地将 `DataSourceName` 设置为 `Trade`,这是我们在前面 `BrokerDescriptor` 中定义的顶层表的名称。
Tripous.Forms.LabelEx 默认情况下,其 `TextAlign` 属性设置为 `MiddleRight`。它还包含代码和逻辑,以便在文本更改时保留其右侧位置。将 `LabelEx` 的 `TextAlign` 属性设置为 `XXXRight`,并在其左侧留出足够的空间,可以更轻松地本地化表单。
Tripous.Forms.LocatorBox 是一个非常特殊的控件。它的部分功能在前面已经描述过。将其 `DataField` 属性设置为 `TraderId`,将其 `DescriptorName` 属性设置为 `Customer`。将 `DescriptorName` 设置为非空字符串会指示 Tripous 在注册表中搜索具有该 `Name` 的 `LocatorDescriptor`,并将其设置提供给控件。也可以将 `LocatorDescriptor` 拖放到表单上,并将该本地 `LocatorDescriptor` 名称分配给控件。查找定位器描述符的逻辑可以在 `DataEntryForm.FindLocatorDescriptor()` 虚拟方法中找到。
现在让我们设置 `DataGridViewEx`。
将其命名为 `gridLines`。将其 `DataSourceName` 属性设置为 `Lines`。这指示 Tripous 将控件绑定到 `BrokerDescriptor.LinesTableName`。当主表只有一个明细表时,这很有用,正如我们在这里与 Trade 和 TradeLines 表的情况一样。
`Tripous.DataGridViewEx` 提供了 `Locators` 属性,它是一个集合。单击 `Locators` 属性旁边的按钮,添加一个条目,并将其 `DescriptorName` 属性设置为 `TradeItem`,将其 `DataField` 属性设置为 `TradeItemId`。您**不**需要手动为网格创建列。Tripous 现在已经有足够的信息来设置网格。这些信息之前在 `BrokerDescriptor` 和 `LocatorDescriptor` 中提供。
TradeForm
的数据录入部分现在应该如下所示:
按照上面为 `TradeForm` 所描述的方式,创建另外两个表单:`TraderForm` 和 `TradeItemForm`。
运行应用程序
测试应用程序现在应该准备就绪。按 F5 键以调试器运行应用程序。
如果按住 Ctrl 键,则会出现“选择活动配置文件”对话框。
用户可以选择活动配置文件或创建一个新配置文件。如果活动配置文件的 MAIN 数据存储的数据库不存在,Tripous 将尝试创建它。
**警告**:如果您选择名为“Firebird”的配置文件作为活动配置文件,那么您应该将 *FirebirdSql.Data.FirebirdClient.dll* 从 *ThirdParty* 目录复制到您的 *bin* 目录。当然,您必须安装 Firebird。
接下来,将出现登录表单。
之后,您可以在不重新启动应用程序的情况下,以不同的用户身份登录,但不能选择不同的配置文件。
如果所选配置文件的 MAIN 数据存储的数据库不存在,Tripous 将尝试创建它。
Tripous 中有三个预定义且硬编码的用户帐户。最高级别是用户名 *god*,密码 *knows* 的帐户。另外两个是 *service-trustno1* 和 *sys-admin*。
输入密码并按确定。Tripous 会创建数据库(如果不存在),然后启动应用程序。然后显示 `MainForm`。
`MainForm` 创建 `ApplicationManager`,然后调用其 `ApplicationManager.ApplicationInitialize()`。检查 `Tripous.Forms.SysMainForm.ApplicationInitialize()` 方法以了解顺序。应用程序创建并显示 `MainForm` 中的菜单项。应用程序现在已准备就绪。
菜单栏中的第二个菜单项,标记为“Administration”的那个,包含了我们在自己的命令之后创建的菜单项。它包含触发命令对象的菜单项,这些命令对象最终会创建并显示我们定义的表单。
表单命令,即 `Kind` 设置为 `Mdi` 或 `Modal` 的 `Command`,最终由本应用程序的 `ApplicationManager` 基类 `Tripous.Forms.ApplicationManagerDesktop` 的 `ExecuteFormCommand()` 和 `ExecuteStandardFormCommand()` 虚拟方法处理。这是 `ExecuteStandardFormCommand()`:
protected virtual bool ExecuteStandardFormCommand(Command Command)
{
if (IsFormCommand(Command))
{
LastFormCommand = Command;
DataEntryForm.Show(Command);
return true;
}
return false;
}
如您所见,它只是调用静态方法 `DataEntryForm.Show()`,并将 `Command` 对象作为参数传递。`DataEntryForm.Show()` 是一个相当复杂的方法,简而言之,它从注册表中收集有关 `FormDescriptor` 和 `BrokerDescriptor` 的信息,创建表单实例和 Broker 实例,将两者连接起来,然后显示表单。
就是这样。我们刚刚创建了我们的第一个功能齐全的 Tripous 应用程序。