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

在没有向导的情况下编写 C# 中的 N 层应用程序:第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (32投票s)

2008年7月3日

CPOL

10分钟阅读

viewsIcon

190605

downloadIcon

4985

在C#中无需使用向导编写N层应用程序。

引言

这是本系列文章的最后一部分。在本文中,我们将大量使用DataGrid:更新、删除和搜索。

我的下一篇文章将带您进入远程处理的世界。我们将把我们构建的层托管到不同的机器上。我一直在寻找一篇易于理解的关于远程处理的文章,大多数文章开始时解释得很好,但最终变得复杂。但我保证我不会偏离我解释文章的方式。这将是您读过的最简单的远程处理文章。我不会再次显示N层图表,我们都知道它,我将直接切入主题。

背景

在本文中,我们将使用DataGrid进行搜索、更新和删除。我们将使用存储过程和“全匹配更新”并发逻辑。存储过程的逻辑应该封装起来,DAL(数据访问层)的逻辑也应该封装起来。只有BLL(业务逻辑层)才能调用正确的函数和方法,而表示层应该只调用BLL的函数,并且BLL的逻辑仍应封装起来。最后,在上一篇文章中,我们创建了一个名为“客户详细信息”的表,我们将使用该表,但我们需要创建另一个名为“产品”的表,以使其更有趣。

使用代码

我们将在文章中使用多个注释,并将C#作为我们的语言。

Start

我们将搜索、更新和删除DataGridView中的记录。我们必须弄清楚该怎么做。我们要做的第一件事是创建另一个表,以添加到我们在上一篇文章中创建的表中。打开查询分析器并像这样创建一个表

Create table Products
(
    Pro_ID int Primary key not null, 
    Client_ID int Foreign key (Client_ID) references Client_Details not null,
    [Prod_Name] [varchar](50) NULL,
    Prod_Description varchar(50) null,
    Purchase_Date DateTime Not null,
    Price Money null,
    Delivered int null
)

按F5运行它,并用数据填充您的表

insert into Products (Pro_ID,Client_ID,Prod_Name,Prod_Description,Purchase_Date,Price)
values(2024,102,'Valuation Roll 2001','Property Values for 2001',Getdate(),15000)

至少填充四条记录,并查询您的表以确保您的表中有值

select * from Client_Details

select * from Products

两个表都应该返回记录。Client_ID是上一篇文章中创建的表的主键,它是产品表的外键。我们尚未完成数据库工作。接下来,我们将为我们的CRUD应用程序创建存储过程。

数据库工作 (SQL)

在我的情况下,我同时拥有SQL Server 2000和SQL Server 2005,您可以选择其中任何一个。如果您使用的是SQL Server 2000,请转到查询分析器并开始编码。请记住,即使您有SQL Server 2005,我们也不使用向导来完成数据库工作。让我们像这样创建存储过程:我们的第一个存储过程用于用户根据客户名称搜索DataGrid,请注意,我们所有的过程都将显示来自Join的数据。现在,我们的存储过程将如下所示

--SEARCHING THE GRID 
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[SEARCH_CLIENT] 

@CLIENT_NAME VARCHAR(12)
AS

select p.Pro_id,p.Client_ID,p.Prod_Name as [Product Name],
p.Prod_Description as [Description],
p.Price,c.Client_Name, p.Delivered from Products p
inner Join Client_Details c on 
p.Client_ID = c.Client_ID
where c.Client_name LIKE @CLIENT_NAME + '%' 
/*I USED '%', TO ALLOW USERS TO SEARCH WITHOUT THE WILDCARD
  BUT IT A USER ENTERS AN EMPTY STRING, IT WILL RETURN
  ALL RECORDS YOU CAN CHOOSE TO ALLOW USERS TO ENTER IT THEMSELVES */
*/

现在请记住,上述存储过程将使用Join进行搜索,当更新时,我们必须知道要更新哪个表。在本文中,我们将只更新一个表和一个字段:“产品”表中的“Delivered”字段,通过更改“产品”表中“Delivered”字段的产品状态。这意味着,我们将只更新产品表。我们的更新存储过程将如下所示

--UPDATING THE GRID 

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--Update Stored Procedure
ALTER PROCEDURE [dbo].[Update_Status_Grid]
(--Carry Original values before Update
@Original_Pro_id int,
@Original_Delivered  int = null,
@Delivered int
)
as 
Update Products 
set Delivered = @Delivered
where (Pro_ID = @Original_Pro_id )
And   (Delivered = @Original_Delivered 
OR     @Original_Delivered     Is Null And Delivered  Is Null)

