DotNetNuke 模块打包器






4.67/5 (12投票s)
2004年12月22日
23分钟阅读

166883

1803
本文描述了 DotNetNuke 模块打包器应用程序和源代码。该应用程序允许用户从程序员开发环境中定义的自定义模块生成可直接使用的 DotNetNuke 私有程序集。
更新!
DotNetNuke 模块打包器已更新,可与 DotNetNuke 3 配合使用!
引言
您可能正在阅读本文,因为您创建了一个 DotNetNuke 模块,并希望将其部署为私有程序集,而无需手动完成所有创建包的工作。因此,您可能非常清楚 DotNetNuke 是什么,所以除了说它是一个构建强大 Web 应用程序的绝佳平台之外,我不会对其进行任何详细介绍。对于出于其他任何原因来到这里的人,您可以在 DotNetNuke 网站了解更多信息。
我构建 DNN 模块打包器有两个原因。首先,似乎任何其他免费工具都没有真正完全做到可以直接从我的开发环境生成可部署的私有程序集(甚至在不大幅修改包的情况下接近)。有些应用程序会生成一个您可以从中构建的 PA 外壳,但这也不是我真正想要的。
第二个原因是,唯一其他似乎能做我想做的事情的应用程序(我说似乎是因为我不愿意花钱去 выяснить)需要花钱。这对于在 DotNetNuke 中创建模块来说是如此基本的事情,显得有些愚蠢(请记住,我是一个比商人更好的慈善家)。我想要一个工具,可以从我在本地安装的 DotNetNuke 开发环境中一直在开发的模块创建一个私有程序集。基本的想法是,只需专注于构建模块的功能,然后让另一个应用程序处理创建私有程序集包。
为了开发这个应用程序,我有几个选择。我选择构建一个基于桌面 GUI 的应用程序,原因如下:
- 作为一名开发者,我喜欢我的开发工具简单而强大。将其本身安装为一个 Web 模块似乎违背了这一理想。
- 我估计每个人都和我差不多,希望有一个简单的工具,运行安装程序后就可以直接使用。
如果您与我不同,请随意深入研究代码并将其制作成模块或其他任何东西。请记住,许可证是 GPL。
我一开始创建了一个更像 MDI 的应用程序,可以在生成 .dnn 文件后编辑它,但我觉得那太过了,甚至没有多大意义。我决定最好使用向导式界面,因为我只是想引导最终用户完成为自定义 DNN 模块创建私有程序集的过程。市面上有几个向导控件,但我发现 Al Gardener 的 以设计器为中心的向导控件 确实是最好的。它的优点在于其事件处理在页面级别。这意味着您可以随意打乱页面的顺序,即使您已经在它们上面做了一些工作,逻辑也不会受到任何影响。我强烈推荐他的控件。
我还要补充最后一点。我在此应用程序中使用了几种不同的工具,所有这些工具都是开源的。对于压缩,我使用来自 Sharp Develop 团队的 ICSharpCode.SharpZipLib 程序集,当然,正如我之前提到的,我正在使用 Al Gardener 的以设计器为中心的向导控件。本文中的项目链接仅包含这些库的二进制文件,但是,您可以通过我在介绍中提供的链接获取它们的源代码。
假设
他们总是说你不应该做假设,因为它只会让你和我变得像个“笨蛋”。嗯,在这种情况下,这是我让这个应用程序工作的唯一方法,所以如果我让你觉得自己像个笨蛋,我道歉。不过,说真的,这些假设的优点在于,如果出现更好的方法,其中一些可以改变。我把它留给社区来帮助做出这个决定。好的,那么接下来是“笨蛋”的假设。
该应用程序假设
- 您一直在 DotNetNuke 中开发自定义模块,并且已通过主机帐户手动设置了模块定义和模块控件。这一点至关重要。打包器的全部理念是,开发人员可以使用他们的开发环境来生成私有程序集。如果您没有这样做,打包器将根本无法工作。
- 您已使用前缀创建了数据库对象(表、存储过程等),以帮助将您的对象与数据库中的其他对象区分开来。虽然这不是生成模块所必需的,但如果您确实有要脚本化的数据库对象,并且您没有提供前缀,则您必须手动脚本化这些对象,然后手动将脚本文件名添加到 .dnn 清单文件以及文件本身添加到 ZIP 包中。
- 您选择的 Web 应用程序(虚拟目录)实际上是一个 DotNetNuke 安装。当您在向导的第一步中选择一个 Web 应用程序时,打包器会通过该虚拟目录指向的本地文件系统查找 Web.config 文件。如果找不到,它将强制您选择不同的项目。从配置文件中,应用程序可以提取您的连接字符串。有了这个,它就可以完成所有需要做的事情。但是,如果在下一步中发现没有 DesktopModules 表,它将发出警告并强制您返回起始页再次选择不同的 Web 应用程序。
- 您没有使用 DotNetNuke 命名空间。这本应不言而喻,但是,我看到许多人试图将 DotNetNuke 命名空间用于其 DNN 模块,这让我不得不发出警告。如果您属于此类,请记住以下几点:如果您使用 DotNetNuke 命名空间,则无法构建私有程序集。如果您使用 DNN 命名空间,您将不得不重新编译主 DotNetNuke 程序集并将其与您的 PA 打包。我不确定模块安装程序是否会在安装过程中尝试覆盖 DotNetNuke.dll 时捕获到这一点,但在我看来,即使它不捕获,也可能会发生文件访问冲突。请相信我,不要尝试这样做。好吗?无论如何,不要将我这里所说的与使用 DotNetNuke 作为引用混淆。实际上,您必须使用 DotNetNuke 作为引用,才能允许您的控件继承自
DotNetNuke.PortalModuleControl
(DNN2) 或DotNetNuke.Entities.Modules.PortalModuleBase
(DNN3)。但是,您必须将模块放在自己的命名空间中,这样您就不会将模块与 DotNetNuke 核心一起编译。 - 数据库所有者是 dbo。我的数据库经验足以危险(难道你不喜欢陈词滥调吗),所以请在这方面多包涵我。而且,如果您看到明显的错误,请告诉我我遗漏了什么,以便我可以修复它。当我创建数据库对象时,我确保它们都以“[dbo].”开头限定。据我了解,“[dbo].”是当前上下文中数据库所有者的别名。我相信这是一种通用的方式,只是说所有者,而不是在需要使用脚本在另一个数据库中创建数据库结构时努力管理用户。在应用程序中,当生成 SQL 脚本时,它将“[dbo].”的所有实例替换为“{databaseOwner}”。{databaseOwner}是 DNN 模块安装程序用于使用正确所有者安装数据库脚本的令牌。如果在该过程中您的数据库所有者显示为 dbo 以外的其他内容,则在尝试安装 PA 时脚本将失败,因为数据库所有者前缀将不会被 {databaseOwner} 令牌取代。
- 您正在使用 Visual Studio .NET、SQL Server、IIS/ASP.NET 开发环境。虽然打包器可能不需要完全相同的设置,但是,如果您没有运行 IIS,它将绝对无法工作,并且如果 SQLDMO 不可用,脚本将无法工作。我为生成清单文件提供了一些 Microsoft Access® 支持,但 Access 不支持数据库脚本。
我通过目录服务访问 IIS Metabase,因此它必须可用。我为 SQLDMO 生成了一个主互操作程序集 (PIA),以便可以通过托管代码访问它。此 PIA 包含在项目中和安装程序中,但是,除非您的机器上实际安装了 SQLDMO,否则它将无法工作。
- 您正在使用 DotNetNuke 2.1.2 或更高版本。模块打包器已在 DNN 2.1.2、3.0 和 3.1 上进行过测试。如果您在使用其中任何一个时遇到问题,请告诉我。
PA 生成过程
我将详细讨论每个步骤的相关部分及代码,但是,现在在高层次上解释一下我是如何考虑这个过程以及每个步骤是什么,会有所帮助。当我考虑这个问题时,我觉得我想要一种方法,可以告诉应用程序在哪里找到一些信息,然后让它处理剩下的部分。对我来说,逻辑上的起点是允许用户选择自定义模块所在的 Web 应用程序(又名虚拟目录)。以下是基本流程:
- 从元数据库中找到的 Web 应用程序列表中选择一个 Web 应用程序(虚拟目录)。
- 从所选 Web 应用程序指向的本地文件系统路径中找到的 Web.config 文件中获取连接字符串(否则失败)。
- 使用连接字符串,获取所选 Web 应用程序中所有模块定义的列表(必须能够找到 DesktopModules 表,否则失败)。
- 从 Web 应用程序 bin 目录中找到的程序集列表中手动选择模块使用的程序集(还有一个自动程序集依赖检测机制,但手动路径确保您得到您想要的)。
- 指定一个前缀,用于搜索模块使用的数据库对象(例如:MyCompany_)。
- 指定一个 ZIP 文件名,将所有文件放入其中。
- 指定是否删除所有已放入临时目录用于压缩的文件,以及系统是否应尝试自动检测在步骤 4 手动选择程序集时遗漏的任何程序集依赖项。您可能不希望删除临时文件,因为有时,根据您的模块,会有您希望手动添加而打包器未识别的文件。
- 坐下来看好戏。
一旦流程完成,如果您指定希望应用程序保留临时文件,您将在指定 ZIP 文件输出的目录中找到一个 ZIP 文件,以及一个包含所有松散文件的以模块名称命名的目录。此外,如果您指定希望应用程序自动检测您可能遗漏的任何程序集依赖项,如果它找到了任何依赖项,您将在最后一页看到它找到的内容报告。
Web 应用程序列表(元数据库)
(致所有尼尔·斯蒂芬森的粉丝,我一直想把这东西叫做元宇宙 ;-)为了获取所有可用 Web 应用程序的列表,最简单的方法似乎是访问元数据库。简而言之,微软网站上写道,“元数据库是用于配置 IIS 的配置信息和架构的分层存储”。无论如何,元数据库是一个基于 XML 的配置文件(和架构)。有关更多信息,请查看 上述定义来源的网站。通过快速的互联网搜索,我能够找到一些关于如何以编程方式访问元数据库的基本信息。在网上看到一些示例并使用 MetaEdit 实用程序(立即下载以更清楚地理解)进行试验后,该实用程序允许我查看数据架构层次结构,我就拥有了编写以下代码所需的一切:
public Hashtable GetSitePaths()
{
Hashtable tmp = new Hashtable();
DirectoryEntry root = new DirectoryEntry("IIS:///w3svc/1/root");
foreach( DirectoryEntry e in root.Children )
{
tmp[ e.Name ] = e.Path;
}
return tmp;
}
我使用 System.DirectoryServices
命名空间来获取服务器根目录的所有目录条目列表。请注意行 DirectoryEntry root = new DirectoryEntry("IIS:///w3svc/1/root");
。该路径为我们提供了所有本地 Web 应用程序(虚拟目录)的列表。如果您查看 MetaEdit 实用程序,您可以图形化地看到这一点
当 GetSitePaths
方法返回时,哈希表会填充虚拟目录名称及其相应的虚拟目录路径列表。该哈希表用于填充组合框,并在选择完成后查找虚拟目录的路径。
关于使用哈希表的注意事项:这对于您作为程序员来说可能是一个常见的做法,但我将为那些不一定使用此技术的其他人稍微解释一下。我将哈希表用于我的许多数据集合,原因如下(一个或两个):
- 哈希表提供了一种实现 MVC(模型-视图-控制器)设计的简单方法。哈希表是模型。在我的应用程序中,视图是组合框,控制器是向导应用程序。由于我用 Web 应用程序的名称填充组合框,我也可以将该名称用作哈希表中的键,这样当我选择了所需的应用程序时,通过使用
(string)this.sites[ this.cmbVirtualDirectories.Text ]
查找该键的值就非常简单了,其中sites
是包含我的 Web 应用程序/虚拟目录的哈希表,而cmbVirtualDirectories.Text
是我的组合框中当前选定的值。 - 哈希表默认只允许唯一的键。这意味着您不能对一个键拥有两个相同的值。当您需要一个不同值的列表并且不想实际遍历集合以查看您是否已经找到了当前值时,这通常很方便。您只需将您拥有的值设置为键,并使用您想要的任何值作为值。如果键已经存在,它只会用相同的值覆盖。如果不存在,它就会被添加。
模块定义选择
当我们选择一个Web应用程序时,会触发 SelectedIndexChange
事件。此时,我能够将与该选择关联的虚拟路径转换为本地文件系统路径,并简单地找到该应用程序的 Web.config 文件。一旦我有了 Web.config 文件,我就可以提取连接字符串。我将该连接字符串用于流程中剩余步骤中的任何数据库用途。以下是我用来获取连接字符串的代码:
private void cmbVirtualDirectories_SelectedIndexChanged(object
sender, System.EventArgs e)
{
// Make sure our combo box has a valid selection
if( this.cmbVirtualDirectories.Text.Length > 0 )
{
// Translate the web path into a local filesystem path
sitePath = new VirtualDirectoryUtility().GetVirtualDirLocalPath(
(string)this.sites[ this.cmbVirtualDirectories.Text ] ) + "\\";
if( sitePath.Length > 0 )
{
// Check to see if a web.config file exists
if( !File.Exists( sitePath + "Web.config" ) )
{
// If not then fail and let user know
// they need to make a different selection
MessageBox.Show( this,
"The web directory you selected has no Web.config file. " +
"Please select a different web directory",
"Invalid Web Directory",
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation );
return;
}
// Load the web config into an XML document
XmlDocument config = new XmlDocument();
config.Load( sitePath + "Web.config" );
// First check to see whether we're using DNN 2 or 3
XmlNode siteSqlServer =
config["configuration"]["appSettings"].SelectSingleNode(
"add[@key = \"SiteSqlServer\"]" );
if( siteSqlServer != null )
this.version = DNN_VERSION.DNN3;
// Find out which data provider type we are using
XmlNode dataNode = config["configuration"]["dotnetnuke"]["data"];
// If both of these are null, then we're not
// finding what we need to continue --
// must go back and try again
if( dataNode == null && siteSqlServer == null )
{
// this node is required. If it's not there,
// then we need to error out.
MessageBox.Show( this,
"The Web.config file found does" +
" have the proper XML format. " +
"Please check to make sure that you" +
" are using a valid DotNetNuke 2 installation. " +
"Please select a different web directory",
"Invalid Web.config File",
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation );
return;
}
if( dataNode != null && siteSqlServer == null )
this.version = DNN_VERSION.DNN2;
this.dataProviderType =
dataNode.Attributes["defaultProvider"].Value;
// Find the correct key field
switch( this.version )
{
case DNN_VERSION.DNN2:
XmlNode providerNode =
config["configuration"]["dotnetnuke"]["data"]
["providers"].SelectSingleNode( "add[@name=\"" +
dataProviderType + "\"]" );
// Set the connection string instance
// variable for use throughout
// the rest of the process and then break out of the loop.
if( dataProviderType.Equals( "SqlDataProvider" ) )
{
connectionString =
providerNode.Attributes["connectionString"].Value;
}
else
{
// Have to attach the datasource to the
.. connection string manually or we won't be
// able to connect to it. We assume that is is in the
// (root)/Providers/DataProviders/AccessDataProvider directory
string fileName =
providerNode.Attributes["databaseFilename"].Value;
connectionString =
providerNode.Attributes["connectionString"].Value +
"Data Source=" +
sitePath +
@"Providers\DataProviders\AccessDataProvider\" +
fileName;
}
break;
case DNN_VERSION.DNN3:
connectionString = siteSqlServer.Attributes["value"].Value;
break;
}
// Enable the next button now that we have a valid selection.
this.wizardMain.NextEnabled = true;
}
}
}
在行 sitePath = new VirtualDirectoryUtility().GetVirtualDirLocalPath( (string)this.sites[ this.cmbVirtualDirectories.Text ] ) + "\\";
中,我正在调用一个实用程序,为我提供虚拟目录路径到实际文件系统路径的转换。为了更好地理解这一点,例如,假设我在本地系统上选择了一个名为 DNNSB 的 DotNetNuke 安装。由我的 GetSitePaths()
方法(见上文)加载的与 DNNSB 虚拟目录关联的虚拟路径将是“IIS:///w3svc/1/root/DNNSB”。现在,将虚拟路径转换为文件系统路径的实用程序函数代码如下所示:
public string GetVirtualDirLocalPath( string directoryEntry )
{
DirectoryEntry virtualPath = new DirectoryEntry( directoryEntry );
return virtualPath.Properties["Path"].Value.ToString();
}
这只是打开“IIS:///w3svc/1/root/DNNSB”目录条目,并查找“Path
”属性。“Path
”包含文件系统目录的完整路径。
注意:如果您想知道加载虚拟目录时 DirectoryEntry.Properties
集合中还有哪些其他属性可用,请使用 MetaEdit 实用程序打开元数据库,并查看 Schema 节点下的属性。请记住,MetaEdit 实用程序只是一个简单的工具,它提供元数据库 XML 和架构文件的树形视图。如果您不熟悉 XML,请记住架构是数据的定义,您在 LM 节点下看到的是数据的实际实现。在 MetaEdit 实用程序中,深入到 LM 节点下,“IIS:///w3svc/1/root”下的特定虚拟目录,您将看到所选虚拟目录上实际实现了哪些属性.
知道了文件系统路径后,我只需将“Web.config”附加到该路径并将其加载到 XML 文档中。然后我可以遍历 XML 节点,直到找到 DNN2 或 DNN3 的相应令牌。请注意,该方法支持获取 SQL Server 和 Microsoft Access 的连接字符串。一旦我有了有效的连接字符串,我就可以获取数据库中找到的模块定义列表。执行此操作的代码如下:
private void LoadModuleDefs()
{
try
{
// Ensure that we have a valid connection string
if( connectionString.Length <= 0 )
{
// If not, warn the user and go back to the
// web application selection page in order
// to select a valid application.
MessageBox.Show( this, "The connection string was not found while" +
" parsing the web.config file for the current web" +
" application. Please go back and select" +
" a different web application.", "Bad Connection String",
MessageBoxButtons.OK, MessageBoxIcon.Error );
this.wizardMain.BackTo( this.wpSiteSelect );
return;
}
Cursor.Current = Cursors.WaitCursor;
// Connection to Sql Server
if( this.dataProviderType.Equals( "SqlDataProvider" ) )
{
// Connect to the DB
SqlConnection connection = new SqlConnection( connectionString );
connection.Open();
// Obtain all Desktop Module definitions in the selected DB
SqlCommand command = new SqlCommand( "SELECT DesktopModuleID,
FriendlyName FROM DesktopModules", connection );
SqlDataReader reader = command.ExecuteReader();
// Instantiate the modules instance variable
modules = new Hashtable();
while( reader.Read() )
{
// Use the "FriendlyName" as the key and
// the "DesktopModuleID" as the value when
// adding to our modules hashtable instance variable
modules[reader[1].ToString()] = reader[0].ToString();
// Add the "FriendlyName" to the list of module definitions
this.cmbModuleDefinitions.Items.Add( reader[1].ToString() );
}
reader.Close();
connection.Close();
}
// Connection to Access
else
{
// Connect to the DB
OleDbConnection connection = new OleDbConnection( connectionString );
connection.Open();
// Obtain all Desktop Module definitions in the selected DB
OleDbCommand command = new OleDbCommand( "SELECT DesktopModuleID,
FriendlyName FROM DotNetNuke_DesktopModules", connection);
OleDbDataReader reader = command.ExecuteReader();
// Instantiate the modules instance variable
modules = new Hashtable();
while(reader.Read())
{
// Use the "FriendlyName" as the key and
// the "DesktopModuleID" as the value when
// adding to our modules hashtable instance variable
modules[reader[1].ToString()] = reader[0].ToString();
// Add the "FriendlyName" to the list of module definitions
this.cmbModuleDefinitions.Items.Add( reader[1].ToString() );
}
reader.Close();
connection.Close();
}
Cursor.Current = Cursors.Default;
}
catch ( Exception ex )
{
// Something went wrong. We should notify the user and the go back to the
// web application selection page.
MessageBox.Show( this, ex.Message, "Exception Caught" );
this.wizardMain.BackTo( this.wpSiteSelect );
return;
}
}
请再次注意,我同时支持 SQL Server 和 Microsoft Access。现在模块定义已加载到组合框中,用户可以选择他们想要打包的模块,然后单击“下一步”。
选择程序集
我曾考虑过只自动检测模块依赖项,但在我看来,充其量这样做可能只会稍微提高效率,而最坏的情况(例如,如果它在检测时不准确),您无论如何都必须手动选择您的程序集。我选择的路径是让用户能够从站点 bin 目录中找到的程序集列表中手动选择模块使用的程序集。然后,在后续过程中,我让用户能够允许自动检测功能尝试查找可能遗漏的任何内容。(我将在后面更深入地讨论自动检测功能——事实证明,自动检测效果很好。)列表框会填充虚拟目录(我只是将“\bin\”附加到元数据库中找到的路径来查找它)的 bin 目录中找到的所有程序集。列表框允许多选。用户只需选择与其模块关联的程序集,然后单击“下一步”。
数据库脚本
下一步允许用户提供一个前缀,该前缀用于定位与正在打包的模块相关的数据库对象。
请阅读本文开头的“假设”部分,以便您了解在使用前缀创建数据库对象时会遇到什么情况。这部分只是使用 SQLDMO 根据提供的前缀脚本化它找到的对象。我创建了另一个实用程序类来完成此操作。您将在下面找到代码。请记住,现在展示这部分,可能看起来在向导过程的这一点上运行此代码。实际上,处理直到您到达向导末尾的进度屏幕才会开始。我在此阶段只收集前缀,但是,这似乎是文章中讨论一旦处理实际开始后应用程序将如何处理此前缀的好地方。这是代码:
public static string GetScript(string hostName, string dbName,
string username, string password,
string prefix )
{
// Instantiate the Sql Server Object
SQLDMO.SQLServer srv = new SQLDMO.SQLServer();
// If there is no username provided, we assume that we're going to use
// a trusted connection
if( username.Length <= 0 )
{
srv.LoginSecure = true;
srv.Connect( hostName, "", "" );
}
// Otherwise we log in with the specified credentials
else
srv.Connect(hostName, username, password );
// We have to set some scripting parameters before we start
SQLDMO.SQLDMO_SCRIPT_TYPE param = SQLDMO_SCRIPT_TYPE.SQLDMOScript_Default|
// Script out indexes
SQLDMO.SQLDMO_SCRIPT_TYPE.SQLDMOScript_Indexes |
// Script out drop statements
SQLDMO_SCRIPT_TYPE.SQLDMOScript_Drops |
// Prefix the object name with the database owner
SQLDMO_SCRIPT_TYPE.SQLDMOScript_OwnerQualify;
string script = "";
foreach(SQLDMO.Database db in srv.Databases)
{
// Have to iterate through the list of databases to find the one that
// was specified
if(db.Name!=null && db.Name.ToLower().Equals(dbName.ToLower()) )
{
// First search through all of the tables and locate the ones
// with the prefix provided (case insensitive)
foreach( SQLDMO.Table table in db.Tables )
{
if( table.Name.ToLower().StartsWith( prefix.ToLower() ) )
{
// Append the script for the current table
script += table.Script( param, null, null,
SQLDMO.SQLDMO_SCRIPT2_TYPE.SQLDMOScript2_Default );
}
}
// Next search through all of the stored procedures and locate
// the ones with the prefix provided (case insensitive)
foreach( SQLDMO.StoredProcedure proc in db.StoredProcedures )
{
if( proc.Name.ToLower().StartsWith( prefix.ToLower() ) )
{
// Append the script for the current stored procedure
script += proc.Script( param, null,
SQLDMO.SQLDMO_SCRIPT2_TYPE.SQLDMOScript2_Default );
}
}
break;
}
}
return script;
}
您可以看到,我为 SQLDMO 脚本机制指定了几个参数,包括使用默认设置、创建索引、创建 drop
语句以及使用数据库所有者名称作为对象名称的前缀。当我们从这个方法返回时,我们手上就有了完整的脚本。为了实现“开箱即用”的可用性,我们现在需要将数据库所有者前缀(必须是“[dbo].”)替换为 DotNetNuke 模块安装程序所期望的 {databaseOwner} 令牌。下面是调用代码的样子:
private void CreateSqlScript()
{
try
{
// Get the script according to the specified connection string and
// db prefix
string script = DbScriptingUtility.GetScript( connectionString,
this.dbPrefix );
// Replace the db owner with the token expected by the DNN Module
// Installer
script = script.Replace( "[dbo].", "{databaseOwner}" );
// Set the version information
string ver = ( this.manifestCreator.Version.Length > 0 ) ?
this.manifestCreator.Version : "01.00.00";
// Write out the file to the temporary directory where our other
// files are being copied.
StreamWriter writer = File.CreateText( tempDirPath + "\\" +
TEMP_DIR_NAME + "\\" + ver + ".SqlDataProvider" );
writer.Write( script );
writer.Close();
}
catch ( Exception ex )
{
MessageBox.Show( this, ex.Message, "Exception Caught" );
}
}
目前,您可以忽略我们使用 manifestCreator
的代码。我们将在下一节中更深入地讨论它。只需满足于,一旦这些方法运行,我们将拥有一个数据库脚本,DNN 模块安装程序可以使用它来在通过 DNN 文件管理器安装私有程序集时创建我们的对象。
清单创建器
清单创建器确实是 DNN 模块打包器的核心和灵魂。它根据我们提供的信息生成我们的 .dnn 清单文件。它首先从数据库中的 DesktopModules 表获取模块信息。从那里,我们获取模块名称、描述和版本。接下来,我们通过连接 ModuleDefinitions 和 ModuleControls 表来选择模块控件数据。一旦这些数据可用,我们只需将其以适当的顺序和适当的标签添加到 XML .dnn 文件中。这是 ManifestCreator
的 Generate
方法:
public XmlDocument Generate( string connectionString,
string desktopModuleId, string dnnVersion )
{
// Set the intance variables
this.connectionString = connectionString;
this.desktopModuleId = desktopModuleId;
// connectionString and desktopModuleId cannot be null or empty
Debug.Assert( this.connectionString.Length > 0,
"ManifestCreator.connectionString must be set before generating." );
Debug.Assert( this.desktopModuleId.Length > 0,
"ManifestCreator.desktopModuleId must be set before generating." );
// Create our XML document and its top level node
doc = new XmlDocument();
XmlElement topElement = doc.CreateElement( "dotnetnuke" );
// Add some attributes to the top level node
topElement.SetAttribute( "version", dnnVersion );
topElement.SetAttribute( "type", "Module" );
doc.AppendChild( topElement );
// Create the folders tag
XmlElement folders = doc.CreateElement( "folders" );
topElement.AppendChild( folders );
// Create the folder tag
XmlElement folder = doc.CreateElement( "folder" );
folders.AppendChild( folder );
// Call virtual method to load the module definition
// information. Overriden child class version is called.
// See SqlServerManifestCreator or AccessManifestCreator for details.
LoadModuleDefinitionInfo();
// Create xml elements to hold the data found in the db
XmlElement name = doc.CreateElement( "name" );
XmlElement desc = doc.CreateElement( "description" );
XmlElement version = doc.CreateElement( "version" );
// Set the xml inner text to reflect the data found in the db
name.InnerText = friendlyName.Replace( " ", "_" );
desc.InnerText = description;
if( ver == null || ver.Length <= 0 )
ver = "01.00.00";
version.InnerText = ver;
// Append these nodes to the folder node
folder.AppendChild( name );
folder.AppendChild( desc );
folder.AppendChild( version );
// Create the modules node.
XmlElement modules = doc.CreateElement( "modules" );
folder.AppendChild( modules );
// Call virtual method to load the list of modules.
// Overriden child class version is called.
// See SqlServerManifestCreator or AccessManifestCreator for details.
LoadModules();
// For each of the modules we found,
// we have to create a module xml node as well
// as each of the control nodes.
Hashtable files = new Hashtable();
bool exPathWasSet = false;
foreach( string key in friendlyNames.Keys )
{
// Create the module node
XmlElement mod = doc.CreateElement( "module" );
// Create the friendlyname node for the module node
XmlElement fName = doc.CreateElement( "friendlyname" );
// Append the nodes to the document
modules.AppendChild( mod );
mod.AppendChild( fName );
// Set the friendly name equal to the key name
fName.InnerText = key;
// Created the controls node to hold all of the control nodes
XmlElement controls = doc.CreateElement( "controls" );
mod.AppendChild( controls );
foreach( object[] row in items )
{
// If this is the module we're currently working on
if( row[8].ToString().Equals( key ) )
{
// Create a control node
XmlElement control = doc.CreateElement( "control" );
controls.AppendChild( control );
// 2 - key, 3 - title, 4 - src, 5 - iconfile, 6 - controltype
// Create the control key node
if( row[2].ToString().Length > 0 )
{
XmlElement itemKey = doc.CreateElement( "key" );
itemKey.InnerText = row[2].ToString();
control.AppendChild( itemKey );
}
// Create the control title node
if( row[3].ToString().Length > 0 )
{
XmlElement itemTitle = doc.CreateElement( "title" );
itemTitle.InnerText = row[3].ToString();
control.AppendChild( itemTitle );
}
// Create the control src node
if( row[4].ToString().Length > 0 )
{
XmlElement itemSrc = doc.CreateElement( "src" );
string source = row[4].ToString();
itemSrc.InnerText =
source.Substring( source.LastIndexOf( "/" ) + 1 );
// Keep a list of all of the files found
// while iterating to keep from
// having to run through the dataset again.
files[source] = 1;
control.AppendChild( itemSrc );
if( !exPathWasSet )
{
this.extendedPath = source.Substring( 0,
source.LastIndexOf( "/" ) );
exPathWasSet = true;
}
}
// Create the control iconfile node
if( row[5].ToString().Length > 0 )
{
XmlElement itemIconFile = doc.CreateElement( "iconfile" );
itemIconFile.InnerText = row[5].ToString();
// Keep a list of all of the files found
// while iterating to keep from
// having to run through the dataset again.
files[row[5].ToString()] = 1;
control.AppendChild( itemIconFile );
}
// Create the control type node and set the correct type name
if( row[6].ToString().Length > 0 )
{
XmlElement itemType = doc.CreateElement( "type" );
switch( row[6].ToString() )
{
case "-2":
itemType.InnerText = "Skin Object";
break;
case "-1":
itemType.InnerText = "Anonymous";
break;
case "0":
itemType.InnerText = "View";
break;
case "1":
itemType.InnerText = "Edit";
break;
case "2":
itemType.InnerText = "Admin";
break;
case "3":
itemType.InnerText = "Host";
break;
}
control.AppendChild( itemType );
}
}
}
}
// Create the files node
xfiles = doc.CreateElement( "files" );
folder.AppendChild( xfiles );
int fileCount = 0;
// contentFiles can be accessed as a property
// once the generate method has run. This
// is where we populate it with a list of all of the files
contentFiles = new string[files.Keys.Count];
foreach( string key in files.Keys )
{
// Add the filename to the contentFiles array
contentFiles[fileCount] = key;
// Create the file node for the current file
XmlElement file = doc.CreateElement( "file" );
xfiles.AppendChild( file );
// Create a name node to be appended to the file node
XmlElement fileName = doc.CreateElement( "name" );
// We only need the filename for the manifest file,
// so we extract it from the path
string source = key.Substring( key.LastIndexOf( "/" ) + 1 );
// Set the name node's inner text
fileName.InnerText = source;
file.AppendChild( fileName );
fileCount++;
}
this.hasBeenGenerated = true;
return doc;
}
这段代码中有很多注释,试图解释到底发生了什么,所以请仔细阅读这些注释。不过,不要让代码压倒你。如果您以前逐个标签地生成过 XML 文档,那么您会认识到这里并没有什么惊天动地的事情发生。它只是获取我们从数据库中获取的数据,以及内容文件列表、程序集文件和 SQL 脚本文件,然后生成一个基于 XML 的 .dnn 清单文件。
关于模块版本的注意事项
如果您的模块未设置版本,则应用程序使用的版本将自动默认为 01.00.00。如果您不希望出现这种情况,那么您需要在运行打包器之前在数据库中更改模块的版本。模块版本可以使用企业管理器手动在 DesktopModules 表中设置。要检查是否已设置模块版本,请以主机帐户身份登录您的 DotNetNuke 站点,然后选择 主机 > 模块定义。现在单击模块定义旁边的编辑铅笔图标。您将看到类似下图的屏幕。请注意,版本字段不可编辑(这也是您必须在数据库中手动设置它的原因)。
如果您的模块中未设置版本,请进入企业管理器中的 DesktopModules 数据库表,找到您的模块记录。在版本列中以 xx.xx.xx 格式设置您的版本号。此版本用于 .dnn 文件信息和您的 SqlDataProvider SQL 文件,该文件根据找到的值命名(如 01.00.00.SqlDataProvider)。
自动依赖检测
检测模块依赖关系无疑是这个项目中最有趣的部分之一。再次,我不得不做几个假设才能使其正常工作。依赖关系检测的基本过程是通过获取所有内容文件列表并读取其头行来查找
- 当前内容文件的命名空间。
- 任何其他 Web 控件引用。
我正在寻找两行。第一行是 Control
标签。它看起来像这样:
<%@ Control Language="c#"
AutoEventWireup="false"
Codebehind="EditProduct.ascx.cs"
Inherits="SkyeRoad.ProductCatalog.EditProduct"
TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
我搜索 Inherits
标签并获取该值。我假设引用此对象的程序集名称将是 完整类命名空间减去类名。因此,在上面的示例代码中,我假设我们正在寻找的程序集(DLL)是 SkyeRoad.ProductCatalog.dll,因为完整类命名空间是 SkyeRoad.ProductCatalog.EditProduct
。
我还查找包含外部控件信息的行。它看起来像这样:
<%@ Register TagPrefix="SRSWC"
Namespace="SkyeRoad.WebControls"
Assembly="SkyeRoad.WebControls"%>
我搜索 Assembly
标签并获取其值,然后在其后附加 .dll。在这种情况下,没有必要假设程序集名称可能是什么,因为它已经清楚地为我们列出了。
我检测依赖项的第三种方法是对任何主程序集使用 Assembly.GetReferencedAssemblies()
方法。我将在内容文件的 Control
标签中找到的任何程序集称为主程序集。
我再次创建了一个实用程序类来处理查找依赖项的工作。下面是方法 FindAssemblies()
。请注意,此课程使用正则表达式来确定一行是否包含我们正在寻找的标签。
public static string[] FindAssemblies( string[] contentFiles, string sitePath )
{
// We create a hashtable to hold the names of the assemblies we find. Since
// the default behaviour of a hashtable is to store unique keys only, it
// provides a simple shortcut to ensure that we only have one of each
// assembly found. Since we're iterating through multiple content files,
// it is possible that we'll find the same assembly name twice so this
// ensures that it will only be in the final list once.
Hashtable assemblies = new Hashtable();
foreach( string contentFilepath in contentFiles )
{
StreamReader reader = File.OpenText( contentFilepath );
// FileInfo fInfo = new FileInfo( contentFilepath );
string line = "";
while( ((line = reader.ReadLine()) != null) )
{
if( line.IndexOf( "<%@" ) > -1 )
{
// If we find an Assembly key, then we have some sort of
// other dependency on the page, probably a web control of
// some sort. We will need to list this as a dependency
Regex regex = new Regex( "Assembly\\s*=\\s*\"?(?[^\"]+)\"",
RegexOptions.IgnoreCase );
Match match = regex.Match( line );
if( match.Success )
assemblies[match.Groups["assembly"].Value + ".dll"] = 0;
// The Inherits tag indicates what namespace the main assembly
// is in for this particular content file. We need to grab
// this as well
regex = new Regex( "Inherits\\s*=\\s*\"?(?<namespace>[^\"]+)\"",
RegexOptions.IgnoreCase );
match = regex.Match( line );
if( match.Success )
{
// Because the inherits tag gets us a full namespace, we
// need to leave off the name of the actual object in
// order to know the assembly name.
string nameSpace =
match.Groups["namespace"].Value.Substring( 0,
match.Groups["namespace"].Value.LastIndexOf( "." ) );
assemblies[nameSpace + ".dll"] = 1;
}
}
}
reader.Close();
ArrayList dependencies = new ArrayList();
// Iterate through our assemblies list and use reflection to
// determine any dependencies we may have missed.
foreach( string key in assemblies.Keys )
{
// If it was set to 1 then we know that it is our primary assembly
if( (int)assemblies[key] == 1 )
{
if( File.Exists( sitePath + "\\bin\\" + key ) )
{
AssemblyName[] assemblyNames = Assembly.LoadFrom(
sitePath + "\\bin\\" + key ).GetReferencedAssemblies();
foreach( AssemblyName aName in assemblyNames )
{
// Make sure they aren't framework assemblies,
// mscorlib, or the dotnetnuke assembly itself
if( !aName.Name.ToLower().StartsWith( "system" ) &&
!aName.Name.ToLower().StartsWith( "microsoft" ) &&
!aName.Name.ToLower().StartsWith( "mscorlib" ) &&
!aName.Name.ToLower().StartsWith( "dotnetnuke" ) )
{
dependencies.Add( aName.Name + ".dll" );
}
}
}
}
}
foreach( string dep in dependencies )
{
assemblies[dep] = 1;
}
}
// Convert our hashtable to an array to pass back to the caller
string[] retArray = new string[ assemblies.Count ];
int index = 0;
foreach( string key in assemblies.Keys )
{
retArray[index++] = key;
}
// Return the array
return retArray;
}
我首先打开内容文件并阅读它。如果一行包含字符串“<%@
”,那么我知道我找到了我感兴趣的行之一。接下来,我检查该行以查看它是否包含 Assembly
或 Inherits
标签。如果包含,我提取每个标签的值,并根据我在本节前面陈述的假设,从中确定我需要的程序集。然后,我将程序集添加到哈希表,如果它是主程序集,则将其值设置为 1,如果不是,则设置为 0(有关“主程序集”的定义,请参见上文)。
我做的下一件事是遍历我找到的所有程序集,如果它们被分配了 1,我使用反射通过 GetReferencedAssemblies()
调用获取依赖项列表。您会注意到我选择从检测过程中排除某些程序集。我们不需要将 .NET 框架程序集包含在我们的模块中,也不需要 MSCorLib 或 DotNetNuke 程序集。可能还有我忽略的其他程序集,但只有随着时间的推移和大量使用才能确定。
返回之前的最后一步是将内容从哈希表转换为数组。这会产生一些额外的开销,但在这种情况下,我更喜欢返回一个常规数组而不是哈希表。这更多是个人偏好,而不是什么特别之处。以这种方式做并没有什么特别的。
处理选项选择
在处理开始之前,您最后要做的是选择两个选项:
- 是否要保留临时文件。
- 是否要尝试自动检测在程序集依赖项选择页面遗漏的程序集。
结论
流程完成后,将为您提供有关最终结果的信息。您将被告知:
- 打包 (.zip) 文件的位置。
- 临时文件是否完好(即,您未选中“删除临时文件,只保留 zip 包”复选框)以及松散文件的位置。
- 在选择程序集时,是否有任何您遗漏的依赖项被检测到。
DNN 模块打包器对我来说是一个非常有学习价值的练习。我确信我还需要添加更多内容才能使其完美无瑕,但是,与此同时,它提供了一种简单快捷的方法,可以从您的开发环境中为您的 DotNetNuke 自定义模块生成私有程序集。
历史
- 2005年10月18日 - 修复脚本实用程序以 UTF-8 编码输出文件。DNN 在解析 SQL 脚本时需要 UTF-8 编码。
- 2005年10月12日 - 添加了对 DNN 3.1.1 的支持。DNN 3.1.1 中数据库对象命名结构发生了变化,因此这解释了前缀“dnn_”。
- 2005年9月7日 - 添加了对 DotNetNuke 3.0 及更高版本模块的支持。还支持获取自定义模块 App_LocalResources 目录中找到的 resx 文件。
- 2004年12月23日 - 修复了一些问题(感谢 Jerry!)。
- 以前使用的是 DNN 1 中的旧
connectionString
值。已更正为使用DataProvider
部分。 - 增加了对 MS Access 的支持(不包括脚本功能)。
- 使数据库前缀的使用不区分大小写。
- 修复了用于 SQLDMO 的连接字符串解析问题。现在使用正确的密码键(“
pwd
”)。
- 以前使用的是 DNN 1 中的旧
- 2004年12月21日 - 首次发布。