使用 VB.NET 构建网站 - 第 7 章:创建自定义模块






4.32/5 (7投票s)
2006 年 5 月 3 日
46分钟阅读

95666
在本章中,我们将引导您为 CoffeeConnections 门户创建一个自定义模块。
|
引言
在本章中,我们将引导您为 CoffeeConnections 门户创建一个自定义模块。一个自定义模块可以由一个或多个自定义 Web 控件组成。我们将涵盖的领域有:
- 创建私有程序集项目来构建和调试您的模块
- 创建视图和编辑控件
- 向模块设置页面添加额外选项
- 实现
IActionable
、ISearchable
和IPortable
接口 - 使用双列表控件
- 创建
SQLDataProvider
- 打包您的模块
- 上传您的模块
咖啡店列表模块概述
CoffeeConnections 门户的主要吸引力之一是用户将能够按邮政编码搜索其所在区域的咖啡店。搜索后,用户将看到其所在区域的咖啡店。为了让本章的重点放在模块开发上,我们将提供此控件的简化版本。我们不会花时间在所使用的 ASP.NET 控件或这些控件的验证上,而是只关注创建自定义模块所必需的内容。
设置您的项目(私有程序集)
我们将使用的设计环境是 Visual Studio .NET 2003。DotNetNuke 中使用的文件预打包为 VS.NET 解决方案,这是为 DotNetNuke 创建自定义模块的最佳方式。Visual Studio 将允许我们创建私有程序集 (PA),这将使我们的自定义模块代码与 DotNetNuke 框架代码分离。
私有程序集是一个将与应用程序一起部署并与该应用程序配合使用的程序集(.dll 或 .exe)。在我们的例子中,主应用程序是 DotNetNuke 核心框架。私有程序集将是一个添加到 DotNetNuke 解决方案 (.sln) 的项目。这将使我们的模块架构与 DotNetNuke 核心架构分离,但允许我们使用 Visual Studio 在框架内调试模块。由于在 PA 中构建模块可以使我们与 DotNetNuke 核心框架分离,因此升级到 DotNetNuke 的新版本是一个简单的过程。
尽管 DotNetNuke 框架是使用 VB.NET 构建的,但您可以使用任何 .NET 语言创建模块私有程序集。由于您的模块逻辑将被编译为 .dll,因此您可以使用您喜欢的语言进行编码。
DotNetNuke 项目分为许多不同的解决方案,使您能够处理项目的不同部分。我们已经看到了 HTTP 模块解决方案和 Providers 解决方案。由于我们希望查看 DotNetNuke 附带的默认模块,因此我们将使用 DotNetNuke.DesktopModules 解决方案。
您甚至可以创建一个新解决方案并将 DotNetNuke 项目添加到新解决方案中。然后,您需要创建一个构建支持项目来支持您的模块。我们正在使用 DotNetNuke.DesktopModules 解决方案,以便您能够查看默认模块以获取设计过程的帮助。
要将您的私有程序集设置为 DotNetNuke.DesktopModules 解决方案的一部分,请执行以下步骤:
- 打开 DotNetNuke Visual Studio.NET 解决方案文件 (C:\DotNetNuke\Solutions\DotNetNuke.DesktopModules\ DotNetNuke.DesktopModules.sln)。
- 在解决方案资源管理器中,右键单击 DotNetNuke 解决方案(而不是项目),然后选择“添加”|“新建项目”
- 在“项目类型”中,确保突出显示“Visual Basic 项目”,然后选择“类库”作为您的项目类型。我们的控件将在 DotNetNuke 虚拟目录中运行,因此我们不想创建 Web 项目。这将创建一个我们不需要的额外虚拟目录。
- 您的项目应位于 C:\DotNetNuke\DesktopModules 文件夹下。请务必将位置更改为此文件夹。
- 您的项目名称应遵循以下约定:CompanyName.ModuleName。这将有助于避免与其他模块开发人员的名称冲突。我们的项目名为 EganEnterprises.CoffeeShopListing。您最终应该会在 DotNetNuke 解决方案中添加一个新项目。
如果您安装了 URLScan(Microsoft IIS Lockdown Tool 的一部分),则包含句点 (.) 的文件夹会遇到问题。在这种情况下,您可以使用下划线而不是句点来创建项目。有关 IIS Lockdown Tool 的更多信息,请参阅 Microsoft。
- 您需要修改一些属性以允许您在 DotNetNuke 解决方案中调试项目
- 在“公共属性”文件夹的“常规”部分下,删除“根命名空间”。我们的模块将在
DotNetNuke
命名空间下运行,因此我们不希望它默认为程序集名称。 - 删除随项目创建的 Class1.vb 文件。
- 右键单击我们的私有程序集项目,然后选择“属性”。
- 在“公共属性”文件夹的“常规”部分下,删除“根命名空间”。我们的模块将在
- 在“公共属性”文件夹的“导入”子部分下,我们希望添加导入,这将有助于我们创建自定义模块。将以下每个命名空间输入到命名空间框中,然后单击“添加导入”。
DotNetNuke
DotNetNuke.Common
DotNetNuke.Common.Utilities
DotNetNuke.Data
DotNetNuke.Entities.Users
DotNetNuke.Framework
DotNetNuke.Services.Exceptions
DotNetNuke.Services.Localization
DotNetNuke.UI
- 单击“确定”保存您的设置。
当我们在 DotNetNuke 中将项目作为私有程序集运行时,模块的 DLL 将构建到 DotNetNuke 的 bin 目录中。当 DotNetNuke 尝试加载您的模块时,它将在此处查找程序集。为了实现这一点,每个解决方案中都有一个名为 BuildSupport 的项目。BuildSupport 项目负责获取项目创建的 DLL 并将其添加到 DotNetNuke 解决方案的 bin 文件夹中。
为了允许 BuildSupport 项目添加我们的 DLL,我们需要添加对自定义模块项目的引用。
- 右键单击 BuildSupport 项目下方的引用文件夹,然后选择“添加引用”。
- 选择“项目”选项卡。
- 双击 EganEnterprises.CoffeeShopListing 项目将其放入“选定组件”框中。
- 单击“确定”添加引用。
最后,我们希望能够在私有程序集中使用 DotNetNuke 中所有可用的对象,因此我们需要在项目中添加对 DotNetNuke 的引用。
- 右键单击我们刚刚创建的 EganEnterprises.CoffeeShopListing 私有程序集项目下方的引用文件夹,然后选择“添加引用”。
- 选择“项目”选项卡。
- 双击 DotNetNuke 项目将其放入“选定组件”框中。
- 单击“确定”添加引用。
在继续之前,我们希望确保可以成功构建解决方案而没有任何错误。我们将在开发的不同阶段执行此操作,以帮助我们查明沿途可能犯的任何错误。
构建解决方案后,您应该在输出窗口中看到类似以下内容:
---------------------- Done ----------------------
Build: 35 succeeded, 0 failed, 0 skipped
您成功构建的数量可能不同,但请确保失败数量为零。如果存在任何错误,请在继续之前修复它们。
在 Visual Studio 中手动创建控件
当使用类库项目作为私有程序集的起点时,您无法通过从项目菜单中选择“添加”|“新建项”来向项目添加 Web 用户控件。因此,我们必须手动添加控件。
创建所需用户控件的一种可选方法是在 DotNetNuke 项目中创建 Web 用户控件,然后将该控件拖到您的 PA 项目中进行修改。
创建视图控件
视图控件是当您将模块添加到门户时非管理员看到的。换句话说,这是模块的公共接口。
让我们逐步创建此控件所需的步骤。
- 确保您的私有程序集项目突出显示,从“项目”菜单中选择“添加新项”。
- 从可用模板列表中选择“文本文件”,并将名称更改为 ShopList.ascx。
- 单击“打开”创建文件。
- 单击“HTML”选项卡,并在页面顶部添加以下指令
<%@ Control language="vb" AutoEventWireup="false" Inherits="EganEnterprises.CoffeeShopListing.ShopList" CodeBehind="ShopList.ascx.vb"%>
指令可以位于文件中的任何位置,但标准做法是将其放置在文件开头。此指令将语言设置为 VB.NET,并指定我们将继承的类和代码隐藏文件。
- 单击工具栏上的保存图标以保存页面。
- 在解决方案资源管理器中,右键单击 ShopList.ascx 文件,然后选择“查看代码”。
这将为我们刚刚创建的 Web 用户控件创建一个代码隐藏文件。代码隐藏文件遵循继承自 System.Web.UserControl
的普通 Web 用户控件的格式。但是,此控件基于 Web.UserControl
,将改为继承自 DotNetNuke
中的一个类。更改代码隐藏文件以使其看起来像下面的代码。这是整个代码隐藏页面,减去了 Web 表单设计器生成的代码
Imports DotNetNuke
Imports DotNetNuke.Security.Roles
Namespace EganEnterprises.CoffeeShopListing
Public MustInherit Class ShopList
Inherits Entities.Modules.PortalModuleBase
Implements Entities.Modules.IActionable
Implements Entities.Modules.IPortable
Implements Entities.Modules.ISearchable
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'Put user code to initialize the page here
End Sub
Public ReadOnly Property ModuleActions() As _
DotNetNuke.Entities.Modules.Actions.ModuleActionCollection _
Implements DotNetNuke.Entities.Modules.IActionable.ModuleActions
Get
Dim Actions As New _
Entities.Modules.Actions.ModuleActionCollection
Actions.Add(GetNextActionID, _
Localization.GetString( _
Entities.Modules.Actions.ModuleActionType.AddContent, _
LocalResourceFile), _
Entities.Modules.Actions.ModuleActionType.AddContent, _
"", _
"", _
EditUrl(), _
False, _
Security.SecurityAccessLevel.Edit, _
True, _
False)
Return Actions
End Get
End Property
Public Function ExportModule(ByVal ModuleID As Integer) _
As String Implements _
DotNetNuke.Entities.Modules.IPortable.ExportModule
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Function
Public Sub ImportModule(ByVal ModuleID As Integer, _
ByVal Content As String, _
ByVal Version As String, _
ByVal UserID As Integer) _
Implements _
DotNetNuke.Entities.Modules.IPortable.ImportModule
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Sub
Public Function GetSearchItems( _
ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
As DotNetNuke.Services.Search.SearchItemInfoCollection _
Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Function
End Class
End Namespace
让我们将上面的代码列表分解,以便我们更好地理解此部分中发生的事情。我们做的第一件事是添加 DotNetNuke
和 DotNetNuke.Security.Roles
的 Imports
语句,以便我们可以访问它们的方法而无需使用完全限定的名称。
Imports DotNetNuke
Imports DotNetNuke.Security.Roles
Namespace EganEnterprises.CoffeeShopListing
接下来,我们向类添加命名空间,并将其设置为继承自 Entities.Modules.PortalModuleBase
。这是 DotNetNuke
中所有模块控件的基类。使用基类是我们的控件保持一致并实现基本模块行为(如模块菜单和标题)的原因。此模块还使我们能够访问有用的项目,例如用户 ID、门户 ID 和模块 ID 等。
此部分随后通过实现三个不同的接口完成。这些接口允许我们向模块添加增强功能。我们只在此文件中实现 IActionable
接口。其他接口将仅放置在此文件中,以允许框架使用反射查看模块是否实现这些接口。其他接口的实际实现发生在我们将稍后创建的控制器类中。
Public MustInherit Class ShopList
Inherits Entities.Modules.PortalModuleBase
Implements Entities.Modules.IActionable
Implements Entities.Modules.IPortable
Implements Entities.Modules.ISearchable
由于我们将在本文件中实现 IActionable
接口,现在我们来看需要实现的 IActionable
ModuleActions
属性。
核心框架会自动创建某些菜单项。这包括移动、模块设置等。您可以通过实现此接口手动向菜单添加功能。
要向模块操作菜单添加一个操作菜单项,我们需要创建 ModuleActionCollection
的实例。这在 ModuleActions
属性声明中完成。
Public ReadOnly Property ModuleActions() As _
DotNetNuke.Entities.Modules.Actions.ModuleActionCollection _
Implements DotNetNuke.Entities.Modules.IActionable.ModuleActions
Get
Dim Actions As New _
Entities.Modules.Actions.ModuleActionCollection
然后,我们使用此对象的 Add
方法向菜单添加项。
Actions.Add(GetNextActionID, _
Localization.GetString( _
Entities.Modules.Actions.ModuleActionType.AddContent, _
LocalResourceFile), _
Entities.Modules.Actions.ModuleActionType.AddContent, _
"", _
"", _
EditUrl(), _
False, _
Security.SecurityAccessLevel.Edit, _
True, _
False)
Return Actions
End Get
End Property
Actions.Add
方法的参数是:
参数 |
类型 |
描述 |
|
|
|
|
|
标题是您模块上下文菜单中显示的内容。 |
|
|
如果您希望菜单项调用客户端代码 (JavaScript),那么这就是您放置命令名称的地方。这用于上下文菜单上的删除操作。选择删除项时,会弹出一个消息,要求您在执行命令之前确认您的选择。对于我们正在添加的菜单项,我们将此项留空。 |
|
|
这允许您为命令添加额外的参数。 |
|
|
这允许您设置一个自定义图标,使其显示在菜单选项旁边。 |
|
|
当您的菜单项被点击时,浏览器将重定向到此处。您可以使用标准 URL,或者使用 |
|
|
顾名思义,这是您要添加客户端脚本以在选择此项时运行的地方。这与上面的 |
|
|
这决定了用户在执行脚本时是否会收到通知。 |
|
|
这是一个 |
|
|
确定此项目是否可见。 |
|
|
确定信息是否在新窗口中显示。 |
您会注意到 Add
方法的第二个参数要求输入一个标题。这是您创建的菜单项上将显示的文本。在我们的代码中,您会注意到我们没有使用字符串,而是使用 Localization.GetString
方法从本地资源文件中获取文本。
Actions.Add(GetNextActionID, _
Localization.GetString( _
Entities.Modules.Actions.ModuleActionType.AddContent, _
LocalResourceFile), _
Entities.Modules.Actions.ModuleActionType.AddContent, _
"", _
"", _
EditUrl(), _
False, _
Security.SecurityAccessLevel.Edit, _
True, _
False)
本地化是 DotNetNuke 3.0 给我们带来的众多功能之一。它允许您将门户大部分部分的语言设置为您选择的语言。本地化有点超出本章的范围,但我们至少会为操作菜单实现它。
要添加本地化文件,我们首先需要创建一个文件夹来放置它。在解决方案资源管理器中右键单击 EganEnterprises.CoffeeShopListing 项目,然后选择“添加”|“新建文件夹”。将文件夹命名为 App_LocalResources。我们将在此处放置我们的本地化文件。要添加文件,请右键单击 App_LocalResources 文件夹,然后从菜单中选择“添加”|“添加新项”。从选项中选择“程序集资源文件”,并将其命名为 ShopList.ascx.resx。完成后单击“打开”。
在名称部分下,添加资源键 AddContent.Action
,并为其赋值“添加咖啡店”。我们之前使用 IActionable
接口实现的动作菜单使用此键将“添加咖啡店”放置在上下文菜单上。
要了解有关如何在 DotNetNuke 模块中实现本地化的更多信息,请参阅 DotNetNuke 本地化白皮书(\DotNetNuke\Documentation\Public\DotNetNuke Localization.doc)。
现在我们可以继续讨论其他接口。如前所述,这些接口只需要我们将已实现函数的 shell 添加到此文件中。这些将仅放置在此文件中,以允许框架使用反射来查看模块是否实现接口。我们稍后将在 CoffeeShopListingController
类中编写代码来实现这些接口。
Public Function ExportModule(ByVal ModuleID As Integer) _
As String Implements _
DotNetNuke.Entities.Modules.IPortable.ExportModule
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Function
Public Sub ImportModule(ByVal ModuleID As Integer, _
ByVal Content As String, _
ByVal Version As String, _
ByVal UserID As Integer) _
Implements DotNetNuke.Entities.Modules.IPortable.ImportModule
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Sub
Public Function GetSearchItems( _
ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
As DotNetNuke.Services.Search.SearchItemInfoCollection _
Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
' included as a stub only so that the core
'knows this module Implements Entities.Modules.IPortable
End Function
这是我们目前设置视图模块所需的所有代码。在 Visual Studio 中打开控件的显示部分,然后通过使用 Visual Studio 主菜单上的“表格”|“插入”|“表格”将一个 HTML 表格添加到表单中。将以下文本添加到表格中:
我们添加表格和文本是因为我们将在进行更高级的编码之前测试我们的模块,以确保一切正常。再次强调,在开发中设置测试点可以帮助您查明代码中可能引入的错误。一旦我们完成 Edit 和 Settings 控件的设置,我们将测试模块以确保我们没有遗漏任何东西。
模块编辑控件
编辑控件供管理员用于修改或更改模块的功能。要设置编辑控件,请按照我们创建视图控件的步骤进行操作,但以下情况除外:
- 不要实现
IPortable
、IActionable
和ISearchable
接口。上下文菜单仅适用于视图控件。 - 控制菜单用于导航到编辑控件。
- 将表中的文本更改为 EditShopList RowOne 和 EditShopList RowTwo。
- 将文件另存为 EditShopList.ascx。
在 HTML 部分添加以下内容
<%@ Control language="vb" AutoEventWireup="false"
Inherits="EganEnterprises.CoffeeShopListing.EditShopList"
CodeBehind="EditShopList.ascx.vb"%>
并将其添加到代码隐藏页面
Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
Public MustInherit Class EditShopList
Inherits Entities.Modules.PortalModuleBase
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'Put user code to initialize the page here
End Sub
End Class
End Namespace
再次,向您的控件添加一个 HTML 表格。在设计模式下查看您的控件时,它应该看起来像下图:
模块设置控件
DotNetNuke 框架允许您向模块设置页面添加自定义设置。为此,您需要实现一个设置控件。
要设置“设置”控件,请按照我们创建“视图”控件的步骤进行操作,但以下情况除外。
- 不要实现
IPortable
、IActionable
和ISearchable
接口。 - 将表中的文本更改为 OptionModule RowOne 和 OptionModule RowTwo。
- 将文件另存为 Settings.ascx。
在 HTML 部分添加以下内容
<%@ Control language="vb" AutoEventWireup="false"
Inherits="EganEnterprises.CoffeeShopListing.Settings"
CodeBehind="Settings.ascx.vb"%>
在代码隐藏部分有点棘手。与其他两个控件不同,此控件继承自 ModuleSettingsBase
而不是 PortalModuleBase
。当您尝试在设计模式下查看表单时,这会在 Visual Studio 设计器中导致问题。Visual Studio 设计器将显示以下错误。
这是因为 ModuleSettingsBase
有两个抽象方法需要我们实现:LoadSettings
和 UpdateSettings
。因此,除非您想只使用 HTML 设计控件,否则您将需要使用以下变通方法。
当您需要在设计器中查看此控件时,只需注释掉 Inherits ModuleSettingsBase
声明和两个公共重写方法(LoadSettings
和 UpdateSettings
),并改为继承自 PortalModuleBase
。然后,您可以将所有要使用的控件从工具箱拖放到表单上并进行调整。当您对设计器中的外观感到满意时,只需切换 Inherits 语句即可。目前,此控件的代码隐藏文件中所需的唯一代码是下面的代码。一旦我们创建了 DAL(数据访问层),我们将向此代码添加内容。
Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
Public Class Settings
Inherits Entities.Modules.ModuleSettingsBase
'Inherits Entities.Modules.PortalModuleBase
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'Put user code to initialize the page here
End Sub
Public Overrides Sub LoadSettings()
End Sub
Public Overrides Sub UpdateSettings()
End Sub
End Class
End Namespace
就像其他控件一样,向控件添加一个 HTML 表格,以便我们可以测试我们的模块到此为止。
完成所有控件后,构建您的项目并验证它是否成功构建。此时,模块仍然无法在 DotNetNuke 框架中的浏览器中查看。为此,您首先需要向门户添加模块定义。
添加模块定义
当您使用主机的文件管理器将免费或购买的模块上传到您的门户时,模块定义会自动为您添加。在开发模块时,您将希望能够使用 Visual Studio 在 DotNetNuke 环境中调试它们。这需要您手动添加模块定义。
添加模块定义后,当您以主机或管理员身份登录时,模块会出现在控制面板模块下拉列表中。它将您的控件连接到门户框架。要添加我们项目所需的模块定义:
- 按 F5 运行 DotNetNuke 解决方案,以主机身份登录,然后单击主机菜单上的“模块定义”选项。
- 在“模块定义”菜单下,选择“添加新模块定义”
- 输入模块名称和简短描述。完成后,单击“更新”链接
- 这将弹出一个新部分,允许您添加模块的定义。输入新定义名称,然后单击“添加定义”。这将把定义添加到“定义”下拉列表中,并弹出第三个部分,允许您添加在上一节中创建的控件
首先,我们将添加模块的视图控件。
- 单击“添加控件”链接开始。
- 输入控件的标题。这是将控件添加到选项卡时的默认标题。
- 从下拉列表中选择控件的源。您将选择控件的文件名。这是我们在上一节中创建的视图控件。从下拉列表中选择控件。
- 选择控件的类型。这是非管理员在门户上查看您的模块时将看到的控件。从下拉列表中选择“视图”。
- 完成后单击“更新”。
接下来,我们要添加编辑控件。
- 在“键”字段中输入“Edit”。这是我们之前创建的“操作菜单”将用于导航到此控件的键。
- 输入控件的标题。
- 从“源”下拉列表中选择 ShopListEdit.ascx 控件。
- 在“类型”下拉列表中选择“编辑”。
- 完成后单击“更新”。
最后,我们需要添加设置控件。
- 点击“添加控件”为该模块添加第三个控件。
- 在键字段中输入“Settings”。
- 输入控件的标题。
- 从“源”下拉列表中选择 Settings.ascx 控件。
- 在“类型”下拉列表中选择“编辑”。
- 完成后单击“更新”。
这将完成模块定义。您的控制页面将如下所示:
单击“主页”菜单项退出模块定义部分。
将您的模块添加到页面
在向模块添加实际功能之前的最后一步是将模块添加到页面。我更喜欢向门户添加一个测试选项卡来测试我的新模块。我们在向模块添加任何功能之前将它们添加到站点,以验证我们是否正确设置了它们。我们将分阶段进行,以便您可以轻松确定遇到的任何错误,确保开发的每个阶段都成功完成。
创建一个名为“测试选项卡”的选项卡,然后从控制面板上的“模块”下拉列表中选择 EganEnterprises ShopList(或您使用的名称),然后单击“添加”链接将其添加到页面上的一个窗格中。
如果一切顺利,您应该会在页面上看到我们创建的模块。验证您可以从上下文菜单访问自定义菜单项。选中后,它们应该会将您带到我们之前创建的“编辑”和“设置”控件。
为了让您的“模块设置”部分在模块设置页面中正确显示,请确保它继承自
ModuleSettingsBase
,而不是PortalModuleBase
。
我们现在有一个创建模块的基本模板。在我们为控件提供所需功能之前,我们需要构建数据层。
数据存储层
数据存储层由存储记录所需的表和访问这些表所需的存储过程组成。我们首先为 SQL Server 创建表和存储过程。
SQL Server
首先,我们需要创建用于存放咖啡店信息的表。在命名您的表和存储过程时,最好以您的公司名称作为前缀(CompanyName_)。这样做有两个原因:
- 它有助于避免您的模块覆盖同名表。简单的表名(如 options 或 tasks)将变为 EganEnterprises_options 或 EganEnterprises_tasks。其他开发人员创建同名表的可能性很小。
- 在 SQL Server Enterprise Manager 中,您的所有表和存储过程都分组在一起,便于查找和使用。
由于我们将使用 Microsoft SQL Server,因此我们将以脚本格式显示我们的表和存储过程信息。我们需要做的第一件事是创建用于存放咖啡店信息的表。这是我们希望收集并存储的每个咖啡店的特定信息。
CREATE TABLE [EganEnterprises_CoffeeShopInfo] (
[coffeeShopID] [int] IDENTITY (1, 1) NOT NULL ,
[moduleID] [int] NOT NULL ,
[coffeeShopName] [varchar] (100) NOT NULL ,
[coffeeShopAddress1] [varchar] (150) NULL ,
[coffeeShopAddress2] [varchar] (150) NULL ,
[coffeeShopCity] [varchar] (50) NOT NULL ,
[coffeeShopState] [char] (2) NOT NULL ,
[coffeeShopZip] [char] (11) NOT NULL ,
[coffeeShopWiFi] [smallint] NOT NULL ,
[coffeeShopDetails] [varchar] (250) NOT NULL
) ON [PRIMARY]
GO
接下来,我们需要创建一个表来存储我们的模块选项信息。这个简单的表只有两个字段,moduleID
和 AuthorizedRoles
,将用于处理我们将用于模块的自定义安全性。此信息将通过我们创建的“设置”控件访问,并将在模块设置页面上显示。
CREATE TABLE [EganEnterprises_CoffeeShopModuleOptions] (
[moduleID] [int] NOT NULL ,
[AuthorizedRoles] [varchar] (200) NOT NULL
) ON [PRIMARY]
GO
当我们创建脚本,这些脚本将在 PA 上传到站点时自动创建数据库表,我们将使用
databaseOwner
和objectQualifier
变量作为脚本的前缀,如下所示:CREATE TABLE databaseOwer {databaseOwner} {objectQualifier} [EganEnterprises_CoffeeShopInfo]在本章中,我们假设您使用 SQL Server 工具来创建数据库对象。如果您正在从主机菜单上的 SQL 选项运行这些脚本,您可以在运行它们之前将这些变量添加到脚本中。请确保选中“作为脚本运行”选项。
然后,我们需要创建访问表所需的存储过程。尽管我们可以创建将添加和更新记录等功能组合在同一存储过程中的存储过程,但我们将其分开,以便更易于阅读和理解。
以下过程将新条目添加到我们的咖啡店列表中
CREATE PROCEDURE dbo.EganEnterprises_AddCoffeeShopInfo
@moduleID int,
@coffeeShopName varchar(100) ,
@coffeeShopAddress1 varchar(150),
@coffeeShopAddress2 varchar(150),
@coffeeShopCity varchar(50) ,
@coffeeShopState char(2),
@coffeeShopZip char(11),
@coffeeShopWiFi int,
@coffeeShopDetails varchar(250)
AS
INSERT INTO EganEnterprises_CoffeeShopInfo (
moduleID,
coffeeShopName,
coffeeShopAddress1,
coffeeShopAddress2,
coffeeShopCity,
coffeeShopState,
coffeeShopZip,
coffeeShopWiFi,
coffeeShopDetails
)
VALUES (
@moduleID,
@coffeeShopName,
@coffeeShopAddress1,
@coffeeShopAddress2,
@coffeeShopCity,
@coffeeShopState,
@coffeeShopZip,
@coffeeShopWiFi,
@coffeeShopDetails
)
以下过程将角色添加到 CoffeeShopModuleOptions 表
CREATE PROCEDURE dbo.EganEnterprises_AddCoffeeShopModuleOptions
@moduleID int,
@authorizedRoles varchar(250)
AS
INSERT INTO EganEnterprises_CoffeeShopModuleOptions
(moduleId, AuthorizedRoles)
VALUES
(@moduleID, @authorizedRoles)
以下过程删除商店列表
CREATE PROCEDURE dbo.EganEnterprises_DeleteCoffeeShop
@coffeeShopID int
AS
DELETE
FROM EganEnterprises_CoffeeShopInfo
WHERE coffeeShopID = @coffeeShopID
以下过程检索授权添加商店的用户
CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopModuleOptions
@moduleId int
AS
SELECT *
FROM EganEnterprises_CoffeeShopModuleOptions
WHERE
moduleID = @moduleID
以下过程检索所有咖啡店
CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShops
@moduleId int
AS
SELECT coffeeShopID,
coffeeShopName,
coffeeShopAddress1,
coffeeShopAddress2,
coffeeShopCity,
coffeeShopState,
coffeeShopZip,
coffeeShopWiFi,
coffeeShopDetails
FROM EganEnterprises_CoffeeShopInfo
WHERE
moduleID = @moduleID
以下过程检索一个商店以进行编辑
CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopsByID
@coffeeShopID int
AS
SELECT coffeeShopID,
coffeeShopName,
coffeeShopAddress1,
coffeeShopAddress2,
coffeeShopCity,
coffeeShopState,
coffeeShopZip,
coffeeShopWiFi,
coffeeShopDetails
FROM EganEnterprises_CoffeeShopInfo
WHERE
coffeeShopID = @coffeeShopID
以下过程按邮政编码检索商店
CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopsByZip
@moduleID int,
@coffeeShopZip char(11)
AS
SELECT coffeeShopID,
coffeeShopName,
coffeeShopAddress1,
coffeeShopAddress2,
coffeeShopCity,
coffeeShopState,
coffeeShopZip,
coffeeShopWiFi,
coffeeShopDetails
FROM EganEnterprises_CoffeeShopInfo
WHERE
coffeeShopZip = @coffeeShopZip AND moduleID = @moduleID
以下过程更新咖啡店列表
CREATE PROCEDURE dbo.EganEnterprises_UpdateCoffeeShopInfo
@coffeeShopID int,
@coffeeShopName varchar(100),
@coffeeShopAddress1 varchar(150),
@coffeeShopAddress2 varchar(150),
@coffeeShopCity varchar(50),
@coffeeShopState char(2),
@coffeeShopZip char(11),
@coffeeShopWiFi int ,
@coffeeShopDetails varchar(250)
AS
UPDATE EganEnterprises_CoffeeShopInfo
SET coffeeShopName = isnull(@coffeeShopName,coffeeShopName),
coffeeShopAddress1 = isnull(@coffeeShopAddress1,
coffeeShopAddress1),
coffeeShopAddress2 = isnull(@coffeeShopAddress2,
coffeeShopAddress2),
coffeeShopCity = isnull(@coffeeShopCity,coffeeShopCity),
coffeeShopState = isnull(@coffeeShopState,coffeeShopState),
coffeeShopZip = isnull(@coffeeShopZip,coffeeShopZip),
coffeeShopWiFi = isnull(@coffeeShopWiFi,coffeeShopWiFi),
coffeeShopDetails = isnull(@coffeeShopDetails,
coffeeShopDetails)
WHERE coffeeShopID = @coffeeShopID
以下过程更新谁可以添加咖啡店列表
CREATE PROCEDURE dbo.EganEnterprises_UpdateCoffeeShopModuleOptions
@moduleID int,
@authorizedRoles varchar(250)
AS
UPDATE EganEnterprises_CoffeeShopModuleOptions
SET AuthorizedRoles = @AuthorizedRoles
WHERE moduleID = @moduleID
数据访问层 (DAL)
DotNetNuke 使用的提供程序模型允许您连接到您选择的数据库。它的设计使得通过简单地更改默认提供程序就可以切换核心和模块使用的数据存储。DAL 是我们放置每个我们希望支持的提供程序所需代码的地方。
在构建我们的 DAL 之前,我们需要创建几个文件夹来组织我们的项目。右键单击您的 PA 项目,然后选择“添加文件夹”。除了之前创建的 App_LocalResources 文件夹之外,再创建两个新文件夹:Providers 和 Installation。
Providers 文件夹将用于存放我们将要创建的提供者,而 Installation 文件夹将用于组织我们到达该部分时的安装文件。
要开始为我们的模块构建 DAL,请右键单击 EganEnterprises.CoffeeShopListing 项目,然后选择“添加类”。将类命名为 DataProvider.vb。这是将用于模块的基提供程序类。我们将逐步讨论此文件的每个部分。
我们首先需要添加一些类所需的导入语句。我们将在提供程序中使用缓存和反射
Imports System
Imports DotNetNuke
就像我们对控件所做的那样,我们希望将此类放在我们的 CompanyName.ModuleName 命名空间中
Namespace EganEnterprises.CoffeeShopListing
此类将用作我们提供程序的基类,因此我们将其声明为 MustInherit
。这意味着我们无法实例化此类;它只能用作我们提供程序的基类。
Public MustInherit Class DataProvider
接下来,我们需要声明将用作此类的单例对象
Private Shared objProvider As DataProvider = Nothing
我们使用单例对象来确保在任何给定时间只创建一个数据提供程序实例。构造函数用于实例化对象。在构造函数中,我们调用 CreateProvider
方法以确保只创建一个实例。
Shared Sub New()
CreateProvider()
End Sub
CreateProvider
方法使用反射来创建正在创建的数据提供程序的实例。我们向它传递提供程序类型、命名空间和程序集名称。
Private Shared Sub CreateProvider()
objProvider = _
CType(Framework.Reflection.CreateObject _
("data", "EganEnterprises.CoffeeShopListing", _
"EganEnterprises.CoffeeShopListing"), DataProvider)
End Sub
最后,Instance
方法用于实际创建数据提供程序的实例。
Public Shared Shadows Function Instance() As DataProvider
Return objProvider
End Function
在 DataProvider
类的底部,我们需要定义所有与我们已创建的存储过程相对应的抽象方法。这些方法被创建为 MustOverride
,因为我们需要在我们的提供程序对象中实现它们。
由于提供者模块允许使用任何数据存储,这些方法的实现将驻留在提供者中。在这里,我们将只创建方法的签名。参数名称与我们存储过程中的名称匹配(减去@)。如您所见,一旦实现,这些方法将负责模块表的所有插入、更新和删除。
' all core methods defined below
Public MustOverride Function EganEnterprises_GetCoffeeShops _
(ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function EganEnterprises_GetCoffeeShopsByZip _
(ByVal ModuleId As Integer, ByVal coffeeShopZip As String) _
As IDataReader
Public MustOverride Function EganEnterprises_GetCoffeeShopsByID _
(ByVal coffeeShopID As Integer) As IDataReader
Public MustOverride Function EganEnterprises_AddCoffeeShopInfo _
(ByVal ModuleId As Integer, _
ByVal coffeeShopName As String, _
ByVal coffeeShopAddress1 As String, _
ByVal coffeeShopAddress2 As String, _
ByVal coffeeShopCity As String, _
ByVal coffeeShopState As String, _
ByVal coffeeShopZip As String, _
ByVal coffeeShopWiFi As System.Int16, _
ByVal coffeeShopDetails As String) As Integer
Public MustOverride Sub EganEnterprises_UpdateCoffeeShopInfo _
(ByVal coffeeShopID As Integer, _
ByVal coffeeShopName As String, _
ByVal coffeeShopAddress1 As String, _
ByVal coffeeShopAddress2 As String, _
ByVal coffeeShopCity As String, _
ByVal coffeeShopState As String, _
ByVal coffeeShopZip As String, _
ByVal coffeeShopWiFi As System.Int16, _
ByVal coffeeShopDetails As String)
Public MustOverride Sub EganEnterprises_DeleteCoffeeShop _
(ByVal coffeeShopID As Integer)
Public MustOverride Function _
EganEnterprises_AddCoffeeShopModuleOptions _
(ByVal ModuleID As Integer, _
ByVal authorizedRoles As String) As Integer
我们有一个单独的表来存放模块的选项。选项表的定义放在这里。
'Options info
Public MustOverride Function _
EganEnterprises_GetCoffeeShopModuleOptions _
(ByVal ModuleID As Integer) As IDataReader
Public MustOverride Function _
EganEnterprises_UpdateCoffeeShopModuleOptions _
(ByVal ModuleID As Integer, _
ByVal authorizedRoles As String) _
As Integer
Public MustOverride Function _
EganEnterprises_AddCoffeeShopModuleOptions _
(ByVal ModuleID As Integer, _
ByVal authorizedRoles As String) _
As Integer
End Class
End Namespace
创建此类别后,我们需要创建模块将使用的 SqlDataProvider 项目。
SQLDataProvider 项目
SqlDataProvider 项目构建为一个单独的私有程序集。我们将再次创建一个类库类型项目。使用 CompanyName.ModuleName.SqlProvider 语法命名项目(其位置应为 Providers 文件夹)。在我们的例子中,该项目将命名为 EganEnterprises.CoffeeShopListing.SqlProvider,并将创建在 C:\DotNetNuke\DesktopModules\EganEnterprises.CoffeeshopListing\Providers 文件夹中。
就像我们对您的模块项目所做的那样,我们需要修改项目的一些属性。右键单击新项目并选择“属性”。这将弹出属性页。
在“公共属性”文件夹的“常规”部分下,清除“根命名空间”。
我们还希望项目构建到 DotNetNuke 项目的 bin 目录中。当 DotNetNuke 尝试加载提供程序时,它将在此处查找程序集。为了允许 BuildSupport 项目添加我们的 DLL,我们需要添加对 SqlDataProvider 项目的引用。
- 右键单击 BuildSupport 项目下方的引用文件夹,然后选择“添加引用”。
- 选择“项目”选项卡。
- 双击 EganEnterprises.CoffeeShopListing.SqlDataProvider 项目,将其放入“选定组件”框中。
- 单击“确定”添加引用。
最后,我们希望能够在私有程序集中使用 DotNetNuke 中所有可用的对象,因此我们需要添加对 DotNetNuke 项目的引用。
- 右键单击我们刚刚创建的
EganEnterprises.CoffeeShopListing.SqlDataProvider
私有程序集项目下方的引用文件夹,然后选择“添加引用”。 - 选择“项目”选项卡。
- 双击 DotNetNuke 项目将其放入“选定组件”框中。
- 单击“确定”添加引用。
提供者文件
项目设置完成后,是时候创建 SqlDataProvider
类了。首先删除随项目创建的 Class1.vb 文件,然后右键单击项目并选择“添加类”。将文件命名为 SqlDataProvider.vb,然后单击“确定”。这将为您提供创建提供程序所需的 shell。我们将逐步介绍创建提供程序所需的修改。
您首先需要引入一些导入。其中大部分您应该很熟悉,但最突出的是 Microsoft.ApplicationBlocks.Data
。这是 Microsoft 创建的一个类,用于帮助处理与 SQL Server 配合使用所需的连接和命令。它用于简化对数据库的调用,而无需手动创建所有 ADO.NET 代码。您将在 DotNetNuke 项目的 C:\DotNetNuke\Providers\DataProviders\SqlDataProvider\SQLHelper 文件夹中找到此类。花时间仔细查看它;它的方法非常容易理解。我们将在数据提供程序中使用此类的 S 方法。首先,我们添加类所需的导入。
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.ApplicationBlocks.Data
Imports DotNetNuke
Imports DotNetNuke.Common.Utilities
Imports DotNetNuke.Framework.Providers
添加导入语句后,我们需要将类封装在模块的命名空间中。如您所见,我们还将继承自之前创建的 DataProvider
基类。我们还需要声明一个常量变量来保存提供程序的类型。DotNetNuke 中使用了许多不同的提供程序,因此我们需要指定类型。这通过将其分配给简单的 lowercase 字符串数据来完成。
Namespace EganEnterprises.CoffeeShopListing
Public Class SqlDataProvider
Inherits EganEnterprises.CoffeeShopListing.DataProvider
Private Const ProviderType As String = "data"
然后,我们使用此类型实例化数据提供程序配置
Private _providerConfiguration As _
ProviderConfiguration = _
ProviderConfiguration.GetProviderConfiguration _
(ProviderType)
然后,我们声明一些变量,它们将保存连接到数据库所需的信息
Private _connectionString As String
Private _providerPath As String
Private _objectQualifier As String
Private _databaseOwner As String
在类的构造函数中,我们读取在 web.config 文件中设置的属性,以填充数据库特定信息,例如连接字符串、数据库所有者等。
Public Sub New()
Dim objProvider As Provider = _
CType(_providerConfiguration.Providers _
(_providerConfiguration.DefaultProvider), _
Provider)
If objProvider.Attributes("connectionStringName") <> "" AndAlso _
System.Configuration.ConfigurationSettings.AppSettings _
(objProvider.Attributes("connectionStringName")) <> "" Then
_connectionString = _
System.Configuration.ConfigurationSettings.AppSettings _
(objProvider.Attributes("connectionStringName"))
Else
_connectionString = _
objProvider.Attributes("connectionString")
End If
_providerPath = objProvider.Attributes("providerPath")
_objectQualifier = _
objProvider.Attributes("objectQualifier")
If _objectQualifier <> "" And _
_objectQualifier.EndsWith("_") = False Then
_objectQualifier += "_"
End If
_databaseOwner = objProvider.Attributes("databaseOwner")
If _databaseOwner <> "" And _
_databaseOwner.EndsWith(".") = False Then
_databaseOwner += "."
End If
End Sub
Public ReadOnly Property ConnectionString() As String
Get
Return _connectionString
End Get
End Property
Public ReadOnly Property ProviderPath() As String
Get
Return _providerPath
End Get
End Property
Public ReadOnly Property ObjectQualifier() As String
Get
Return _objectQualifier
End Get
End Property
Public ReadOnly Property DatabaseOwner() As String
Get
Return _databaseOwner
End Get
End Property
如您所记,在基本提供程序类中,我们将方法声明为 MustOverride
。在本节中,我们正是这样做的。我们重写了基类中的方法,并使用 Microsoft.ApplicationBlocks.Data
类进行数据库调用。
GetNull
函数用于将应用程序编码的空值转换为为预期数据类型定义的数据库空值。我们将在本节的其余部分中利用这一点。
' general
Private Function GetNull(ByVal Field As Object) As Object
Return Null.GetNull(Field, DBNull.Value)
End Function
Public Overrides Function EganEnterprises_GetCoffeeShops( _
ByVal ModuleId As Integer) _
As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_GetCoffeeShops", _
ModuleId), _
IDataReader)
End Function
Public Overrides Function EganEnterprises_GetCoffeeShopsByZip( _
ByVal ModuleId As Integer, _
ByVal coffeeShopZip As String) _
As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_GetCoffeeShopsByZip", _
ModuleId, _
coffeeShopZip), _
IDataReader)
End Function
Public Overrides Function EganEnterprises_GetCoffeeShopsByID( _
ByVal coffeeShopID As Integer) _
As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_GetCoffeeShopsByID", _
coffeeShopID), _
IDataReader)
End Function
Public Overrides Function EganEnterprises_AddCoffeeShopInfo( _
ByVal ModuleId As Integer, _
ByVal coffeeShopName As String, _
ByVal coffeeShopAddress1 As String, _
ByVal coffeeShopAddress2 As String, _
ByVal coffeeShopCity As String, _
ByVal coffeeShopState As String, _
ByVal coffeeShopZip As String, _
ByVal coffeeShopWiFi As System.Int16, _
ByVal coffeeShopDetails As String) _
As Integer
Return CType(SqlHelper.ExecuteScalar(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_AddCoffeeShopInfo", _
ModuleId, _
coffeeShopName, _
GetNull(coffeeShopAddress1), _
GetNull(coffeeShopAddress2), _
coffeeShopCity, _
coffeeShopState, _
coffeeShopZip, _
coffeeShopWiFi, _
coffeeShopDetails), _
Integer)
End Function
Public Overrides Sub EganEnterprises_UpdateCoffeeShopInfo( _
ByVal coffeeShopID As Integer, _
ByVal coffeeShopName As String, _
ByVal coffeeShopAddress1 As String, _
ByVal coffeeShopAddress2 As String, _
ByVal coffeeShopCity As String, _
ByVal coffeeShopState As String, _
ByVal coffeeShopZip As String, _
ByVal coffeeShopWiFi As System.Int16, _
ByVal coffeeShopDetails As String)
SqlHelper.ExecuteNonQuery(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_UpdateCoffeeShopInfo", _
coffeeShopID, _
coffeeShopName, _
GetNull(coffeeShopAddress1), _
GetNull(coffeeShopAddress2), _
coffeeShopCity, _
coffeeShopState, _
coffeeShopZip, _
coffeeShopWiFi, _
coffeeShopDetails)
End Sub
Public Overrides Sub EganEnterprises_DeleteCoffeeShop( _
ByVal coffeeShopID As Integer)
SqlHelper.ExecuteNonQuery(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_DeleteCoffeeShop", _
coffeeShopID)
End Sub
Public Overrides Function EganEnterprises_GetCoffeeShopModuleOptions( _
ByVal ModuleId As Integer) _
As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_GetCoffeeShopModuleOptions", _
ModuleId), _
IDataReader)
End Function
Public Overrides Function EganEnterprises_UpdateCoffeeShopModuleOptions( _
ByVal ModuleID As Integer, _
ByVal AuthorizedRoles As String) _
As Integer
Return CType(SqlHelper.ExecuteNonQuery(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_UpdateCoffeeShopModuleOptions", _
ModuleID, _
AuthorizedRoles), _
Integer)
End Function
Public Overrides Function EganEnterprises_AddCoffeeShopModuleOptions( _
ByVal ModuleID As Integer, _
ByVal AuthorizedRoles As String) _
As Integer
Return CType(SqlHelper.ExecuteNonQuery(ConnectionString, _
DatabaseOwner & _
ObjectQualifier & _
"EganEnterprises_AddCoffeeShopModuleOptions", _
ModuleID, _
AuthorizedRoles), _
Integer)
End Function
End Class
End Namespace
业务逻辑层 (BLL)
此提供程序难题的第三部分是业务逻辑层 (BLL)。BLL 将我们刚刚完成的数据访问部分与表示层连接起来。由于我们将有一个设置控件,我们需要创建四个不同的类:
CoffeeShopListingInfo
CoffeeShopListingController
CoffeeShopListingOptionsInfo
CoffeeShopListingOptionsController
CoffeeShopListingInfo 和 CoffeeShopListingOptionsInfo
CoffeeShopListingInfo
和 CoffeeShopListingOptionsInfo
类是非常简单的类,它们包含我们需要传递给数据库层的信息。它们用于传递填充的对象而不是单独的信息片段。每个类都将包含与每个对象关联的所有信息。
我们首先添加 Imports
语句和 Namespace
声明。
Imports System
Imports System.Configuration
Imports System.Data
Namespace EganEnterprises.CoffeeShopListing
代码的下一个区域由保存数据的私有变量和允许设置和获取变量的公共属性组成。这两个类如下所示
Public Class CoffeeShopListingInfo
#Region "Private Members"
Private m_moduleID As Integer
Private m_coffeeShopID As Integer
Private m_coffeeShopName As String
Private m_coffeeShopAddress1 As String
Private m_coffeeShopAddress2 As String
Private m_coffeeShopCity As String
Private m_coffeeShopState As String
Private m_coffeeShopZip As String
Private m_coffeeShopWiFi As System.Int16
Private m_coffeeShopDetails As String
#End Region
#Region "Constructors"
Public Sub New()
End Sub
#End Region
#Region "Public Properties"
Public Property moduleID() As Integer
Get
Return m_moduleID
End Get
Set(ByVal Value As Integer)
m_moduleID = Value
End Set
End Property
Public Property coffeeShopID() As Integer
Get
Return m_coffeeShopID
End Get
Set(ByVal Value As Integer)
m_coffeeShopID = Value
End Set
End Property
Public Property coffeeShopName() As String
Get
Return m_coffeeShopName
End Get
Set(ByVal Value As String)
m_coffeeShopName = Value
End Set
End Property
Public Property coffeeShopAddress1() As String
Get
Return m_coffeeShopAddress1
End Get
Set(ByVal Value As String)
m_coffeeShopAddress1 = Value
End Set
End Property
Public Property coffeeShopAddress2() As String
Get
Return m_coffeeShopAddress2
End Get
Set(ByVal Value As String)
m_coffeeShopAddress2 = Value
End Set
End Property
Public Property coffeeShopCity() As String
Get
Return m_coffeeShopCity
End Get
Set(ByVal Value As String)
m_coffeeShopCity = Value
End Set
End Property
Public Property coffeeShopState() As String
Get
Return m_coffeeShopState
End Get
Set(ByVal Value As String)
m_coffeeShopState = Value
End Set
End Property
Public Property coffeeShopZip() As String
Get
Return m_coffeeShopZip
End Get
Set(ByVal Value As String)
m_coffeeShopZip = Value
End Set
End Property
Public Property coffeeShopWiFi() As System.Int16
Get
Return m_coffeeShopWiFi
End Get
Set(ByVal Value As System.Int16)
m_coffeeShopWiFi = Value
End Set
End Property
Public Property coffeeShopDetails() As String
Get
Return m_coffeeShopDetails
End Get
Set(ByVal Value As String)
m_coffeeShopDetails = Value
End Set
End Property
#End Region
End Class
Namespace EganEnterprises.CoffeeShopListing
Public Class CoffeeShopListingOptionsInfo
Private m_moduleID As Integer
Private m_AuthorizedRoles As String
Public Property moduleID() As Integer
Get
Return m_moduleID
End Get
Set(ByVal Value As Integer)
m_moduleID = Value
End Set
End Property
Public Property AuthorizedRoles() As String
Get
Return m_AuthorizedRoles
End Get
Set(ByVal Value As String)
m_AuthorizedRoles = Value
End Set
End Property
End Class
End Namespace
完成这些类后,我们创建控制器类。顾名思义,这些类负责控制模块的数据流。
CoffeeShopListingController 和 CoffeeShopListingOptionsController
CoffeeShopListingController
类与 CoffeeShopListingInfo
类配对,用于将 CoffeeShopListingInfo
对象传递给数据提供者。为了最大程度地减少从数据层填充自定义业务对象的任务,DotNetNuke 核心团队创建了一个通用实用程序类来帮助填充您的业务对象,即 CBO 类。此类包含两个公共函数——一个用于填充单个对象实例,一个用于填充对象集合。
有关自定义业务对象的更多信息,请参阅 C:\DotNetNuke\Documentation\Public 下的 DotNetNuke Data Access.doc。
在查看 CoffeeShopListingController
和 CoffeeShopListingOptionsController
类时,您会注意到一些事情:
- 对于像
EganEnterprises_AddCoffeeShopInfo
这样的函数,模块特定信息的参数不是单独传递,而是作为CoffeeShopListingInfo
对象传递。 - 用于填充数据的函数可以在
CBO
类中找到。此类使用数据库中立的对象来填充数据,以便可以将其传递给您选择的数据库。 - 我们将实现
ISearchable
和IPortable
接口。
首先,我们将查看 CoffeeShopListingController
类。我们首先将命名空间添加到类中。
Namespace EganEnterprises.CoffeeShopListing
接着是实际的类声明和接口的声明。
Public Class CoffeeShopListingController
Implements Entities.Modules.ISearchable
Implements Entities.Modules.IPortable
我们将代码分为两个不同的区域。在“公共方法”区域中,我们创建将对数据库进行调用的函数。我们使用调用已实现的 DataProvider
方法的 CBO
对象。请注意,我们将在 CoffeeShopListInfo
对象中传递有关咖啡店的详细信息,然后函数将分解调用 DataProvider
方法所需的所有单个项目。
#Region "Public Methods"
Public Function EganEnterprises_GetCoffeeShops( _
ByVal ModuleId As Integer) As ArrayList
Return CBO.FillCollection _
(DataProvider.Instance(). _
EganEnterprises_GetCoffeeShops _
(ModuleId), GetType(CoffeeShopListingInfo))
End Function
Public Function EganEnterprises_GetCoffeeShopsByZip( _
ByVal ModuleId As Integer, _
ByVal coffeeShopZip As String) _
As ArrayList
Return CBO.FillCollection _
(DataProvider.Instance(). _
EganEnterprises_GetCoffeeShopsByZip _
(ModuleId, coffeeShopZip), _
GetType(CoffeeShopListingInfo))
End Function
Public Function EganEnterprises_GetCoffeeShopsByID( _
ByVal coffeeShopID As Integer) As CoffeeShopListingInfo
Return CType(CBO.FillObject _
(EganEnterprises.CoffeeShopListing. _
DataProvider.Instance(). _
EganEnterprises_GetCoffeeShopsByID( _
coffeeShopID), GetType(CoffeeShopListingInfo)), _
CoffeeShopListingInfo)
End Function
Public Function EganEnterprises_AddCoffeeShopInfo( _
ByVal objShopList As _
EganEnterprises.CoffeeShopListing.CoffeeShopListingInfo) _
As Integer
Return CType(EganEnterprises.CoffeeShopListing. _
DataProvider.Instance(). _
EganEnterprises_AddCoffeeShopInfo( _
objShopList.moduleID, _
objShopList.coffeeShopName, _
objShopList.coffeeShopAddress1, _
objShopList.coffeeShopAddress2, _
objShopList.coffeeShopCity, _
objShopList.coffeeShopState, _
objShopList.coffeeShopZip, _
objShopList.coffeeShopWiFi, _
objShopList.coffeeShopDetails), Integer)
End Function
Public Sub EganEnterprises_UpdateCoffeeShopInfo( _
ByVal objShopList As _
EganEnterprises.CoffeeShopListing.CoffeeShopListingInfo)
EganEnterprises.CoffeeShopListing. _
DataProvider.Instance(). _
EganEnterprises_UpdateCoffeeShopInfo( _
objShopList.coffeeShopID, _
objShopList.coffeeShopName, _
objShopList.coffeeShopAddress1, _
objShopList.coffeeShopAddress2, _
objShopList.coffeeShopCity, _
objShopList.coffeeShopState, _
objShopList.coffeeShopZip, _
objShopList.coffeeShopWiFi, _
objShopList.coffeeShopDetails)
End Sub
Public Sub EganEnterprises_DeleteCoffeeShop( _
ByVal coffeeShopID As Integer)
EganEnterprises.CoffeeShopListing. _
DataProvider.Instance(). _
EganEnterprises_DeleteCoffeeShop(coffeeShopID)
End Sub
#End Region
请记住,当我们创建 ShopList.ascx.vb 文件时,我们只为接口创建了 shell。在我们的控制器类中,我们将编写这些接口的实现代码。
实现 IPortable
可以实现 IPortable
接口,以允许用户将数据从一个模块实例传输到另一个模块实例。这在模块的上下文菜单中访问。
要使用此接口,您需要实现两种不同的方法:ExportModule
和 ImportModule
。这些方法的实现会因模块中存储的数据而略有不同。由于我们将在模块中保存某些咖啡店的信息,因此这是我们需要导入和导出的信息。这通过使用 .NET 中内置的 System.XML
命名空间来完成。
ExportModule
方法使用我们的 EganEnterprises_GetCoffeeShops
存储过程来构建 CoffeeShopListingInfo
对象的 ArrayList
。然后,这些对象被转换为 XML 节点并返回给调用者。我们不需要自己调用 ExportModule
函数;当单击“导出”链接时,DotNetNuke 框架会获取此信息,并将数据导出到物理文件。
Public Function ExportModule(ByVal ModuleID As Integer) _
As String Implements _
DotNetNuke.Entities.Modules.IPortable.ExportModule
Dim strXML As String
Dim arrCoffeeShops As ArrayList = _
EganEnterprises_GetCoffeeShops(ModuleID)
If arrCoffeeShops.Count <> 0 Then
strXML += "<coffeeshops>"
Dim objCoffeeShop As CoffeeShopListingInfo
For Each objCoffeeShop In arrCoffeeShops
strXML += "<coffeeshop>"
strXML += "<name>" & _
XMLEncode(objCoffeeShop.coffeeShopName) & "</name>"
strXML += "<address1>" & _
XMLEncode(objCoffeeShop.coffeeShopAddress1) & "</address1>"
strXML += "<address2>" & _
XMLEncode(objCoffeeShop.coffeeShopAddress2) & "</address2>"
strXML += "<city>" & _
XMLEncode(objCoffeeShop.coffeeShopCity) & "</city>"
strXML += "<state>" & _
XMLEncode(objCoffeeShop.coffeeShopState) & "</state>"
strXML += "<zip>" & _
XMLEncode(objCoffeeShop.coffeeShopZip.ToString) & "</zip>"
strXML += "<wifi>" & _
XMLEncode(objCoffeeShop.coffeeShopWiFi.ToString) & "</wifi>"
strXML += "<details>" & _
XMLEncode(objCoffeeShop.coffeeShopDetails) & "</details>"
strXML += "</coffeeshop>"
Next
strXML += "</coffeeshops>"
End If
Return strXML
End Sub
ImportModule
方法则恰恰相反;它获取由 ExportModule
方法创建的 XML 文件,并创建 CoffeeShopListingInfo
项目。然后它使用 EganEnterprises_AddCoffeeShopInfo
方法将它们添加到数据库中,从而用传输的数据填充模块。
Public Sub ImportModule(ByVal ModuleID As Integer, _
ByVal Content As String, ByVal Version As String, _
ByVal UserID As Integer) _
Implements DotNetNuke.Entities.Modules.IPortable.ImportModule
Dim xmlCoffeeShop As XmlNode
Dim xmlCoffeeShops As XmlNode = _
GetContent(Content, "coffeeshops")
For Each xmlCoffeeShop In xmlCoffeeShops
Dim objCoffeeShop As New CoffeeShopListingInfo
objCoffeeShop.moduleID = ModuleID
objCoffeeShop.coffeeShopName = _
xmlCoffeeShop.Item("name").InnerText
objCoffeeShop.coffeeShopAddress1 = _
xmlCoffeeShop.Item("address1").InnerText
objCoffeeShop.coffeeShopAddress2 = _
xmlCoffeeShop.Item("address2").InnerText
objCoffeeShop.coffeeShopCity = _
xmlCoffeeShop.Item("city").InnerText
objCoffeeShop.coffeeShopState = _
xmlCoffeeShop.Item("state").InnerText
objCoffeeShop.coffeeShopZip = _
xmlCoffeeShop.Item("zip").InnerText
objCoffeeShop.coffeeShopWiFi = _
xmlCoffeeShop.Item("wifi").InnerText
objCoffeeShop.coffeeShopDetails = _
xmlCoffeeShop.Item("details").InnerText
EganEnterprises_AddCoffeeShopInfo(objCoffeeShop)
Next
End Sub
实现 ISearchable
DotNetNuke 3.0 带来了搜索门户内容的能力。为了允许搜索您的模块,您需要实现 ISearchable
接口。此接口只有一个方法需要您实现:GetSearchItems
。
此方法使用 DotNetNuke.Services.Search
命名空间中的 SearchItemCollection
来保存搜索中可用项目的列表。在我们的实现中,我们使用 EganEnterprises_GetCoffeeShops
方法用数据库中的咖啡店填充 ArrayList
。然后,我们使用返回到 ArrayList
的对象添加到 SearchItemInfo
对象。此对象的构造函数已重载,它包含标题、描述、作者和搜索键等项目。您放置在这些属性中的内容取决于您的数据。对于我们的咖啡店项目,我们将使用 coffeeShopName
、coffeeShopID
和 coffeeShopCity
来填充对象。
Public Function GetSearchItems _
(ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
As DotNetNuke.Services.Search.SearchItemInfoCollection _
Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
Dim SearchItemCollection As New SearchItemInfoCollection
Dim CoffeeShops As ArrayList = _
EganEnterprises_GetCoffeeShops(ModInfo.ModuleID)
Dim objCoffeeShop As Object
For Each objCoffeeShop In CoffeeShops
Dim SearchItem As SearchItemInfo
With CType(objCoffeeShop, CoffeeShopListingInfo)
SearchItem = New SearchItemInfo _
(ModInfo.ModuleTitle & " - " & .coffeeShopName, _
.coffeeShopName, _
Convert.ToInt32(10), _
DateTime.Now, ModInfo.ModuleID, _
.coffeeShopID.ToString, _
.coffeeShopName & " - " & .coffeeShopCity)
SearchItemCollection.Add(SearchItem)
End With
Next
Return SearchItemCollection
End Function
每次循环遍历数组列表时,都会向 SearchItemCollection
添加一个搜索项。核心框架会处理在您的门户上实现此功能所需的所有其他事项。
由于我们只需要为 CoffeeShopListingController
类实现接口,因此 CoffeeShopListingOptionsController
类的代码要简单得多。
Imports System
Imports System.Data
Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
Public Class CoffeeShopListingOptionsController
Public Function EganEnterprises_GetCoffeeShopModuleOptions( _
ByVal ModuleId As Integer) _
As ArrayList
Return CBO.FillCollection(DataProvider.Instance(). _
EganEnterprises_GetCoffeeShopModuleOptions(ModuleId), _
GetType(CoffeeShopListingOptionsInfo))
End Function
Public Function EganEnterprises_UpdateCoffeeShopModuleOptions( _
ByVal objShopListOptions As EganEnterprises. _
CoffeeShopListing.CoffeeShopListingOptionsInfo) _
As Integer
Return CType(DataProvider.Instance(). _
EganEnterprises_UpdateCoffeeShopModuleOptions( _
objShopListOptions.moduleID, _
objShopListOptions.AuthorizedRoles), _
Integer)
End Function
Public Function EganEnterprises_AddCoffeeShopModuleOptions( _
ByVal objShopListOptions As EganEnterprises. _
CoffeeShopListing.CoffeeShopListingOptionsInfo) _
As Integer
Return CType(DataProvider.Instance(). _
EganEnterprises_AddCoffeeShopModuleOptions( _
objShopListOptions.moduleID, _
objShopListOptions.AuthorizedRoles), Integer)
End Function
End Class
End Namespace
表示层
现在我们可以回到我们在私有程序集项目中创建的视图、编辑和设置控件。现在我们可以编写代码来与数据存储交互。
ShopList.aspx
我们的视图控件将由两个面板组成,在任何给定时刻只显示一个。第一个面板将用于按邮政编码搜索和查看咖啡店。
第二个面板将允许用户将咖啡店添加到数据库中。
由于在提交此表单时留空字段可能会导致各种运行时错误,因此在实际应用程序中,有必要对所有用户输入添加验证。您可以使用 ASP.NET 验证控件来完成此任务。
<%@ Control language="vb" AutoEventWireup="false"
Inherits="EganEnterprises.CoffeeShopListing.ShopList"
CodeBehind="ShopList.ascx.vb"%>
<asp:Panel id="pnlGrid" runat="server">
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%"
border="1">
<TR>
<TD>
<P align="center">Enter Zip code
<asp:TextBox id="txtZipSearch" runat="server">
</asp:TextBox>
<asp:LinkButton id="lbSearch" runat="server">Search
By Zip</asp:LinkButton></P>
</TD>
</TR>
<TR>
<TD>
<P align="center">
<asp:linkbutton id="lbAddNewShop" runat="server">
Add New Shop</asp:linkbutton></P>
</TD>
</TR>
</TABLE>
<asp:datagrid id="dgShopLists" runat="server" Width="100%"
BorderWidth="2px" BorderColor="Blue" AutoGenerateColumns="False">
<AlternatingItemStyle BackColor="Lavender">
</AlternatingItemStyle>
<HeaderStyle BackColor="Silver"></HeaderStyle>
<Columns>
<asp:TemplateColumn>
<ItemTemplate>
<asp:HyperLink id=hlcoffeeShopID runat="server"
Visible="<%# IsEditable %>"
NavigateUrl='<%# EditURL("coffeeShopID",
DataBinder.Eval(Container.DataItem,
"coffeeShopID")) %>'
ImageUrl="~/images/edit.gif">
</asp:HyperLink>
</ItemTemplate>
</asp:TemplateColumn>
<asp:BoundColumn DataField="coffeeShopName" ReadOnly="True"
HeaderText="Coffee Shop Name"></asp:BoundColumn>
<asp:BoundColumn DataField="coffeeShopAddress1"
ReadOnly="True" HeaderText="Address"></asp:BoundColumn>
<asp:BoundColumn DataField="coffeeShopCity" ReadOnly="True"
HeaderText="City"></asp:BoundColumn>
<asp:BoundColumn DataField="coffeeShopZip" ReadOnly="True"
HeaderText="Zip Code"></asp:BoundColumn>
</Columns>
</asp:datagrid>
</asp:Panel>
<asp:Panel id="pnlAdd" runat="server">
<TABLE id="Table2" cellSpacing="1" cellPadding="1"
width="100%" border="1">
<TR>
<TD align="center" bgColor="lavender" colSpan="2">
<STRONG><FONT color="#000000">Enter A New Coffee Shop
</FONT></STRONG></TD>
</TR>
<TR>
<TD>
<P align="center">ShopName</P>
</TD>
<TD>
<asp:textbox id="txtcoffeeShopName" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">Address1</P>
</TD>
<TD>
<asp:textbox id="txtCoffeeShopAddress1" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">Address2</P>
</TD>
<TD>
<asp:textbox id="txtCoffeeShopAddress2" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">City</P>
</TD>
<TD>
<asp:textbox id="txtcoffeeShopCity" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">State</P>
</TD>
<TD>
<asp:textbox id="txtcoffeeShopState" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">zip</P>
</TD>
<TD>
<asp:textbox id="txtcoffeeShopZip" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD height="31">
<P align="center">WiFi Yes or No</P>
</TD>
<TD height="31">
<asp:RadioButtonList id="rblWiFi" runat="server"
RepeatDirection="Horizontal">
<asp:ListItem Value="1">Yes</asp:ListItem>
<asp:ListItem Value="0">No</asp:ListItem>
</asp:RadioButtonList></TD>
</TR>
<TR>
<TD>
<P align="center">Extra Details</P>
</TD>
<TD>
<asp:TextBox id="txtcoffeeShopDetails" runat="server">
</asp:TextBox></TD>
</TR>
<TR>
<TD>
<P align="center"> </P>
</TD>
<TD>
<P>
<asp:LinkButton id="cmdAdd" runat="server"
CssClass="CommandButton" BorderStyle="none"
Text="Update">Add</asp:LinkButton>
<asp:LinkButton id="cmdCancel" runat="server"
CssClass="CommandButton" BorderStyle="none"
Text="Cancel" CausesValidation="False">
</asp:LinkButton>
</P>
</TD>
</TR>
</TABLE>
</asp:Panel>
我们将从查看代码隐藏文件开始,查看当搜索按钮被点击时触发的代码。此事件,当然,期望在执行之前在文本框中输入邮政编码。
第一行实例化一个 CoffeeShopListingController
对象。这是我们在上一节中创建的类,它处理与数据提供程序的接口。接下来,我们创建一个 ArrayList
来保存当我们调用 EganEnterprises_GetCoffeeShopsByZip
函数时返回的数据。此函数仅接受 ModuleID
和 Zipcode
作为参数。您会注意到我们只是输入了 ModuleID
,而从未声明该变量。ModuleID
是我们从 PortalModuleControl
类继承的变量。它将保存此模块的唯一 ModuleID
。这当然会填充我们的 ArrayList
,然后我们将其绑定到我们的 DataGrid
。
Private Sub lbSearch_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
Dim objCoffeeShops As New CoffeeShopListingController
Dim myList As ArrayList
myList = _
objCoffeeShops.EganEnterprises_GetCoffeeShopsByZip _
(ModuleId, txtZipSearch.Text)
Me.dgShopLists.DataSource = myList
Me.dgShopLists.DataBind()
End Sub
我们将查看的下一个方法是 AddNewShop 链接按钮的 Click
事件处理程序。正如我们在查看 Page_Load
事件时将看到的那样,此按钮仅适用于某些安全角色。此按钮单击只是将页面重定向回自身,并在末尾添加一个查询字符串。然后 NavigateURL
函数用于与 URL 重写协同工作。
Private Sub lbAddNewShop_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
Response.Redirect(NavigateURL(TabId, "", "Add=YES"), True)
End Sub
现在我们将查看 Page_Load
方法。事件首先检查是否存在 Add 查询字符串。基于此,控件将显示带有 DataGrid
的面板,或显示带有允许用户将咖啡店添加到列表的表单的面板。如果显示网格,我们将使用与搜索方法中相同的技术填充它。
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'If we are not adding show the grid
If (Request.Params("Add") Is Nothing) Then
'Grid panel is visible
pnlAdd.Visible = False
pnlGrid.Visible = True
'Then fill the grid
If Not Page.IsPostBack Then
Dim objCoffeeShops As New CoffeeShopListingController
Dim myList As ArrayList
myList = objCoffeeShops.EganEnterprises_GetCoffeeShops _
(ModuleId)
Me.dgShopLists.DataSource = myList
Me.dgShopLists.DataBind()
End If
我们现在将查看为门户设置的安全角色。我们希望能够与安全角色挂钩,只允许某些用户添加新的咖啡店。我们不想使用模块设置,因为那会赋予角色修改模块的权限,而这超出了我们想要的范围。我们将把可以添加咖啡店的安全角色保存到我们之前创建的选项表中。这将在 ShopListOptions
控件上完成。在本节中,我们将读取该表。
'Check roles to see if the user can add items to the listing
'String of roles for shoplist
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As CoffeeShopListingOptionsInfo
Dim arrShopRoles As ArrayList = _
objShopRoles.EganEnterprises_GetCoffeeShopModuleOptions _
(ModuleId)
'Put roles into a string
Dim shopRoles As String = ""
For Each objShopRole In arrShopRoles
shopRoles = objShopRole.AuthorizedRoles.ToString
Next
我们使用创建的 CoffeeShopListingOptionsController
类将授权添加咖啡店的角色放入分隔字符串中。
然后,我们使用门户设置和角色控制器来查找用户拥有的安全角色。这些角色被放入一个数组中,并与允许添加咖啡店的角色进行比较。
RoleController
类的工作方式与我们为模块创建的控制器类类似。
Dim bAuth = False
If UserInfo.UserID <> -1 Then
If UserInfo.IsSuperUser = True Then
bAuth = True
Else
Dim objRoles As New RoleController
Dim Roles As String() = objRoles.GetPortalRolesByUser _
(UserInfo.UserID, PortalSettings.PortalId)
Dim maxRows As Integer = UBound(Roles)
Dim i As Integer
For i = 0 To maxRows
Dim objRoleInfo As RoleInfo
objRoleInfo = objRoles.GetRoleByName(PortalId, Roles(i))
If shopRoles.IndexOf(objRoleInfo.RoleID & ";") <> -1 Then
bAuth = True
Exit For
End If
Next
End If
End If
如果用户获得授权,则“添加新商店”链接按钮可见。
If bAuth Then
lbAddNewShop.Visible = True
Else
lbAddNewShop.Visible = False
End If
如果 Add querystring 存在,那么我们想显示 Add panel 并隐藏网格面板。
Else ' If we are adding...
'Add panel is visible
pnlAdd.Visible = True
pnlGrid.Visible = False
End If
End Sub
我们将查看的下一部分是“添加”按钮单击事件。请记住,这仅在用户有权添加咖啡店时才可见。这就是添加输入到添加咖啡店表单中的信息的方式。
我们做的第一件事是创建 CoffeeShopListingInfo
类的一个实例,并用文本框中填写的信息填充它。
Private Sub cmdAdd_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
Dim objShopList As New CoffeeShopListingInfo
With objShopList
.moduleID = ModuleId
.coffeeShopID = coffeeShopID
.coffeeShopName = txtcoffeeShopName.Text
.coffeeShopAddress1 = txtCoffeeShopAddress1.Text
.coffeeShopAddress2 = txtCoffeeShopAddress2.Text
.coffeeShopCity = txtcoffeeShopCity.Text
.coffeeShopState = txtcoffeeShopState.Text
.coffeeShopZip = txtcoffeeShopZip.Text
.coffeeShopDetails = txtcoffeeShopDetails.Text
.coffeeShopWiFi = rblWiFi.SelectedValue
End With
然后,我们创建一个控制器类的实例,并将 objShopList
传递给 EganEnterprises_AddCoffeeShopInfo
函数。完成后,我们被重定向回控件的网格视图。
Dim objShopLists As New CoffeeShopListingController
coffeeShopID = _
objShopLists.EganEnterprises_AddCoffeeShopInfo(objShopList)
' Redirect back to the portal
Response.Redirect(NavigateURL())
End Sub
然后,我们通过向“取消”按钮单击事件添加重定向代码来完成。
Private Sub cmdCancel_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
' Redirect back to the portal
Response.Redirect(NavigateURL())
End Sub
EditShopList.ascx
EditShopList
控件的设计类似于视图控件上的“添加咖啡店”表单。唯一的区别是模块的管理员不仅可以添加新商店,还可以修改和删除它们。我们需要做的第一件事是构建管理员将使用的表单。
由于在提交此表单时留空字段可能会导致各种运行时错误,因此在实际应用程序中,有必要对所有用户输入添加验证。您可以使用 ASP.NET 验证控件来完成此任务。
<%@ Control language="vb" AutoEventWireup="false"
Inherits="EganEnterprises.CoffeeShopListing.EditShopList"
CodeBehind="EditShopList.ascx.vb"%>
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%" border="1">
<TR>
<TD>
<P align="center">ShopName</P>
</TD>
<TD><asp:textbox id="txtcoffeeShopName" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">Address1</P>
</TD>
<TD><asp:textbox id="txtCoffeeShopAddress1" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">Address2</P>
</TD>
<TD><asp:textbox id="txtCoffeeShopAddress2" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">City</P>
</TD>
<TD><asp:textbox id="txtcoffeeShopCity" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">State</P>
</TD>
<TD><asp:textbox id="txtcoffeeShopState" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD>
<P align="center">zip</P>
</TD>
<TD><asp:textbox id="txtcoffeeShopZip" runat="server">
</asp:textbox></TD>
</TR>
<TR>
<TD height="31">
<P align="center">WiFi Yes or No</P>
</TD>
<TD height="31">
<asp:RadioButtonList id="rblWiFi" runat="server"
RepeatDirection="Horizontal">
<asp:ListItem Value="1">Yes</asp:ListItem>
<asp:ListItem Value="0">No</asp:ListItem>
</asp:RadioButtonList></TD>
</TR>
<TR>
<TD>
<P align="center">Extra Details</P>
</TD>
<TD>
<asp:TextBox id="txtcoffeeShopDetails" runat="server">
</asp:TextBox></TD>
</TR>
<TR>
<TD>
<P align="center">∓nbsp;</P>
</TD>
<TD>
<P>
<asp:LinkButton id="cmdUpdate" runat="server"
Text="Update" BorderStyle="none"
CssClass="CommandButton"></asp:LinkButton>
<asp:LinkButton id="cmdCancel" runat="server"
Text="Cancel" BorderStyle="none"
CssClass="CommandButton"
CausesValidation="False"></asp:LinkButton>
<asp:LinkButton id="cmdDelete" runat="server"
Text="Delete" BorderStyle="none"
CssClass="CommandButton"
CausesValidation="False"></asp:LinkButton>
</P>
</TD>
</TR>
</TABLE>
我们将从查看代码隐藏文件开始,查看当 Page_Load
事件触发时执行的代码。我们做的第一件事是检查查询字符串中是否存在 coffeeShopID
。这将用于确定这是更新还是新记录。
Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
Public MustInherit Class EditShopList
Inherits Entities.Modules.PortalModuleBase
Dim coffeeShopID As Integer = -1
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
' get parameter
If Not (Request.Params("coffeeShopID") Is Nothing) Then
coffeeShopID = _
Integer.Parse(Request.Params("coffeeShopID"))
Else
coffeeShopID = Null.NullInteger
End If
然后,如果这不是页面回发,我们向 cmdDelete
按钮添加一些 JavaScript,这将使他们在删除发生之前确认其操作。尽管此代码显示在服务器端,但此操作将在客户端使用。
If Page.IsPostBack = False Then
cmdDelete.Attributes.Add("onClick", _
"javascript:return confirm('Are You" & _
" Sure You Wish To Delete This Item ?');")
接下来,我们检查 coffeeShopID
值,以确定它是更新还是新记录。如果 coffeeShopID
不为 Null
,则它是一个现有记录。
If Not DotNetNuke.Common.Utilities.Null.IsNull(coffeeShopID) Then
由于记录存在,我们需要创建一个 CoffeeShopListingController
并使用它从数据库获取信息。此信息加载到 CoffeeShopListingInfo
对象中,并用于填充表单上的文本框。
If Not objCoffeeShop Is Nothing Then
txtcoffeeShopName.Text = objCoffeeShop.coffeeShopName
txtCoffeeShopAddress1.Text = objCoffeeShop.coffeeShopAddress1
txtCoffeeShopAddress2.Text = objCoffeeShop.coffeeShopAddress2
txtcoffeeShopCity.Text = objCoffeeShop.coffeeShopCity
txtcoffeeShopState.Text = objCoffeeShop.coffeeShopState
txtcoffeeShopZip.Text = objCoffeeShop.coffeeShopZip
If objCoffeeShop.coffeeShopWiFi Then
rblWiFi.Items(0).Selected = True
Else
rblWiFi.Items(1).Selected = True
End If
txtcoffeeShopDetails.Text = objCoffeeShop.coffeeShopDetails
Else
' If object has no data we want to go back
Response.Redirect(NavigateURL())
End If
如果这是一个新记录,那么我们只需要从表单中删除“删除”链接。
Else
' This is new item
cmdDelete.Visible = False
End If
一旦我们确定这是一条新记录,我们就要查看点击“更新”按钮时调用的代码。同样,我们将同时使用 CoffeeShopListingInfo
对象和 CoffeeShopListingController
对象。我们用表单中找到的数据填充第一个对象,并使用最后一个对象调用更新或插入代码。
Private Sub cmdUpdate_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
Try
Dim objShopList As New CoffeeShopListingInfo
objShopList.moduleID = ModuleId
objShopList.coffeeShopID = coffeeShopID
objShopList.coffeeShopName = txtcoffeeShopName.Text
objShopList.coffeeShopAddress1 = txtCoffeeShopAddress1.Text
objShopList.coffeeShopAddress2 = txtCoffeeShopAddress2.Text
objShopList.coffeeShopCity = txtcoffeeShopCity.Text
objShopList.coffeeShopState = txtcoffeeShopState.Text
objShopList.coffeeShopZip = txtcoffeeShopZip.Text
objShopList.coffeeShopDetails = txtcoffeeShopDetails.Text
objShopList.coffeeShopWiFi = rblWiFi.SelectedValue
Dim objShopLists As New CoffeeShopListingController
If Null.IsNull(coffeeShopID) Then
coffeeShopID = _
objShopLists.EganEnterprises_AddCoffeeShopInfo(objShopList)
Else
objShopLists.EganEnterprises_UpdateCoffeeShopInfo(objShopList)
End If
' Redirect back to the portal
Response.Redirect(NavigateURL())
Catch ex As Exception
ProcessModuleLoadException(Me, ex)
End Try
End Sub
我们将看的最后一部分是当“删除”按钮被点击时调用的。此代码使用 coffeeShopID
并调用 EganEnterprises_DeleteCoffeeShop
存储过程。
Private Sub cmdDelete_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
If Not Null.IsNull(coffeeShopID) Then
Dim objShopLists As New CoffeeShopListingController
objShopLists.EganEnterprises_DeleteCoffeeShop(coffeeShopID)
End If
' Redirect back to the portal
Response.Redirect(NavigateURL())
End Sub
这完成了我们的编辑控件,只剩下我们的设置控件。
Settings.ascx
“设置”控件允许您为模块设置将在模块设置页面中显示的其他属性。我们目前只保存一个属性,但它是一个独特的属性。我们希望能够与 DotNetNuke 中内置的安全角色挂钩,并使用它们来决定哪些用户可以添加项目,而无需授予他们上下文菜单的访问权限。为此,我们使用 DotNetNuke 控件文件夹中的 DualList
控件。
为了能够在设计模式下使用此控件,您首先需要将类更改为继承自
PortalModuleBase
而不是ModuleSettingsBase
。请确保您在完成后将此更改改回,否则它将无法正常工作。
我们将在 HTML 文本框中添加一个双列表控件。这是设置控件的代码:
<%@ Register TagPrefix="Portal"
TagName="DualList"
Src="~/controls/DualListControl.ascx" %>
<%@ Control language="vb" AutoEventWireup="false"
Inherits="EganEnterprises.CoffeeShopListing.Settings"
CodeBehind="Settings.ascx.vb"%>
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%"
border="1">
<TR>
<TD>
<P align="center">ShopListOptions RowOne</P>
</TD>
</TR>
<TR>
<TD><portal:duallist id="ctlAuthRoles" runat="server"
ListBoxWidth="130" ListBoxHeight="130"
DataValueField="Value" DataTextField="Text" /></TD>
</TR>
</TABLE>
<asp:LinkButton id="lbUpdate" runat="server">Update</asp:LinkButton>
为了让这个控件集成到模块设置页面中,我们需要在基类中重写两个方法:LoadSettings
和 UpdateSettings
。当访问模块设置页面时调用 LoadSettings
,当在模块设置页面上点击更新按钮时调用 UpdateSettings
。
我们将使用模块设置页面的选项部分来保存此模块的安全设置,这些设置超出了正常的模块安全设置范围。我们希望让用户能够添加咖啡店,而无需授予他们上下文菜单的访问权限,并且我们还希望读取和存储我们 EganEnterprises_ShopListOptions 表中的已分配角色。
我们将从 LoadSettings
方法开始,声明 ArrayList
对象来保存我们的可用角色和授权角色。
' declare roles
Dim arrAvailableAuthRoles As New ArrayList
Dim arrAssignedAuthRoles As New ArrayList
可用角色从门户中检索,并与门户安全相关联。
' Get list of possible roles
Dim objRoles As New RoleController
Dim objRole As RoleInfo
Dim arrRoles As ArrayList = _
objRoles.GetPortalRoles(PortalId)
授权角色从我们的 EganEnterprises_ShopListOptions 表中获取。
'String of roles for shoplist
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As CoffeeShopListingOptionsInfo
Dim arrShopRoles As ArrayList = _
objShopRoles.EganEnterprises_GetCoffeeShopModuleOptions _
(ModuleId)
这仅返回与此模块对应的单个分号分隔字符串。
'Put roles into a string
Dim shopRoles As String = ""
For Each objShopRole In arrShopRoles
'If it makes it here then we will be updating
shopRoles = objShopRole.AuthorizedRoles.ToString
Next
然后我们遍历门户中所有可用的角色,并将它们放置在正确的列表中。
'Now loop through all available roles in portal
For Each objRole In arrRoles
Dim objListItem As New ListItem
objListItem.Value = objRole.RoleID.ToString
objListItem.Text = objRole.RoleName
'If it matches a role in the ShopRoles string put
'it in the assigned box
If shopRoles.IndexOf(objRole.RoleID & ";") _
<> -1 Or objRole.RoleID = _
PortalSettings.AdministratorRoleId Then
arrAssignedAuthRoles.Add(objListItem)
Else ' put it in the available box
arrAvailableAuthRoles.Add(objListItem)
End If
Next
' assign to duallist controls
ctlAuthRoles.Available = arrAvailableAuthRoles
ctlAuthRoles.Assigned = arrAssignedAuthRoles
双列表的内置功能允许您在列表之间移动角色,以授予或撤销用户的权限。
UpdateSettings
方法将授权列表保存到我们的表中。我们从列表框构建一个以分号分隔的列表,并使用我们的 CoffeeShopListingOptionsInfo
和 CoffeeShopListingOptionsController
类将其添加到表中。
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As New CoffeeShopListingOptionsInfo
Dim item As ListItem
Dim strAuthorizedRoles As String = ""
For Each item In ctlAuthRoles.Assigned
strAuthorizedRoles += item.Value & ";"
Next item
objShopRole.AuthorizedRoles = strAuthorizedRoles
objShopRole.moduleID = ModuleId
Dim intExists As Integer
intExists = objShopRoles. _
EganEnterprises_UpdateCoffeeShopModuleOptions(objShopRole)
If intExists = 0 Then
'New record
objShopRoles.EganEnterprises_AddCoffeeShopModuleOptions _
(objShopRole)
End If
这完成了我们模块所需的所有三个控件。我们剩下的就是测试我们的工作。
测试您的模块
在整个开发过程中,您应该使用 Visual Studio 的所有调试功能来确保您的代码正常工作。由于我们将模块设置为 DotNetNuke 解决方案中的私有程序集,您将能够在各种监视窗口中设置断点并查看您的代码。请确保您的项目已设置为允许调试。
您的项目属性配置应设置为“活动 (调试)”,并且应启用 ASP.NET 调试器。您还需要确保 web.config 文件中的 debug
设置为 true
。完成模块调试后,您就可以打包并准备分发了。
创建您的安装脚本
准备分发模块的第一步是创建安装脚本,这些脚本用于创建模块所需的表和过程。应该有两个文件:一个安装脚本和一个卸载脚本。您应该按照以下方式命名您的脚本。
脚本类型 |
描述 |
示例 |
卸载脚本 |
将单词“卸载”与脚本所代表的提供程序类型连接起来。 |
|
安装脚本 |
将模块的版本号与脚本所代表的提供程序类型连接起来。 |
01.00.00.SqlDataProvider 01.00.00.AccessDataProvider |
这些脚本与本章开头用于创建表的代码类似。您的 PA 安装脚本应使用 databaseOwner
和 objectQualifier
变量,并包含检查您正在创建的数据库对象是否已存在于数据库中的代码。这将有助于确保上传您的模块不会覆盖以前的数据。完整的脚本可在本章的代码下载中找到。
您的脚本版本号非常重要。如果模块的某个版本已安装在您的门户上,框架会检查脚本文件上的版本号以确定是否运行脚本。如果文件上的数字与数据库中的数字匹配,则脚本将不会运行。通过这种方式,您可以让一个包既用作安装包又用作升级包。
打包您的模块以供分发
为了将我们的模块包准备好供大众使用,我们首先需要为我们的模块创建一个清单。DotNetNuke 使用一个基于 XML 的文件,其扩展名为 .dnn 来完成此操作。由于这是一个 XML 文件,因此重要的是要注意它需要格式良好。这意味着所有开放标签 <mytag>
都需要有相关的关闭标签 </mytag>
。
要开始设置我们的清单,右键单击我们之前创建的 Installation 文件夹,然后选择“添加”|“添加新项”。从列表中选择“XML 文件”,并将文件命名为 CoffeeShopListing.dnn。DotNetNuke 使用 .dnn 扩展名将此文件指定为模块安装文件。下面您将看到文件本身。
外部标签 <dotnetnuke>
和 </dotnetnuke>
用于告诉上传器正在上传的项目的版本和类型。<folder>
元素然后开始映射它将放置我们模块所有文件的位置。
<?xml version="1.0" encoding="utf-8" ?>
<dotnetnuke version="3.0" type="Module">
<folders>
<folder>
<name>
元素是为您的模块创建的文件夹的名称。此文件夹将在 DotNetNuke\DesktopModules 文件夹下创建。重要的是,在创建模块时,您要遵循 CompanyName.ModuleName 格式,以避免与其他模块开发人员发生命名冲突。然后,<version>
元素确定正在上传的模块版本。接着是 <businesscontrollerclass>
元素。如果您在模块中实现了我们之前讨论的任何接口,您将需要此元素才能使导入、导出和搜索功能正常工作。此元素包含模块的完整类名(包括命名空间),后跟模块的程序集名称。
由于我们的控制器类名为 CoffeeShopListingController
,因此我们将在此节点内使用它。
<name>EganEnterprises.CoffeeShopListing</name>
<description>Listing of Coffee Shops</description>
<version>01.00.00</version>
<businesscontrollerclass>EganEnterprises.CoffeeShopListing.
CoffeeShopListingController,EganEnterprises.
CoffeeShopListing
</businesscontrollerclass>
然后,我们开始描述控件本身。我们在构建私有程序集时手动创建控件时遵循相同的过程。<key>
是您的上下文菜单与控件连接的方式。这对于视图控件是省略的。<title>
是将显示在模块定义表单中的内容。<src>
是控件的物理文件名,而 <type>
决定这是视图控件还是编辑控件。我们的编辑和选项控件都使用此元素。
<modules>
<module>
<friendlyname>Coffee Shop Listing</friendlyname>
<controls>
<control>
<title>View Coffee Shops</title>
<src>ShopList.ascx</src>
<type>View</type>
</control>
<control>
<key>Edit</key>
<title>Edit CoffeeShop Listing</title>
<src>EditShopList.ascx</src>
<type>Edit</type>
</control>
<control>
<key>Settings</key>
<title>Shop List Settings</title>
<src>Settings.ascx</src>
<type>Edit</type>
</control>
</controls>
</module>
</modules>
创建 <module>
标签后,我们需要声明模块的物理文件。务必包含控件和 DLL,以及模块的安装和卸载脚本。
<files>
<file>
<name>ShopList.ascx</name>
</file>
<file>
<name>EditShopList.ascx</name>
</file>
<file>
<name>Settings.ascx</name>
</file>
<file>
<name>01.00.00.SqlDataProvider</name>
</file>
<file>
<name>Uninstall.SqlDataProvider</name>
</file>
<file>
<name>EganEnterprises.CoffeeShopListing.dll</name>
</file>
<file>
<name>EganEnterprises.CoffeeShopListing
.SqlDataProvider.dll</name>
</file>
</files>
</folder>
</folders>
</dotnetnuke>
安装 ZIP 文件
现在是时候将所有文件打包成一个 ZIP 文件,以便能够将它们上传并安装到您的门户。不要仅仅将包含这些文件的文件夹拖到 ZIP 文件中。确保它们都在主 ZIP 文件夹中。DNN 框架将负责将文件放置在正确的文件夹中。
您需要放入 ZIP 文件中的文件有:
- EganEnterprises.CoffeeShopListing.dll
- EganEnterprises.CoffeeShopListing.SqlDataProvider.dll
- ShopList.ascx
- EditShopList.ascx
- Settings.ascx
- CoffeeShopListing.dnn
- 01.00.00.SqlDataProvider
- uninstall.SqlDataProvider
测试您的安装
这是最后一步。在此阶段,您所有的编码都应该运行良好,因为您已在 Visual Studio .NET 环境中对其进行了测试。现在您需要测试上传模块是否能正常工作。您有几个选择。由于您已经在 Visual Studio 环境中手动设置了此模块,因此您必须删除私有程序集并删除数据库中的表才能完全测试您的上传文件是否有效。我不喜欢这种方法。我喜欢我的 PA 保持原样,以便于进一步开发。我用于测试的方法是在我的开发计算机上设置一个单独的 DotNetNuke 实例,该实例仅用于测试模块上传。您可以决定哪种方法最适合您。
上传模块很简单。以主机身份登录,导航到主机菜单上的模块定义项。将光标悬停在上下文菜单上,然后选择“上传新模块”。浏览到您的 ZIP 文件,并将其添加到文件下载框中。单击“上传新文件”加载您的模块。
这将创建一个文件上传日志,该日志将仅显示在上传框下方的屏幕上。
在日志中搜索上传过程中可能发生的任何错误并修复它们。由于我们已经在 Visual Studio 中进行了测试,因此此处遇到的任何错误都应该与 ZIP 或 DNN 文件相关。
将您的模块添加到选项卡并进行全面测试。务必尝试所有功能。让其他人尝试“破坏”它是一个好主意。您会惊讶于用户会对您的模块做些什么。当所有功能都经过测试后,您就可以将其分发到 DotNetNuke 世界了。
摘要
我们在本章中涵盖了大量代码。从创建私有程序集和设置项目开始,到创建控件、业务逻辑层和数据访问层。然后,我们了解了如何打包模块以便共享。
由于此代码非常广泛,我们将其分解为几个部分,并建议您定期构建项目。这样做应该能够解决您在构建模块时遇到的任何问题。然后我们更进一步,向您展示了如何使用一些额外的项目,例如设置页面、双列表框和可选接口。这应该让您对所有不同部分如何协同工作有一个扎实的理解。
需要注意的是,一旦您熟悉了所有不同部分如何协同工作,有一些可用的工具可以帮助自动化构建模块的过程。Visual Studio DNN 项目模板可以在 DNNJungle 找到,CodeSmith 模板可以帮助您设计数据架构,可以在 Smcculloch.Net 找到。这两个工具可以极大地加快您的模块开发速度。与任何代码向导一样,如果您不理解底层代码,那么您将花费更多时间尝试弄清楚向导创建了什么,并将失去获得的任何优势。希望本章为您构建知识奠定了坚实的基础。