第4章 项目的第一个版本(1)

4. 项目的第一个版本

讨论了这么久,相信很多朋友已迫不及待地想要开始编码了,本章我们就会完成这个项目的第一个版本,一个真正的、完整功能的数据处理程序。

根据需求分析,我们知道将要开发的项目是一个Windows窗体类型的数据处理程序;前面,我们已经做了很多的工作,并充分考虑了这个软件的功能;另外,我们还准备了项目的数据库,关于这一点,我想还需要说明一下,虽然在本书中,我们的主题是软件开发,但是,一些附加的工作,我们还是会自己动手准备,这样,我们也会从更广泛的层面来理解一个完整的软件系统,多学点没坏处,对吧。

现在,我们打开Visual C# 2010 Express,新建一个Windows窗体项目,并命名为“XYZ”,然后,并保存在一个指定的位置。此外,我还有一个习惯,就是随时运行一下程序以检查项目配置的正确性和有效性。

本章源代码位于“\source\XYZ\”目录。

4.1. 项目的准备工作

当一个软件开始编码工作时,我们还会做一些准备工作,这主要是创建一些通用的、或者是初始化工作代码等,本节,我们就讨论相关的主题,这些内容包括:

  • 连接数据库
  • 程序初始化工作
  • 启动程序

4.1.1. 连接数据库

无论是使用本地数据库文件,还是网络数据库,它们实际上都是应用程序外部的资源,所以,在进行数据应用开发时,我们必须首先确保数据库连接的正确性。本章,我们使用是Access数据库,也就是上一章准备的“xyz.mdb”文件;现在,我们可以将这个数据库文件放在一个指定的位置,以便调用,问题是放在什么地方呢?我的选择是放在与主程序相同的文件夹里。

那么,在测试时,我们的数据库应该在什么地方呢?答案是项目文件夹中的“\bin\debug”目录,如果你的项目设置没有修改过,就可以把xyz.mdb文件复制一份到这个目录中。

接下来,我们就要在软件中使用代码连接这个数据库文件了。我们知道,一般来讲,连接数据库需要使用一个数据库连接字符串,而在C#中使用OLEDB连接Access数据库时,我们经常会使用以下格式的字符串(请写在一行):

Provider=Microsoft.Jet.OLEDB.4.0;Data Source=<文件名>;
Jet OLEDB:Database Password=<数据库密码>;

在实际应用中,我们不可能到处写这么长的数据库连接串吧!

我们知道,在本项目中只有一个数据库,我们只需要在一个地方定义这个字符串就可以在整个项目中使用了,那么,像这样在整个项目中使用的代码,应该怎么定义呢?

我的方案是创建一个静态类CApp,作为项目的通用资源类,关于数据库连接串的定义如下:

// 代码位置:CApp.cs
using System;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Data.OleDb;

namespace XYZ
{
    public static class CApp
    {
        // 数据连接串
        public static string DbCnnStr = GetCnnStr(GetAppPath() + @"xyz.mdb", "");
        // 创建数据库连接串
        public static string GetCnnStr(string fileName, string pwd)
        {
            StringBuilder sb = 
            new StringBuilder("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=");
            sb.Append(fileName);
            sb.Append(";Jet OLEDB:Database Password=");
            sb.Append(pwd);
            sb.Append(";");
            return sb.ToString();
        }
    }
}

代码中,有两个主要的成员:

  • GetCnnStr()方法,用于生成连接Access数据库的连接串,两个参数分别是.mdb文件路径和打开此数据库所需要的密码。
  • DbCnnStr字段,用于连接项目数据库的字符串;使用了GetCnnStr()方法创建,此时,数据库密码为空,因为现在,我们的数据库还没有设置安全密码。

本项目中,我们将主程序和数据库放在了同一目录,在连接数据库时,我们就必须先得到程序所在的路径,我使用了如下代码来完成这项工作。

