为桌面和 Web 创建一个应用程序
在 Habanero 的支持下,您可以使用 Visual WebGui 编写一个应用程序,然后部署到桌面或 Web。
引言
毫无疑问,Web 2.0 和 AJAX 已经简化了将应用程序概念从草图变为实际产品的开发流程。即便如此,桌面应用程序仍然有其优势——启动速度快、响应丰富、独立于网络连接。而第三种场景是:提供一个可以两种方式运行的应用程序。
背景
Habanero 于 2007 年 6 月作为免费开源项目推出,主要是一个面向 .NET 的对象关系映射 (ORM) 框架,它超越了普通的 ORM 数据焦点,提供了一个运行时生成的表示层。Habanero 采用测试驱动开发进行协作编程,简单地实现了完整的开发流程。Habanero 的开发者 Chillisoft 多年来一直为工业和商业客户生产定制软件,Habanero 封装了大多数常用例程。
Chillisoft 一直偏爱开发桌面应用程序,因为 ASP.NET 的工作方式很麻烦。当 Gizmox 发布 Visual WebGui 时,这种观点发生了改变。Visual WebGui (VWG) 在 Web 环境中模拟了 WinForms 的 API 和外观,为开发人员提供了一个使用桌面方法设计 Web 应用程序的平台。更妙的是,由于其与 WinForms 行为的相似性,Habanero 的表示层可以适应 VWG。Habanero 2.0 版本于 2008 年 7 月发布,成为一个完整的企业应用程序框架,支持 WinForms 和 VWG 的生成。
设置分层
附带的项目为一家名为 Replace-IT 的电脑零件供应商提供了一个简单的应用程序。我们将逐步介绍该应用程序的创建过程,您可以将示例代码作为参考。
ORM 的一个关键特性是将业务对象层从数据库中分离出来。在示例代码中,我们提供了一个 Firebird 数据库,但您同样可以轻松地将数据存储在 MySQL 或 SQL Server 中。我们在解决方案下提供了一个 BusinessObject (BO) 项目,其中存储了所有领域对象,例如 ComputerPart
、OrderItem
和 ComputerPartType
。
在我们的示例中,我们创建了一个通用的 UI 层,它会为部署环境构建适当的用户界面。最后,提供了两个执行项目,一个用于桌面应用程序,一个用于 VWG Web 应用程序。
运行应用程序
打开示例项目时,您需要设置 Firebird 数据库文件的绝对目录。在 Replace_it 项目下,打开 app.config 并设置数据库值以包含完整路径。在 Replace_it.VWG 项目下的 web.config 中也执行相同的操作。要选择运行哪个环境,只需右键单击 Replace_it 或 Replace_it.VWG,然后选择“设置为启动项目”。运行这两个应用程序,您会发现使用 WinForms 和互联网浏览器都能获得几乎相同的结果。
介绍 IControlFactory
显而易见的问题是:通用 UI 层如何生成特定类型的控件?另外,如何为控件分配自定义行为,使其在不同环境中表现不同?例如,Web 环境对数据获取要求很高,因此 VWG 提供了一个分页机制,可以防止用户在一个网格中加载一千个项目,而桌面应用程序通常可以通过更快的连接访问数据库服务器。
为了解决这些挑战,Habanero 使用一个控件工厂,该工厂可以生成具有特定行为的特定类型的控件。通用的 UI 控件管理器(例如窗体)现在可以通过调用传递给它的控件工厂的“创建”方法来创建特定的控件,而不是使用特定类型的构造函数。
让我们从代码中看一个例子。以下类用于生成一个显示计算机零件网格的窗体。
public class ComputerPartsGridManager : ControlManager
{
private IReadOnlyGridControl _computerPartsGrid;
public ComputerPartsGridManager(IControlFactory
controlFactory) : base(controlFactory)
{
}
protected override void InitialiseControl()
{
SetupComputerPartsGrid();
AddGridFilters();
LoadComputerPartsGrid();
}
private void SetupComputerPartsGrid()
{
_computerPartsGrid = _controlFactory.CreateReadOnlyGridControl();
BorderLayoutManager manager =
_controlFactory.CreateBorderLayoutManager(_control);
manager.AddControl(_computerPartsGrid, BorderLayoutManager.Position.Centre);
}
private void AddGridFilters()
{
BusinessObjectCollection<ComputerPartType> computerPartTypes =
Broker.GetBusinessObjectCollection<ComputerPartType>(null, "Code");
List<string> options = new List<string>();
computerPartTypes.ForEach(
delegate(ComputerPartType type) { options.Add(type.Code); });
_computerPartsGrid.FilterControl.AddStringFilterComboBox(
"Category:", "PartCode", options, false);
_computerPartsGrid.FilterControl.AddStringFilterTextBox(
"Description:", "Description");
}
private void LoadComputerPartsGrid()
{
BusinessObjectCollection<ComputerPart> computerParts =
new BusinessObjectCollection<ComputerPart>();
computerParts.LoadAll();
_computerPartsGrid.SetBusinessObjectCollection(computerParts);
}
}
通过构造函数传递适当的 ControlFactory
,并用于创建 ReadOnlyGridControl
,这是一个 Habanero 控件,可在网格中显示对象,并提供过滤数据和在弹出窗口中编辑单个项目的工具。请注意,该控件是使用接口 IReadOnlyGridControl
实例化的。
创建通用主菜单
有一个名为 UIManager
的中间类,它为应用程序提供主菜单,为每个菜单项实例化相应的控件,并将控件工厂传递过去。在可执行项目中,UIManagerWin
继承自 UIManager
,并实例化 ControlFactoryWin
以供 UI 层使用。最后一步是向项目的启动窗体/类添加代码。在 WinForms 可执行文件中,您会在 Program.cs 中添加两行代码,用于实例化一个类型的窗体并将其传递给 UIManager
。
代码中的流程
让我们从头开始,通过代码示例来跟踪这个过程。首先,在 Program.cs 中
FormWin programForm = new FormWin();
new UIManagerWin().SetupMainForm(programForm);
这会调用 UIManagerWin
中重写的方法
public class UIManagerWin : UIManager
{
public override void SetupMainForm(IFormHabanero programForm)
{
IMainMenuHabanero mainMenuHabanero =
SetupMainMenu(programForm, new ControlFactoryWin(),
new MenuBuilderWin());
mainMenuHabanero.DockInForm(programForm);
programForm.Text = GlobalRegistry.ApplicationName +
" " + GlobalRegistry.ApplicationVersion;
programForm.WindowState = FormWindowState.Maximized;
}
}
子类会调用父类,父类会设置主菜单并为每个菜单项分配控件。
public abstract class UIManager
{
public IMainMenuHabanero SetupMainMenu(IFormHabanero programForm,
IControlFactory controlFactory, IMenuBuilder menuBuilder)
{
HabaneroMenu menu = new HabaneroMenu("Main",
programForm, GlobalUIRegistry.ControlFactory);
HabaneroMenu dataMenu = menu.AddSubmenu("Data");
HabaneroMenu.Item productsMenuItem = dataMenu.AddMenuItem("Computer Parts");
productsMenuItem.ControlManagerCreator +=
delegate(IControlFactory factory)
{ return new ComputerPartsGridManager(factory); };
HabaneroMenu.Item customerOrdersMenuItem =
dataMenu.AddMenuItem("Customer Orders");
customerOrdersMenuItem.ControlManagerCreator +=
delegate(IControlFactory factory)
{ return new OrdersGridManager<CustomerOrder>(factory); };
HabaneroMenu.Item supplierOrdersMenuItem = dataMenu.AddMenuItem("Supplier Orders");
supplierOrdersMenuItem.ControlManagerCreator +=
delegate(IControlFactory factory)
{ return new OrdersGridManager<SupplierOrder>(factory); };
HabaneroMenu toolsMenu = menu.AddSubmenu("Tools");
HabaneroMenu.Item looklupEditorMenuItem = toolsMenu.AddMenuItem("Lookup Editor");
looklupEditorMenuItem.ControlManagerCreator +=
delegate(IControlFactory factory) { return new LookupEditorManager(factory); };
return menuBuilder.BuildMainMenu(menu);
}
public abstract void SetupMainForm(IFormHabanero programForm);
}
最后一步是实例化正确的控件,如前面所示的示例网格代码所示。
开发成本是多少?
这种设计用户界面的方法有三个主要好处。首先,您可以发布到多个环境。其次,这种设计非常适合表示层的测试驱动开发,这是开发人员经常回避的挑战。第三,ControlFactory
引入了相当大的灵活性。虽然我们已经讨论了 Web 和桌面之间的差异,但即使是单个桌面应用程序也可以使用不同的 ControlFactory
在不同时间为同一个控件生成不同的行为。
至于成本,Visual Studio 设计器不支持这种类型的处理(当然,有些人会说:摆脱设计器万岁!)。其次,开发模式略有改变。您不是直接实例化控件,而是通过 ControlFactory
进行调用,并将结果分配给接口而不是特定类型。
总的来说,上面提供的代码提供了一个相当简单的应用程序基础。在此基础上,您的大部分工作将用于扩展通用 UI 层,添加您需要的所有控件来提供一个可用的应用程序。
Habanero 是免费开源的,您可能想查看一些用于实现此结果的有趣代码结构。您可以在官方网站下载完整包并访问教程和支持。