--Refreshing Data
;
select p.Pro_id,p.Client_ID,p.Prod_Name as [Product Name],
p.Prod_Description as [Description],
p.Price,c.Client_Name, p.Delivered from Products p
inner Join Client_Details c on 
p.Client_ID = c.Client_ID
where (Pro_id = @Original_Pro_id )

嗯……有意思,让我告诉你上面这个过程发生了什么。首先,我们必须存储原始值和用于更新变量的值,并且在更新之前,我们需要检查自我们使用第一个过程检索记录以来值是否未发生更改。如果值未发生更改,则可以进行更新。最后,我们正在刷新数据。我们使用原始值构造了查找原始记录的Where子句。这样,如果单个字段已被修改,记录将不匹配,并且更改将不会发生。这是并发控制,您可以选择以不同的方式编写上述过程来解决并发问题。

--Delete THE GRID 

Create PROCEDURE Delete_Record
(--Carry Original values before Update
@Pro_id int
)
as 
Delete Products 
where Pro_ID =@Pro_id

存储过程很简单,它只是根据主键进行删除。现在,我们的数据库工作已经完成,是时候编码了。打开您的Visual Studio,选择C#中的Windows应用程序,并将其命名为“Client_App”。像我一样设置一个漂亮的背景,尽管这不重要。完成之后,您的窗体将如下所示

N-Tier22/Search_Form.jpg

如果您仍然使用Visual Studio 2003,则添加一个“DataGrid”,而在VS2005中,您添加一个“DataGridView”。从上面的窗体中可以看出,我们首先要搜索,然后是更新和删除。您的解决方案资源管理器现在应该如下所示

N-Tier22/SolutionEx.jpg

请记住,我们正在构建一个N层应用程序。我们将要做的是像我们在之前的文章中那样添加其他层。下一步是添加一个BLL作为类项目。向现有项目添加另一个类项目并将其命名为BLL,再添加一个并将其命名为“DAL”。现在,您的解决方案资源管理器将如下所示

N-Tier22/Complete_Ex.jpg

如您所见,粗体字Client_App是表示层,用户将与它进行交互。我们还有BLL(业务逻辑层)和我们的DAL(数据访问层)。在编码之前,我希望您更改类的名称以与其项目对应。例如,将BLL class1.cs 更改为“BLL.cs”。完成后,您在解决方案资源管理器中的项目应该如下所示

N-Tier22/Com_Ex.jpg

现在一切都完成了,是时候编码了。问题是我们从哪里开始。好的,我更喜欢从后往前,这意味着我们从DAL(数据访问层)到PL(表示层)。

DAL(数据访问层)

我们的表示层已完成,但没有代码。我们将从编码DAL开始。打开DAL类文件。我们需要添加不自动可用的命名空间,让我们导入这些命名空间并为之前创建的每个存储过程编写代码。以下是DAL类的代码

public class DAL
{
    String strcon = @"User id =SA;Password=MYNEWPASORWS;" + 
                    @"Server=SERVER1;Database=ValRollClients";
    SqlCommand cmdselect = null;
    SqlCommand cmdupdate = null;
    SqlCommand cmddelete = null;
    SqlConnection con;
    SqlDataAdapter da;

   /*=====================================================================
    //This Function is Being Called from the BLL, and it uses SP's, and   
    //Delete the Data
    ======================================================================*/

    public void Delete_Client(DataSet dsdata)
    {

        con = new SqlConnection(strcon);
        cmddelete = new SqlCommand();
        cmddelete.CommandText = "Delete_Record";
        cmddelete.CommandTimeout = 0;
        cmddelete.CommandType = CommandType.StoredProcedure;
        cmddelete.Connection = con;

        cmddelete.Parameters.Add("@Pro_id", 
                  SqlDbType.Int,4,"Pro_id");

        cmddelete.Parameters["@Pro_id"].Value = "Prop_Id";


        try
        {
            con.Open();

            cmddelete.ExecuteNonQuery();

            con.Close();
        }
        catch (SqlException)
        {
            throw;
        }
    }