// 代码位置:CApp.cs
// 给出应用程序所在目录,包括路径最后的“\”
public static string GetAppPath()
{
    string path = Application.StartupPath;
    if (path.Substring(path.Length - 1) == @"\")
        return path;
    else
        return path + @"\";
}

相信这个方法的功能不难理解,虽然Application.StartupPath可以给出主程序的目录;如果主程序在根目录时,最后会有“\”字符(如“C:\”、“D:\”等);但是,程序不在根目录时,则路径最后没有“\”字符,这样就造成返回结果的不一致性;而我们在这里定义的GetAppPath()方法,就可以有效解决了这个问题。

接下来,我们要做一些项目的初始化工作了。

4.1.2. 程序初始化工作

我们知道,在软件项目中,经常用会有一些初始化工作,比如,数据库连接测试、工作目录的准备等,接下来,我们就来做这两项工作。程序初始化方法如下。

// 代码位置:CApp.cs
// 程序的初始化工作
public static bool AppInit()
{
    try
    {
        // 准备临时目录,清空并重建
        string path = GetAppPath() + "temp";
        if (Directory.Exists(path)) Directory.Delete(path, true);
        Directory.CreateDirectory(path);
        // 检查数据库的连接是否正确
        using (OleDbConnection cnn = new OleDbConnection(DbCnnStr))
        {
            cnn.Open();
            return true;
        }
    }
    catch
    {
        MessageBox.Show("软件初始化错误,程序将会关闭,请与开发者联系");
        return false;
    }
}

在这个项目中,需要初始化的工作并不多,在AppInit()方法中,我们主要完成了两个方面的工作:

  • 在主程序所在的目录准备一个名为temp的子目录,作为项目中临时存放文件的目录,我们会在程序启动时自动删除(完成临时文件的清理工作),然后再创建一个空的临时目录。在程序启动时创建临时目录是我的选择,如果有需要,你也可以考虑在程序退出时做这项工作。关于临时目录的作用,在本项目中主要用于报表的生成,稍后会看到相关的应用。
  • 测试一下数据库的连接,操作很简单,只要能正确打开数据库就可以了,这样做是防止数据库文件意外丢失或破坏造成程序不能正常工作。在执行这个任务时,需要引用System.Data.OleDb命名空间。

4.1.3. 启动程序

当我们创建一个Windows窗体项目时,会自动创建一个名为Form1的窗体,为了简化工作,我们可以将其作为程序的主窗体(即启动窗体),然后,我们在此窗体的Load事件中调用CApp.AppInit()方法用于程序的初始化工作,如下面的代码:

// 代码位置:Form1.cs
private void Form1_Load(object sender, EventArgs e)
{
if (CApp.AppInit() == false)
        Application.Exit();
}

代码很简单,如果初始化不成功,则退出程序。

在项目中,如果不想让Form1窗体作为启动窗体,则可以在Program.cs文件中修改相关代码,如下面代码中加粗的位置:

//代码位置:Program.cs
namespace XYZ
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

本项目中,我们将使用Form1窗体作为程序的主窗体,并在此窗体中添加三个按钮,其功能分别是:

  • 问卷录入。启动问卷录入窗体,即打开FormQuestionnaireInput窗体实例。
  • 报表中心。启动报表窗体,即打开FormReportMain窗体实例。
  • 退出。退出本程序。

相关的代码如下。

// 代码位置:Form1.cs
private void button1_Click(object sender, EventArgs e)
{
    // 问卷录入
FormQuestionnaireInput f = new FormQuestionnaireInput();
    f.ShowDialog();
}

private void button2_Click(object sender, EventArgs e)
{
    // 报表中心
    FormReportMain f = new FormReportMain();
    f.ShowDialog();
}

private void button3_Click(object sender, EventArgs e)
{
    // 退出
    Application.Exit();
}

最后,来张Form1窗体的快照。

图像说明文字

4.2. 问卷录入

本节,我们将实现问卷的录入功能,首先,我们需要创建FormQuestionnaireInput窗体,其设计如下图。

图像说明文字

在设计窗体时,我们用两个Panel控件将它分成两个区域,一是上面的操作区,我们使用panel1作为操作按钮的容器,请注意将panel1的Dock属性设置为Top;然后,我们使用panel2作为问卷内容的容器,此时,panel2的Dock属性应该设置为Fill,而AutoScroll属性设置为True。

问卷内容设计中,我们对所有的数据字段都使用了文本框控件(TextBox),而它们的名称将与数据库的字段名相对应,如问卷编号数据控件名为RNumber,问题一的数据控件名称为q1,以此类推。

在实现这些功能之前,还有一个很重要的问题要明确:当我们保存一张问卷的数据时,如何确定是添加一条新记录,还是修改一条已存在的记录呢?

也许你已经发现了,在我们设计的窗体中,并没有RID数据,因为这是一个Identity字段,它的数据是自动生成的,而我们则可以利用这个数据来判断记录是一条新的记录或是修改一个已存在的记录,我的方案就是在FormQuestionnaireInput窗体中创建一个私有字段CurrentId用于标识当前记录的Identity值。我们知道,Identity值一般是一个大于0的整数,所以,当CurrentId字段的值小于或等于0时,我们的保存操作就在数据表中添加一条新记录;如果CurrentId字段值大于0,则我们就更新数据表中相应的数据记录。CurrentId字段的定义如下代码:

// 代码位置:FormQuestionnaireInput.cs
namespace XYZ
{
    public partial class FormQuestionnaireInput : Form
    {
        // 当前记录Identity值
        private int CurrentId = -1;

        //
        public FormQuestionnaireInput()
        {
            InitializeComponent();
        }

    //...
    }
}

此外,由于我们在窗体中使用OLEDB连接数据库,还需要在窗体代码前部添加如下引用:

// 代码位置:FormQuestionnaireInput.cs
using System.Data.OleDb;

接下来,我们按照按钮从左到右的顺序来实现这些功能。

4.2.1. 新问卷

新问卷的功能就清除当前的数据,窗体中,我们应该清除所有TextBox控件的内容;另外一个需要注意的问题就是CurrentId字段的值,当我们需要一个新的问卷时,应该将它的值设置为一个小于或等于0的值,我的选择是将它设置为-1。这样一来,创建新问卷的清理工作,我们就可以封装成如下的方法:

// 代码位置:FormQuestionnaireInput.cs
// 清除数据
private void ClearData()
{
    CurrentId = -1;
    foreach (Control ctr in panel2.Controls)
    {
        if (ctr is TextBox)
        {
            TextBox txt = (ctr as TextBox);
            txt.BackColor = Color.White;
            txt.Text = "";
        }
    }
    RNumber.Focus();
}

代码中,我们首先将CurrentId字段设置为-1。然后,我们通过遍历panel2中的所有控件,将所有的TextBox控件的背景色(BackColor属性)设置为白色,而内容(Text属性)设置为空。最后,将窗体的焦点设置在RNumber控件,这样可以方便立即进行新问卷的数据录入工作。

接下来,你可以选择在“新问卷”按钮中直接调用ClearData()方法,当然,你也可以考虑更多一些问题,比如,当前已显示了一张问卷的数据,那么,创建新问卷时,是不是要提醒用户保存这条信息呢?这完全取决于你看待这个问题了,是吧?我的选择是提醒用户一下,如下面的代码。

// 代码位置:FormQuestionnaireInput.cs
private void button1_Click(object sender, EventArgs e)
{
// 新问卷
    if (CurrentId > 0 &&
        MessageBox.Show("您正在编辑一张问卷,要先保存它吗?", "",
        MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
    {
        // 执行保存操作
        button2_Click(sender, e);
    }
    else
    {
        // 直接清理
        ClearData();
    }
}

你一定也发现了,代码中的button2_Click()方法还没有定义,它正是“保存”按钮的代码,不过不用担心,我们马上就来看看它的实现。

4.2.2. 保存

前面,我们已经调用了“保存”的代码,现在,我们就首先看一下它的定义,不过,你会接着发现,其中的关键代码依然需要慢慢来实现。

// 代码位置:FormQuestionnaireInput.cs
private void button2_Click(object sender, EventArgs e)
{
// 检查数据
    if (CheckData() == false) return;
    // 保存数据
    if (SaveData())
    {
        MessageBox.Show("保存成功,‘确定’后添加新的问卷", "",
            MessageBoxButtons.OK, MessageBoxIcon.Information);
        ClearData();
}
    else
    {
        MessageBox.Show("保存时产生未知错误,可与开发者联系", "",
            MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}

你会发现代码中加粗的CheckData()和SaveData()方法还没有实现,而这两个方法正好就是我们在分析需求时所考虑过的处理过程,即检查数据正确性和保存数据,现在,我们就来看一看它们的实现。

检查数据正确性

除了问题六(不知道是什么?在前面看一下吧!^^),其它问题的数据都是整数型,而且,问题六的答案很有可能是空的,所以,我们的数据检查工作实际上是比较简单的,只是需要对问题一到问题五的每一个数值字段进行相应的检查。

我们首先看一下问卷编号的正确性检查,前面,我们已经约定,这应该是一个6位的整数,也就是说,它应该是从100000到999999之间的整数,当然,你还可以根据问卷编号的规则做更详细的检查,在这里,为了简化代码,我们就不这么做了。那么,问卷编号的数据检查工作应该怎么做呢?我想应该是类似下面的代码:

bool checkResult = false;
int convertResult;
if (int.TryParse(RNumber.text, out covertResult))
{
if (covertResult >= 100000 && convertResult <= 999999)
    checkResult = true;
}

代码看上去并不复杂,的确是这样,不过,如果每一个数据都这样检查,你一定对大量重复的代码感到不舒服,这样,你也许就会和我一样,找一下代码的规则,然后简化这些数据检查工作。

分析一下数值的检查过程,我想这个过程应该是这样的: - 将输入的文本内容(string类型)转换为int类型,如果转换不成功,则数据错误。 - 对转换后的int数据范围进行判断,如果在指定的范围内则数据正确,否则数据错误。

根据这一思路,实际上,我们可以提炼出这一过程中的三个参数,即输入的内容,以及允许的最小值和最大值,而检查的方法是相同的;这样一来,我们就可以创建一个方法来做数据范围的检查工作,而需要的三个参数将会定义为方法的三个参数。以下就是我们创建的Interval()方法,不过,我们创建了三个重载版本,它们的不同点是带入的输入内容的类型,代码如下:

// 代码位置:FormQuestionnaireInput.cs
// 检查数据范围
private bool Interval(int value, int min, int max)
{
return (value >= min && value <= max);
}

private bool Interval(string str, int min, int max)
{
int value;
    if (int.TryParse(str, out value))
        return Interval(value, min, max);
else
        return false;
}

private bool Interval(TextBox txt, int min, int max)
{
bool result = Interval(txt.Text, min, max);
    if (result)
        txt.BackColor = Color.White;
else
        txt.BackColor = Color.LightPink;
return result;
}

为什么要有三个Interval()方法,我们会在“代码分析”小节中进行讨论。现在,我们先看一看CheckData()方法中是如何使用Interval()方法的。

// 代码位置:FormQuestionnaireInput.cs
// 检查数据
private bool CheckData()
{
    bool result = true;
    //
    result = Interval(RNumber, 100000, 999999) && result;
    result = Interval(q1, 1, 2) && result;
    result = Interval(q2, 1, 3) && result;
    result = Interval(q3a, 1, 3) && result;
    result = Interval(q3b, 1, 3) && result;
    result = Interval(q3c, 1, 3) && result;
    result = Interval(q3d, 1, 3) && result;
    result = Interval(q3e, 1, 3) && result;
    result = Interval(q3f, 1, 3) && result;
    result = Interval(q4, 1, 5) && result;
    result = Interval(q5, 1, 6) && result;
    //
    return result;
}

CheckData()方法很简单,当所有的数值类数据都输入正确时,方法返回true,否则返回false,同时,数据不正确的TextBox控件的背景色会显示为LightPink色。

在这里,需要注意的是,在CheckData()方法的实现中,我们一次性检查了所有的字段,这样做的优势就是可以一次性提醒用户所有的问题数据项,方便用户快速修改,这样的做法在此项目中是没有问题的,因为检查操作会很快完成。但有一点需要注意,如检查数据的操作比较耗时,这在大型,特别是Web项目或分布式项目中是很常见的,我们可以考虑只要检测到一个错误就终止检查工作,方法立即返回false值,不再做其它的检查工作。

保存数据

在保存按钮中,当CheckData()方法返回true,即所有数据检查通过后,将调用SaveData()方法实际保存数据,以下就是SaveData()方法的实现。

// 代码位置:FormQuestionnaireInput.cs
// 保存数据
private bool SaveData()
{
using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
    {
        cnn.Open();
        OleDbCommand cmd = cnn.CreateCommand();
        if (CurrentId <= 0)
        {
            cmd.CommandText = @"insert into questionnaire_data
 (RNumber,q1,q2,q3a,q3b,q3c,q3d,q3e,q3f,q4,q5,q6)                     
values(@RNumber,@q1,@q2,@q3a,@q3b,@q3c,@q3d,@q3e,@q3f,@q4,@q5,@q6);";
        }
else
{
            cmd.CommandText = @"update questionnaire_data set 
            RNumber=@RNumber,q1=@q1,q2=@q2,q3a=@q3a,q3b=@q3b,
            q3c=@q3c,q3d=@q3d,q3e=@q3e,q3f=@q3f,
            q4=@q4,q5=@q5,q6=@q6 where RID=@RID;";
        }
//
        cmd.Parameters.AddWithValue("@RNumber", Convert.ToInt32(RNumber.Text));
        cmd.Parameters.AddWithValue("@q1", Convert.ToInt32(q1.Text));
        cmd.Parameters.AddWithValue("@q2", Convert.ToInt32(q2.Text));
        cmd.Parameters.AddWithValue("@q3a", Convert.ToInt32(q3a.Text));
        cmd.Parameters.AddWithValue("@q3b", Convert.ToInt32(q3b.Text));
        cmd.Parameters.AddWithValue("@q3c", Convert.ToInt32(q3c.Text));
        cmd.Parameters.AddWithValue("@q3d", Convert.ToInt32(q3d.Text));
        cmd.Parameters.AddWithValue("@q3e", Convert.ToInt32(q3e.Text));
        cmd.Parameters.AddWithValue("@q3f", Convert.ToInt32(q3f.Text));
        cmd.Parameters.AddWithValue("@q4", Convert.ToInt32(q4.Text));
        cmd.Parameters.AddWithValue("@q5", Convert.ToInt32(q5.Text));
        cmd.Parameters.AddWithValue("@q6", q6.Text);
        // 更新时添加RID参数
        if (CurrentId > 0) 
cmd.Parameters.AddWithValue("@RID", CurrentId);
        //
        if (cmd.ExecuteNonQuery() >= 0)
            return true;
        else
            return false;
}
}

使用过ADO.NET组件的话,对这些代码应该非常熟悉了;在这里有一点需要注意一下,就是使用CurrentId字段的值来判断是插入新记录(insert语句)还是更新已存在的记录(update语句)。这里使用了常见的SQL语句,如果你还不太熟悉,建议可以系统地了解一下,这对我们开发数据应用项目是非常有帮助的。

另一个需要注意的问题是,ExecuteNonQuery()方法的调用,我们知道,此方法会返回命令执行时影响的记录数。如果是insert语句执行成功,则此方法会返回1;如果是update语句执行成功呢?

如果新的数据与原数据一致,则没有记录被修改,此时ExecuteNonQuery()方法会返回0;所以,当此方法返回值大于或等于0时,我们就认为SQL已经成功执行了。但是,这也是有风险的,如果指定RID值的记录不存在,则ExecuteNonQuery()方法也会返回0,所以,严格来讲,此时应该先检查指定RID的记录是否真的存在,不过,在这里我们根据项目特点(同时只有一名用户操作)简化了操作,就没有做过多的检查了;不过,在后续的讨论中,我们是不会忽略这个问题的。

到现在为止,我们已经可以成功地添加新记录了,此时,你甚至可以请用户真正地来录入一些真实数据感觉一下,对一些细节问题进行讨论,并对可能的问题做出相应的改进。

4.2.3. 载入——问卷查询

载入操作,实际就是通过问卷编号查询已录入的问卷数据,载入一张问卷数据后,我们可以修改并重新保存这些数据。

查询问卷的条件非常简单,即需要用户输入一个6位的问卷编号就可以了,问题是,用户在哪里输入问卷编号呢?

一个最简单的方法就是在RNumber控件中输入,然后,通过“载入”按钮来执行查询。但这样有一个小问题,即RNumber控件和“载入”按钮在位置上并没有直观的联系,虽然我们可以告诉用户这个操作的小秘密,但我们还是决定自己麻烦点,而不要让用户感到什么不自然。

我的做法是,在单击“载入”按钮后,会有一个弹出窗口让用户输入查询的问卷编号,然后通过输入的内容查询相应的问卷数据,如下图。

图像说明文字

使用过VB或VB.NET的朋友一定会对这个窗口比较熟悉了,对了,它就是InputBox;但在C#中,我们并没有可以直接使用的输入对话框,不过,这并不难实现,我们马上来自己动手做一个。

实现FormInputBox对话框窗体

创建一个Windows窗体,并命名为“FormInputBox”,设置ControlBox属性值为false,AcceptButton属性值为button1(确定),CancelButton属性值为button2(取消)。下面就是所有的代码。

// 代码位置:FormInputBox.cs
using System;
using System.Windows.Forms;

namespace XYZ
{
    public partial class FormInputBox : Form
    {
        //
        public string ReturnValue = "";
        //
        public FormInputBox()
        {
            InitializeComponent();
        }
        // 取消
        private void button2_Click(object sender, EventArgs e)
        {
            ReturnValue = "";
            this.Close();
        }
        // 确定
        private void button1_Click(object sender, EventArgs e)
        {
            ReturnValue = textBox1.Text;
            this.Close();
        }

        // 显示窗体,并返回输入的内容
        public static string ShowInputBox()
        {
            FormInputBox input = new FormInputBox();
            input.ShowDialog();
            return input.ReturnValue;
        }
        //
    }
}

代码中的加粗部分就是需要我们手工添加的内容,实际上并不多。需要注意的几个地方是:

  • ReturnValue字段,确定输入对话框返回的内容。如果点击“确定”按钮,则返回textBox1控件输入的内容;如果点击了“取消”按钮,则返回空字符串。
  • ShowInputBox()静态方法,这才是我们要调用的唯一方法。

显示问卷数据

下面,我们来看看如果通过FormInputBox窗体输入的问卷编号来显示相应的问卷数据,如代码。

// 代码位置:FormQuestionnaireInput.cs
private void button3_Click(object sender, EventArgs e)
{
ClearData();
// 载入数据
    string sNumber = FormInputBox.ShowInputBox().Trim();
    if (sNumber == "") return;
    //
    if (Interval(sNumber, 100000, 999999) == false)
    {
        MessageBox.Show("问卷编号应该是一个6位整数");
        return;
    }
    //
    using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
    {
        cnn.Open();
        OleDbCommand cmd = cnn.CreateCommand();
        cmd.CommandText = @"select top 1 * from questionnaire_data where RNumber=@RNumber;";
        cmd.Parameters.AddWithValue("@RNumber", Convert.ToInt32(sNumber));
        using (OleDbDataReader dr = cmd.ExecuteReader())
        {
            if (dr.Read())
            {
                CurrentId = Convert.ToInt32(dr["RID"]);
                RNumber.Text = dr["RNumber"].ToString();
                q1.Text = dr["q1"].ToString();
                q2.Text = dr["q2"].ToString();
                q3a.Text = dr["q3a"].ToString();
                q3b.Text = dr["q3b"].ToString();
                q3c.Text = dr["q3c"].ToString();
                q3d.Text = dr["q3d"].ToString();
                q3e.Text = dr["q3e"].ToString();
                q3f.Text = dr["q3f"].ToString();
                q4.Text = dr["q4"].ToString();
                q5.Text = dr["q5"].ToString();
                q6.Text = dr["q6"].ToString();
            }
            else
            {
                MessageBox.Show("指定编号的问卷数据不存在");
            }
        }
    }
}

相信这些代码并不难理解,这里就不多说明了。

4.2.4. 退出

退出功能,你当然可以简单的关闭窗体,如:

// 代码位置:FormQuestionnaireInput.cs
private void button4_Click(object sender, EventArgs e)
{
// 退出
    this.Close();
}

同时,你也可以像“新问卷”功能那样,考虑是否存在当前问卷数据需要保存的情况。一个常用的做法是在窗体的FormClosing事件中添加相应的代码,这样一来,无论是通过“退出”按钮,还是通过窗体右上角的关闭按钮来关闭窗体,都可以执行判断操作。如下面的代码:

// 代码位置:FormQuestionnaireInput.cs
private void FormQuestionnaireInput_FormClosing(object sender, 
FormClosingEventArgs e)
{
    if (CurrentId > 0 &&
        MessageBox.Show("您正在编辑一张问卷,要先保存它吗?", "",
        MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
    {
        // 取消关闭,执行保存操作
        e.Cancel = true;
        button2_Click(sender, e);
    }
}

取消窗体关闭操作时,只需要将FormClosingEventArgs对象的Cancel属性设置为true即可。

4.2.5. 为什么没有删除功能

有没有删除功能,似乎要取决于开发人员了,一般来讲,普通用户是不会向开发者提出这样的需求的。

那么,我们需不需要添加“删除”功能呢?

在我们的项目中,并没有添加删除功能,主要是防止数据的误操作,另一原因是,在本项目中,没有删除功能并不会影响软件的整体操作。

当然,如果你需要添加“删除”功能,也是非常的简单,在FormQuestionnaireInput窗体中添加一个按钮,然后使用如下代码就可以了。

// 代码位置:FormQuestionnaireInput.cs
private void button5_Click(object sender, EventArgs e)
{
// 删除操作
    if (MessageBox.Show("真的要删除当前问卷数据吗?", "", 
        MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
    {
        if (CurrentId > 0)
        {
            using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
            {
                cnn.Open();
                OleDbCommand cmd = cnn.CreateCommand();
                cmd.CommandText = @"delete from questionnaire_data where RID=@RID;";
                cmd.Parameters.AddWithValue("@RID", CurrentId);
                if (cmd.ExecuteNonQuery() >= 0)
                {
                    MessageBox.Show("问卷已删除");
                }
            }
        }
        ClearData();
}
}

代码中,只有CurrentId大于0,也就是已经载入有问卷数据时,才会执行从数据库中删除记录的操作;否则,直接清理数据控件内容就可以了。

请注意,很多时候,数据是删除容易,而获取难,所以,在数据应用软件中添加删除功能一定要谨慎,要保守地判断用户对删除功能的需求问题。

现在为止,对于问卷数据的基本操作(录入、保存、查询等)已经完成了,接下来,我们将继续实现另一个主要的功能,即报表的实现。

目录