65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的数据库查看器 - DBViewer

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (37投票s)

2005 年 4 月 18 日

CDDL

8分钟阅读

viewsIcon

178790

downloadIcon

10167

一个简单的数据库查看器,用于操作 SQL Server 数据类型(特别是:image、binary、varbinary 和 text)。

DBViewer

引言

本文介绍了一个简单的数据库查看器,该查看器可以操作 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 以检索/更新数据。

通过 SqlConnectionSqlCommand 分别连接到数据库和操作数据,它们被定义为上述类的成员。

/// <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 不是首选类,我们在示例中仅为此目的使用它,因为它是动态生成 SqlCommands 的最简单方法。在开发商业产品时,请尽量避免使用此类,因为它确实会影响性能。)

通过使用事务来避免数据库中的不一致。

(请注意,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 层由以下三个类组成:DBViewerGuiDatabaseTreeViewerDBViewerDataGrid

DBViewerGui 是一个 MDI 容器,它在其左侧停靠 DatabaseTreeViewer,并将其 MDI 子项 DBViewerDataGrid 包含在内。DatabaseTreeViewer 表示 SQL Server 及其数据库,并允许选择由 DBViewerDataGrid 表示的数据表。

DBViewerDataGrid 包含一个自定义控件,该控件继承自 DataGrid,称为 DatabaseDataGrid

DatabaseDataGrid 控件将二进制数据表示为二进制字符串,这与二进制类型的常规默认表示值:“Byte[]”相反。DatabaseDataGrid 控件将图像列表示为底层数据源图像的小缩略图,允许双击单元格以查看/加载其他图像。它还允许使用常见的“CTRL-C”和“CTRL-V”组合键复制/粘贴带二进制数据的行。

本文仅讨论主要的 DatabaseDataGrid 问题。

  • 如何以不同于底层源的方式表示数据?

    为了以不同于底层源的方式表示数据,使用 DataGridTableStyle

    每个 DataGrid 可能有许多表样式,这些表样式定义在 DataGridTableStyles 集合中。每个 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,它继承自 DataGridTextBoxColumnDataGridTextBoxColumn 由 .NET Framework 提供,并允许表示可编辑的列。这实际上是我们想要的,但除此之外,我们还希望看到二进制信息。因此,我们将重写 DataGridTextBoxColumnSetColumnValueAtRowGetColumnValueAtRow,它们分别影响这些行的更新和表示方式。

      这是实现方式。

      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 报告。
© . All rights reserved.