在开发需要在客户端服务器上部署SQL Server数据库的应用程序时,可能会遇到一些挑战。虽然Visual Studio .NET的安装项目提供了一些帮助,但它们可能不足以满足所有需求。此外,使用如InstallShield这样的商业产品虽然方便,但会增加成本。因此,寻找一个免费的解决方案是很有必要的。
在MSDN上,发现了一篇关于使用Installer类和自定义动作来实现这一功能的文章。原始代码是用VB.NET编写的,但本文将其移植到C#,并添加了一些发现有用的新特性。
首先,需要创建一个从System.Configuration.Install派生的类,并在解决方案中添加两个名为install.txt和uninstall.txt的嵌入式资源。install.txt将包含数据库的SQL脚本,而uninstall.txt将包含删除脚本。
对于数据库脚本,使用了Microsoft团队为ASP.NET InSQL会话状态制作的ASPstate脚本:
<sessionState mode="SQLServer" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=aleph;User ID=ASPsession;Password=ASPsession;" cookieless="false" timeout="60" />
为了使Web应用程序能够在客户端服务器上工作,需要创建一个包含应用程序和SQL脚本的MSI。为了在安装时运行脚本,创建了一个名为ScriptInstall的.dll,其代码如下:
首先,声明一个string,它将有一个默认值,并且可以通过Install方法被覆盖:
string conStr = "packet size=4096;integrated security=SSPI;" + "data source=\"(local)\";persist security info=False;" + "initial catalog=master";
使用两个函数来返回连接字符串和脚本内容:
private static string GetScript(string name)
{
Assembly asm = Assembly.GetExecutingAssembly();
Stream str = asm.GetManifestResourceStream(asm.GetName().Name + "." + name);
StreamReader reader = new StreamReader(str);
return reader.ReadToEnd();
}
private static string GetLogin(string databaseServer, string userName, string userPass, string database)
{
return "server=" + databaseServer + ";database=" + database + ";User ID=" + userName + ";Password=" + userPass;
}
然后,使用两个函数来在SQL服务器上运行install.txt和uninstall.txt。ExecuteSQL有一个正则表达式,它在GO之后分割脚本,以便可以使用SQLCommand逐个执行,因为如果SQL脚本包含"GO",ADO.NET会抛出异常。
private static void ExecuteSql(SqlConnection sqlCon)
{
string[] SqlLine;
Regex regex = new Regex("^GO", RegexOptions.IgnoreCase | RegexOptions.Multiline);
string txtSQL = GetScript("install.txt");
SqlLine = regex.Split(txtSQL);
SqlCommand cmd = sqlCon.CreateCommand();
cmd.Connection = sqlCon;
foreach (string line in SqlLine)
{
if (line.Length > 0)
{
cmd.CommandText = line;
cmd.CommandType = CommandType.Text;
try
{
cmd.ExecuteNonQuery();
}
catch (SqlException)
{
// rollback
ExecuteDrop(sqlCon);
break;
}
}
}
}
private static void ExecuteDrop(SqlConnection sqlCon)
{
if (sqlCon.State != ConnectionState.Closed) sqlCon.Close();
sqlCon.Open();
SqlCommand cmd = sqlCon.CreateCommand();
cmd.Connection = sqlCon;
cmd.CommandText = GetScript("uninstall.txt");
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();
sqlCon.Close();
}
现在有了这些函数,可以覆盖Install(IDictionary stateSaver)和Uninstall(IDictionary savedState)。在Install方法中,除了在服务器上运行SQL脚本外,还保存了用户提交的连接数据。保存明文连接字符串是危险的,所以使用RijndaelManaged来加密它。可以在源代码中找到这个类。保存连接字符串是因为需要在卸载时删除数据库ASPstate。
public override void Install(IDictionary stateSaver)
{
base.Install(stateSaver);
if (Context.Parameters["databaseServer"].Length > 0 && Context.Parameters["userName"].Length > 0 && Context.Parameters["userPass"].Length > 0)
{
conStr = GetLogin(Context.Parameters["databaseServer"], Context.Parameters["userName"], Context.Parameters["userPass"], "master");
RijndaelCryptography rijndael = new RijndaelCryptography();
rijndael.GenKey();
rijndael.Encrypt(conStr);
// save information in the state-saver IDictionary to be used in the Uninstall method
stateSaver.Add("key", rijndael.Key);
stateSaver.Add("IV", rijndael.IV);
stateSaver.Add("conStr", rijndael.Encrypted);
}
SqlConnection sqlCon = new SqlConnection(conStr);
sqlCon.Open();
ExecuteSql(sqlCon);
if (sqlCon.State != ConnectionState.Closed) sqlCon.Close();
}
在Uninstall方法中,如果savedState包含conStr,使用RijndaelCryptography来解密它:
public override void Uninstall(IDictionary savedState)
{
base.Uninstall(savedState);
if (savedState.Contains("conStr"))
{
RijndaelCryptography rijndael = new RijndaelCryptography();
rijndael.Key = (byte[])savedState["key"];
rijndael.IV = (byte[])savedState["IV"];
conStr = rijndael.Decrypt((byte[])savedState["conStr"]);
}
SqlConnection sqlCon = new SqlConnection(conStr);
ExecuteDrop(sqlCon);
}
现在安装器类已经完成,可以创建一个安装项目并添加主要输出。在用户界面编辑器中,选择安装下的起始节点。在操作菜单中,选择添加对话框。在添加对话框对话框中,选择文本框(A)对话框,然后点击确定关闭对话框。在操作菜单中,选择向上移动。重复直到文本框(A)对话框位于安装文件夹节点之上。编辑文本框(A)表单的属性如下:
转到自定义动作编辑器,并将主要输出添加到安装和卸载节点。点击安装节点上的主要输出并编辑其属性。在CustomActionData中输入以下内容:
/databaseServer=[EDITA1] /userName=[EDITA2] /userPass=[EDITA3]
在安装器类中使用Context.Parameters获取文本框的值:
conStr = GetLogin(Context.Parameters["databaseServer"], Context.Parameters["userName"], Context.Parameters["userPass"], "master");