    public DataSet Search_Client(String strClient)
    {
        DataSet ds = new DataSet();
        con = new SqlConnection(strcon);
        cmdselect  = new SqlCommand();
        cmdselect.CommandText = "SEARCH_CLIENT";
        cmdselect.CommandTimeout = 0;
        cmdselect.CommandType = CommandType.StoredProcedure;
        cmdselect.Connection = con;
        da = new SqlDataAdapter(cmdselect);
        cmdselect.Parameters.Add(new SqlParameter("@CLIENT_NAME",
                  SqlDbType.VarChar, 12,"Client_name"));
        cmdselect.Parameters["@CLIENT_NAME"].Value = strClient;

        try
        {
            con.Open();

            da.Fill(ds,"Products");    

            con.Close();
        }
        catch (SqlException)
        {   
            throw;
        }
         return ds;
    }

    public void  Update_Records(DataSet dsdata)
    {
        da = new SqlDataAdapter();
        con = new SqlConnection(strcon);
        //For Update
        cmdupdate = new SqlCommand();
        cmdupdate.CommandType = CommandType.StoredProcedure;
        cmdupdate.CommandText = "Update_Status_Grid";
        cmdupdate.CommandTimeout = 0;
        cmdupdate.Connection = con;
        //For Delete
        cmddelete = new SqlCommand();
        cmddelete.CommandText = "Delete_Record";
        cmddelete.CommandTimeout = 0;
        cmddelete.CommandType = CommandType.StoredProcedure;
        cmddelete.Connection = con;


        //Adding Parameters  for Update 
        cmdupdate.Parameters.Add("@Original_Pro_id", SqlDbType.Int, 4, "Pro_id");
        cmdupdate.Parameters["@Original_Pro_id"].SourceVersion = DataRowVersion.Original;
        cmdupdate.Parameters.Add("@Original_Delivered",SqlDbType.Int,4,"Delivered");
        cmdupdate.Parameters["@Original_Delivered"].SourceVersion = DataRowVersion.Original;
        cmdupdate.Parameters.Add("@Delivered", SqlDbType.Int, 4, "Delivered");

        //   Adding Parameters for Delete
        cmddelete.Parameters.Add("@Pro_id", SqlDbType.Int, 4, "Pro_id");

        cmddelete.Parameters["@Pro_id"].Value = "Prop_Id";

        //Telling the Adapter that we are going to use this Command Object for that

        da.UpdateCommand = cmdupdate;
        da.DeleteCommand = cmddelete;

        try
        {
            con.Open();
            da.Update(dsdata, "Products");
            con.Close();
        }
        catch (SqlException)
        {
            throw;
        }
    }
}

我们的DAL类已完成。让我们看看我们的BLL。它将如何与DAL和PL通信?请记住,我们的客户端不应该直接访问我们的DAL,这意味着我们的BLL应该成为DAL和客户端之间的中间人。这意味着,我们必须将对DAL项目的引用添加到BLL项目,并且在客户端项目中,我们将只添加对BLL的引用。

BLL(业务逻辑层)

在我们的BLL中,我们将处理所有的业务规则和错误处理程序。正如您所看到的,在我们的DAL类中,我们只捕获了异常但从未显示它们。我们只将它们抛给调用方法,该方法将只来自BLL,并且BLL会将消息发送给客户端。这意味着BLL旨在控制层之间数据的有效性和一致性。从DAL传入的数据已准备好由客户端呈现,而从客户端应用程序(表示层)传入的数据经过有效性检查并传递给DAL。我们要做的第一件事是将BLL的引用添加到DAL项目,以便我们可以调用Save方法。完成此操作后,让我们去编写BLL的代码。我们的BLL将如下所示

public class BLL
{
    public DataSet Search_Client(String strClient)
    {
        DAL.DAL obj = new DAL.DAL();

        DataSet dsdata = new DataSet();
        try
        {
              dsdata = obj.Search_Client(strClient);

        }
        catch (SqlException e)
        {
            MessageBox.Show(e.Message,"Trapped in BLL");
        
        }
        return dsdata;
    }

    public void Update_Records(DataSet dsdata)
    {
    
        DAL.DAL obj = new DAL.DAL();

        try
        {
           obj.Update_Records(dsdata);

        }
        catch (SqlException e)
        {
            MessageBox.Show(e.Message, "Trapped in BLL");
          
        }
    }
}

现在我们的BLL已完成。让我们再次回到客户端并完成工作。

PL(表示层)

这是我们的日常用户看到和使用的部分。我们必须设置引用,以便我们可以使用BLL的函数和方法。完成引用设置后,双击“搜索”按钮并开始编码。

