简单的数据库查看器 - DBViewer
一个简单的数据库查看器,用于操作 SQL Server 数据类型(特别是:image、binary、varbinary 和 text)。
引言
本文介绍了一个简单的数据库查看器,该查看器可以操作 SQL Server 数据库数据。它允许以简单熟悉的方式复制一行到另一行,即使该行包含一些二进制数据。当您尝试使用现有有效记录生成数据库中的一些示例数据时,这尤其方便。为了举例说明,本文将 DB 中的 image 列视为真正的图像(而不是任何 BLOB
),从而允许将图像加载/查看到/从数据库中的特定记录。
背景
在数据库中生成示例数据时,需要更容易地操作图像、二进制数据、ntext 和 text 数据类型,并在执行 T-SQL 时对其进行操作。以下文章介绍了一个简单的数据库查看器,应该可以完成此任务。
DBViewer 描述
DBViewer 应用程序由两部分组成。第一部分是应用程序注册的 SQL Server 和数据库的树状视图(使用鼠标右键上下文菜单注册 SQL Server)。第二部分是自定义的 DataGrid
,表示数据库的表。如果表中的某一行包含二进制信息,我们的自定义 DataGrid
将其表示为二进制字符串,而不是二进制类型的普通默认值:“Byte[]
”。此外,如果某一行包含图像,我们的 DataGrid
将显示该图像的小缩略图,允许双击单元格以查看/加载其他图像。
请参阅随源代码提供的类图。
代码概述
应用程序有四个层:DAL、BL、UI 和 Common 层。
Common - Common 层
Common 层包含 DBViewer 应用程序的一些常量定义。
它还包含一个简单的日志操作实现,该实现使用 .NET 框架的命名空间进行调试和跟踪:System.Diagnostics
。
该框架在 System.Diagnostics
命名空间中提供了 Trace
类,该类可以满足我们简单的日志操作需求,并允许我们将一个侦听器添加到 Trace
类的 Listeners
集合中。将侦听器添加到 Listeners
集合后,我们可以使用 Trace.WriteLine
方法编写任何消息,该方法会循环遍历 Listeners
集合中的侦听器,并根据侦听器的类型编写所需的消息。
我们将一个 TextWriterTraceListener
(它将其输出重定向到 TextWriter
类的实例或任何 Stream
类)添加到 Trace.Listeners
集合中,如下所示:
/// <summary>
/// Log - logs the errors in the application.
/// </summary>
public sealed class Log
{
/// <summary>
/// Log file name.
/// </summary>
/// <remarks> DBViewerConstants.ApplicationPath
/// - just returns the current application's path </remarks>
private static readonly string LogFileName =
DBViewerConstants.ApplicationPath + "\\log.txt";
...
/// <summary>
/// Constructor.
/// </summary>
static Log()
{
try
{
// creates the log or appends the text at the end.
StreamWriter log;
if(File.Exists(LogFileName))
log = File.AppendText(LogFileName);
else
log = File.CreateText(LogFileName);
Trace.Listeners.Add(new TextWriterTraceListener(log));
}
catch
{
//writing to log shouldn't raise an exception.
}
}
...
}
为了在 DBViewer 应用程序的日志中输出错误,使用以下方法(该方法查找调用方法的名称,以便将其写入日志)
/// <summary>
/// Writes the error to the log.
/// </summary>
/// <param name="message">error message</param>
public static void WriteErrorToLog(string message)
{
try
{
// variables.
int index = 1;
// current class name
string cls;
string method;
StackFrame frame;
// gets the info of the calling method.
StackTrace stack = new StackTrace();
// while the class's name is the Log
// continue to extract the callers from the stack.
do
{
frame = stack.GetFrame(index++);
cls = frame.GetMethod().ReflectedType.FullName;
}
while(cls == Log.LogInstanceName);
// gets the caller method's name.
method = frame.GetMethod().Name;
// constructs the message.
StringBuilder logMessage = new StringBuilder(LogMessageLength);
logMessage.Append(DateTime.Now.ToShortDateString());
logMessage.Append("-");
logMessage.Append(DateTime.Now.ToLongTimeString());
logMessage.Append(":: ");
logMessage.Append(cls);
logMessage.Append(".");
logMessage.Append(method);
logMessage.Append("()- ");
logMessage.Append(message);
// writes the message to the log.
Trace.WriteLine(logMessage.ToString(), TraceLevel.Error.ToString());
}
catch
{
//writing to log shouldn't raise an exception.
}
}
尽管消息已发送到其侦听器,但直到调用 Trace.Flush
方法后才会被写入。我宁愿配置 .config 文件自动刷新跟踪,而不是调用 Flush
方法。
操作方法如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- automatically writes the trace messages -->
<system.diagnostics>
<trace autoflush="true" indentsize="3" />
</system.diagnostics>
</configuration>
DAL - 数据访问层
该层中的主要类是 DBViewerDAL
,它负责连接到 SQL Server 以检索/更新数据。
通过 SqlConnection
和 SqlCommand
分别连接到数据库和操作数据,它们被定义为上述类的成员。
/// <summary>
/// SQL Connection.
/// </summary>
private SqlConnection connection;
/// <summary>
/// SQL Command.
/// </summary>
private SqlCommand command;
通过以下方法构造 SQL 连接字符串(请注意,该方法使用 StringBuilder
对象来提高字符串连接的性能)。
/// <summary>
/// Connects to the Database using the following parameters.
/// </summary>
/// <param name="server">server</param>
/// <param name="database">database</param>
/// <param name="user">user</param>
/// <param name="password">password</param>
private void ConnectToDatabase(string server, string database,
string user, string password)
{
try
{
StringBuilder sqlConnectionStr =
new StringBuilder(ConnectionStringLength);
// sets the server.
sqlConnectionStr.Append("Server=");
sqlConnectionStr.Append(server);
sqlConnectionStr.Append("; ");
// sets the database.
if(database.Length != 0)
{
sqlConnectionStr.Append("DataBase=");
sqlConnectionStr.Append(database);
sqlConnectionStr.Append("; ");
}
/* sets the user name and the password.
* (the password isn't required,
* but if the name exists then the user
* tries to authenticate throught sql authentication)
**/
if(user.Length != 0)
{
sqlConnectionStr.Append("User Id=");
sqlConnectionStr.Append(user);
sqlConnectionStr.Append("; Password=");
sqlConnectionStr.Append(password);
sqlConnectionStr.Append(";");
}
else
{
sqlConnectionStr.Append("Integrated Security=SSPI;");
}
connection = new SqlConnection(sqlConnectionStr.ToString());
command = new SqlCommand();
}
catch(Exception e)
{
Log.WriteErrorToLog(e.Message);
throw;
}
}
该应用程序应反映给定 SQL Server 中所有可用的数据库及其数据表和数据。
将在各层之间传递的基本数据单元是 DataTable
。由于某些类型(例如图像和二进制数据)将以“特殊”方式引用,以便稍后轻松重新格式化,因此结果 DataTable
将使用 SqlDataReader
手动构造,而不包含任何架构。
通过以下方法提取数据库中的数据。
/// <summary>
/// Gets the data from the database according to the user's query.
/// </summary>
/// <param name="query">query to extract the data from the database.</param>
/// <returns>Queried data in DataTable</returns>
public DataTable GetData(string query)
{
try
{
// opens connection.
command.CommandType = CommandType.Text;
command.CommandText = query;
command.Connection = connection;
connection.Open();
// executes the query.
SqlDataReader reader = command.ExecuteReader();
DataTable dataTable = ConstructData(reader);
// closes connection.
reader.Close();
return dataTable;
}
catch(Exception e)
{
Log.WriteErrorToLog(e.Message);
throw;
}
finally
{
connection.Close();
}
}
/// <summary>
/// Constructs the data which was extracted
/// from the database according to user's query.
/// </summary>
/// <param name="reader">SqlReader - holds the queried data.</param>
///<returns>Queried data in DataTable.</returns>
private static DataTable ConstructData(SqlDataReader reader)
{
try
{
if(reader.IsClosed)
throw new
InvalidOperationException("Attempt to" +
" use a closed SqlDataReader");
DataTable dataTable = new DataTable();
// constructs the columns data.
for(int i=0; i<reader.FieldCount; i++)
dataTable.Columns.Add(reader.GetName(i),
reader.GetFieldType(i));
// constructs the table's data.
while(reader.Read())
{
object[] row = new object[reader.FieldCount];
reader.GetValues(row);
dataTable.Rows.Add(row);
}
// Culture info.
dataTable.Locale = CultureInfo.InvariantCulture;
// Accepts changes.
dataTable.AcceptChanges();
return dataTable;
}
catch(Exception e)
{
Log.WriteErrorToLog(e.Message);
throw;
}
}
为了更新数据库中的表(即删除一行、添加一行或更改行的值),应构造适当的命令。由于要更新的 DataTable
没有架构,因此使用 SqlCommandBuilder
来动态构造 Delete、Update 和 Insert 命令。
(现在,SqlCommandBuilder
不是首选类,我们在示例中仅为此目的使用它,因为它是动态生成 SqlCommand
s 的最简单方法。在开发商业产品时,请尽量避免使用此类,因为它确实会影响性能。)
通过使用事务来避免数据库中的不一致。
(请注意,DBViewer 仅在具有主键的 DataTable
上工作。)
/// <summary>
/// Saves the table to the DB.
/// </summary>
/// <param name="table">database table.</param>
public void Save(DataTable table)
{
try
{
// prepares select command.
string query = "SELECT * FROM " + table.TableName;
command.CommandType = CommandType.Text;
command.CommandText = query;
command.Connection = connection;
// opens connection.
connection.Open();
// gets transaction context.
SqlTransaction transaction =
connection.BeginTransaction(IsolationLevel.RepeatableRead);
command.Transaction = transaction;
// sets the SqlCommandBuilder
// that constructs update, delete, insert commands.
SqlDataAdapter dataAdapter = new SqlDataAdapter();
dataAdapter.SelectCommand = command;
SqlCommandBuilder commandBuilder =
new SqlCommandBuilder(dataAdapter);
try
{
DataTable changes;
// The specific order of this execution is very important.
// Consider the case that the user
// first deletes the row with primary key X,
// then adds a new row with primary key X
// - by executing the update in the following order it won't fail.
changes = table.GetChanges(DataRowState.Deleted);
if(changes != null)
dataAdapter.Update(changes);
changes = table.GetChanges(DataRowState.Modified);
if(changes != null)
dataAdapter.Update(changes);
changes = table.GetChanges(DataRowState.Added);
if(changes != null)
dataAdapter.Update(changes);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
catch(Exception e)
{
Log.WriteErrorToLog(e.Message);
throw;
}
}
BL - 业务逻辑层
业务逻辑负责从数据库检索数据、更新数据库中的数据以及缓存用户凭据/数据,以便更好地与 SQL Server 配合使用。为简洁起见,我将在此不讨论缓存,请参阅源代码以获取更多信息。主要兴趣点在于如何检索 SQL Server 中有关数据库及其表的信息。
- 为了获取数据库(目录),应在 SQL Server 2000 中执行以下语句。
SELECT CATALOG_NAME FROM INFORMATION_SCHEMA.SCHEMATA
- 为了获取给定数据库中的表,应在 SQL Server 2000 中执行以下语句。
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
例如,为了检索已注册的 SQL Server 中的数据库并将其显示给用户,正在执行以下方法。
...
private const string DatabasesQuery =
"SELECT CATALOG_NAME FROM INFORMATION_SCHEMA.SCHEMATA";
...
/// <summary>
/// Retrieves the list of databases from the SQL server.
/// </summary>
/// <param name="server">server</param>
/// <param name="user">user</param>
/// <param name="password">password</param>
public void DatabasesInfoRequested(string server, string user, string password)
{
try
{
if(server == null || user == null || password == null)
throw new ArgumentNullException();
// gets the list of all databases in the given sql server.
DBViewerDAL.Instance().SetConnection(server, user, password);
DataTable data = DBViewerDAL.Instance().GetData(DatabasesQuery);
data.TableName = server;
// saves user's credentials.
DBViewerCache.Instance().AddCredentials(server, user, password);
// represents the data in the DatabaseTreeView.
DBViewerGui.Instance().ShowDatabasesInfo(data);
}
catch(Exception e)
{
// all exception catched.
Log.WriteErrorToLogAndNotifyUser(e.Message);
}
}
DBViewerDAL.Instance().SetConnection(server, user, password);
调用前面看到的 DAL 中的 ConnectToDatabase
方法。特定数据库的表信息以类似的方式检索。检索到信息后,会将其传递给 GUI 层:例如,有关给定 SQL Server 中可用数据库的信息将传递给 TreeView
,而包含数据的特定表将传递给 DataGrid
控件,以便向用户显示。如您所见,BL 还缓存了用户对特定服务器的凭据。
UI - 用户界面层
UI 层由以下三个类组成:DBViewerGui
、DatabaseTreeViewer
和 DBViewerDataGrid
。
DBViewerGui
是一个 MDI 容器,它在其左侧停靠 DatabaseTreeViewer
,并将其 MDI 子项 DBViewerDataGrid
包含在内。DatabaseTreeViewer
表示 SQL Server 及其数据库,并允许选择由 DBViewerDataGrid
表示的数据表。
DBViewerDataGrid
包含一个自定义控件,该控件继承自 DataGrid
,称为 DatabaseDataGrid
。
DatabaseDataGrid
控件将二进制数据表示为二进制字符串,这与二进制类型的常规默认表示值:“Byte[]
”相反。DatabaseDataGrid
控件将图像列表示为底层数据源图像的小缩略图,允许双击单元格以查看/加载其他图像。它还允许使用常见的“CTRL-C”和“CTRL-V”组合键复制/粘贴带二进制数据的行。
本文仅讨论主要的 DatabaseDataGrid
问题。
- 如何以不同于底层源的方式表示数据?
为了以不同于底层源的方式表示数据,使用
DataGridTableStyle
。每个
DataGrid
可能有许多表样式,这些表样式定义在DataGrid
的TableStyles
集合中。每个DataGridTableStyle
反过来又包含许多DataGridColumnStyle
,这些样式在表样式GridColumnStyles
集合中定义。每个DataGrid
列样式实际定义了特定列将如何向用户表示。这样,就可以轻松地表示相同底层数据的不同视图。
- 为了在存在类型为“
image
”的列时表示图像,我们将定义一个自定义的DataGridImageColumn
,它继承自DataGridColumnStyle
并重写其Paint
方法。这是实现方式。
public class DataGridImageColumn : DataGridColumnStyle { ... protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight) { PaintImage(g, bounds, source, rowNum); } ... private void PaintImage(Graphics g, Rectangle bounds, CurrencyManager manager, int rowNum) { SolidBrush backBrush = new SolidBrush(Color.White); try { // thumbnail image from the cell's image. Image thumbnailImage; // gets the img from the DataSource. byte[] img = (GetColumnValueAtRow(manager, rowNum) as byte[]); // if no image in the current cell - displays the default image. if(img == null) { thumbnailImage = defaultImage; } else { Image cellImage = Image.FromStream(new MemoryStream(img)); // creates thumbnail image from cell's // image with default size : thumbnailSize thumbnailImage = cellImage.GetThumbnailImage(ThumbnailSize, ThumbnailSize, new System.Drawing.Image.GetThumbnailImageAbort(ThumbnailCallback), IntPtr.Zero); } g.FillRectangle(backBrush, bounds); g.DrawImage(thumbnailImage, bounds); } catch(ArgumentException e) { g.FillRectangle(backBrush, bounds); g.DrawImage(unknownImage, bounds); Log.WriteErrorToLog(e.Message); } catch(Exception e) { Log.WriteErrorToLog(e.Message); throw; } } ... }
请注意,有时我们无法从
Stream
获取Image
,因此我添加了ArgumentException
处理,该处理绘制一个未知图像,因为我们可能无法识别图像格式。 - 为了在存在类型为“
binary
”或“varbinary
”的列时表示二进制数据,我们将定义另一个自定义的DataGridImageColumn
,它继承自DataGridTextBoxColumn
。DataGridTextBoxColumn
由 .NET Framework 提供,并允许表示可编辑的列。这实际上是我们想要的,但除此之外,我们还希望看到二进制信息。因此,我们将重写DataGridTextBoxColumn
的SetColumnValueAtRow
和GetColumnValueAtRow
,它们分别影响这些行的更新和表示方式。这是实现方式。
public class DataGridBinaryColumn : DataGridTextBoxColumn { ... protected override void SetColumnValueAtRow(CurrencyManager source, int rowNum, object value) { // converts the value to string. string strValue = value.ToString(); // constructs the binary data from the value. byte[] data = new byte[strValue.Length]; for(int i=0; i < strValue.Length; i++) data[i] = Convert.ToByte(strValue[i]); // saves the data. base.SetColumnValueAtRow (source, rowNum, data); } protected override object GetColumnValueAtRow(CurrencyManager source, int rowNum) { // gets the binary data if available object value = base.GetColumnValueAtRow(source, rowNum); // converts the data to binary if possible. byte[] data = value as byte[]; // if the conversion failed then returns the base value. if(data == null) return value; // else else { // constructs binary data representation from the value. StringBuilder binaryRepresentation = new StringBuilder(); int i=0; while(i < data.Length) binaryRepresentation.Append(data[i++]); return binaryRepresentation.ToString(); } }
- 为了在存在类型为“
- 如何在
DataGrid
上执行“复制”-“粘贴”操作?为了允许“CTRL+C”组合键,我们首先应定义此组合键不是控件组合键。
然后,我们应捕获
KeyDown
事件,并分别使用Clipboard
对象处理“CTRL+C”和“CTRL+V”组合键。其实现方式如下。
... /// <summary> /// IsInputKey /// </summary> /// <remarks> /// Marks Ctrl+C combination as InputKey in order to catch its event later. /// </remarks> /// <param name= "keyData"> </param> ///<returns> </returns> protected override bool IsInputKey(Keys keyData) { if(keyData == ( Keys.Control | Keys.C)) return true; return base.IsInputKey (keyData); } ... ///<summary> ///OnKeyDown ///</summary> ///<remarks> ///1. on Ctrl+C copies the DataRow into the Clipboard object. ///2. on Ctrl+V pasts the data from the Clipboard /// object into the DataSource = DataTable. ///</remarks> ///<param name= "e"> </param> protected override void OnKeyDown(KeyEventArgs e) { // if Ctrl+C if(e.KeyData == (Keys.C | Keys.Control)) { DataTable table = (DataTable)this.DataSource; if(selectedDataGridRow < table.Rows.Count) { // saves the DataRow's data // under the name of the DatabaseDataGrid class. DataFormats.Format format = DataFormats.GetFormat(this.ToString()); // copies the data to the clipboard. IDataObject data = new DataObject(); DataRow row = table.Rows[selectedDataGridRow]; data.SetData(format.Name, false, row.ItemArray); Clipboard.SetDataObject(data, false); } } // else if Ctrl+V else if(e.KeyData == (Keys.V | Keys.Control)) { // retrieves the data from the clipboard IDataObject data = Clipboard.GetDataObject(); string format = this.ToString(); if(data.GetDataPresent(format)) { object[] row = data.GetData(format) as object[]; //adds new row to the underline //DataSoruce - DataTable if needed. DataTable table = (DataTable)this.DataSource; if(table.Rows.Count < (selectedDataGridRow + 1)) table.Rows.Add(row); else table.Rows[selectedDataGridRow].ItemArray = row; } } // else if Ctrl+S else if(e.KeyData == (Keys.S | Keys.Control)) { SaveDatabaseTable(); } // else else base.OnKeyDown (e); }
如您所见,当按下“CTRL+C”组合键时,当前行
selectedDataGridRow
的信息将使用Clipboard.SetDataObject
方法保存到剪贴板。当按下“CTRL+V”组合键时,我们使用IDataObject data = Clipboard.GetDataObject();
从剪贴板获取数据,并使用data.GetDataPresent
检查提取的数据是否为我们的数据,data.GetDataPresent
在提取的数据是我们的类型时返回true
。最后,我们将数据保存到新选定的行:selectedDataGridRow
。
备注
- 为了举例说明,我将 Image 列视为仅包含图像二进制信息的列,但使用本文中解释的
DataGridBinaryColumn
样式,它可以轻松地转换为表示任何二进制数据。 - 非常感谢您提出任何建议、改进和 bug 报告。