BLL.BLL obj = new BLL.BLL();
String strsearch = txtsearch.Text.Trim();
try
{
    dsdata = obj.Search_Client(strsearch);

    if (dsdata.Tables[0].Rows.Count < 1)
    {
        MessageBox.Show("Record not Found");
    }
    else
    {
        dataGridView1.DataSource = dsdata;
        dataGridView1.DataMember = "Products";
    }
}
catch (ApplicationException ex)
{   
    MessageBox.Show(ex.Message);
}

完成后,请记住有一个“更新”按钮,按以下方式编写代码

BLL.BLL obj = new BLL.BLL();
            
try
{
    if (dsdata.HasChanges()) 
    {
        obj.Update_Records(dsdata);
        MessageBox.Show("Updated"); 
    }
    else
    {
        MessageBox.Show("No Changes Made");
    }
}
catch (ApplicationException ex)
{
    MessageBox.Show(ex.Message);
}

现在我们的“更新”按钮将为我们做两件事:更新和删除。这意味着,如果“已交付”字段正在修改,则将调用更新命令以执行更新存储过程,如果用户选择记录并按键盘上的“删除”,则记录将消失。但是当用户单击“更新”按钮时,适配器会查找我们创建的删除命令,然后记录才会从我们的表中删除。现在,让我们测试我们的应用程序。按键盘上的F5,您将看到您的PL(表示层);搜索一个名称,或者将文本框留空,然后单击“搜索”按钮。您将看到Join中的所有记录。首先,您必须从测试更新命令开始。

N-Tier22/Screen_Start.jpg

在网格上执行任何操作之前,单击“更新”按钮,捕获到的事件会给您一条消息,内容是

N-Tier22/NoChanges.jpg

现在,这来自我们PL中的以下代码行。这意味着,如果DataSet中没有更改,将向用户显示一条消息,说明自检索以来DataSet中没有进行任何更改。

if (dsdata.HasChanges()) 
{
    obj.Update_Records(dsdata);
    MessageBox.Show("Updated"); 
}
else
{
    MessageBox.Show("No Changes Made");
}

从上面的屏幕中,向左滚动并查找“已交付”字段

N-Tier22/Delivered.jpg

更改第一个数字并单击更新,您将收到一条消息,显示“已更新”。再次搜索网格,并再次查看该字段。

N-Tier22/Delivered_Changed.jpg

为了确认记录已保存,而无需依赖我们显示的消息或网格本身,您可以在SQL数据库中运行Select *语句,您会注意到我们删除的产品已消失

N-Tier22/DB_Check.jpg

如您所见,2002年的产品已消失

N-Tier22/Screen_Confirm_Deleted_Product.jpg

我们没有使用任何向导。我们的代码不容易受到 SQL 注入的攻击。您应该始终实践的唯一一件事是将连接字符串放在 Settings 文件或 app.config 文件中。不要将其硬编码。这将在您下次想要更改服务器名称、数据库名称或密码时很有帮助;这样,您就不需要再次编译应用程序了。

结论

我们没有向导的帮助编写了这个应用程序。在我的下一篇文章(不是第三部分),远程处理的介绍中,我们将在不同的物理机器(计算机)中托管这些层。棘手的部分是层之间跨边界的通信。我将解释我们如何设计DAL和BLL以通过远程处理在不同边界之间进行通信。请注意,在构建应用程序时,我们将继续N层的传统;无论它们多小,我们都将始终采用N层。我要感谢大家的支持和电子邮件。我们永远不会停止学习,尤其是我。下一篇文章将是我关于Windows开发的最后一篇文章。在远程处理文章之后,我将撰写Web文章。

关注点

远程处理文章之后,我将转向 Web,正如您所见,我到目前为止的所有示例都是在 WinForms 中完成的,但它们仍然可以应用于 Web。我们在 Windows 中使用的相同逻辑也可以在 Web 中使用。Web 中的下一篇文章将是“N层 ASP.NET AJAX 3.5 简介”。听起来很酷。嗯,我的兴趣在于应用程序的设计逻辑,而不是应用程序的修饰,即为了更好的用户体验而提供的图形。这将会到来,但不是现在。

历史

我想借此机会感谢所有给我写电子邮件的人,感谢所有在我遇到困难时回答我编码问题的人。我在非洲,如果世界各地的人们认为我所做的很好并表示赞赏,我会感到很高兴。我在 CodeProject 得到了很多帮助,也帮助了很多人。也许有一天,我会醒来并决定为其他国家的公司编码。但目前,南非对我很好。

© . All rights reserved.