1、在ASP.NET 2.0中操作数据之五十四:添加新记录时包含一个文件上传选项 作者:heker2007 字体:[增加 减小] 类型:转载 时间:2016-05-17 我要评论 上篇文章主要介绍了,ASP.NET 2.0中如何显示二进制数据,本文主要介绍如何将图片上传,转换成二进制数据保存在数据库中。 导言: 在前面2节教程,我们探讨了如何使用FileUpload控件从客户端向服务器上传文件,以及如何在数据Web控件里显示二进制数据。 在本节,我们将创建一个web页面以添加新的种类。除了为类的name和description属性添加TextBoxes控件外,我们还要在页面上添加
2、2个FileUpload控件——一个用来上传新类的图片,另一个用来上传类的小说明册子。上传的图片将直接存储在新记录的Picture列。与此相反,小册子将存储在~/Brochures 文件夹,同时将文件路径存储在新记录的BrochurePath列。 在创建页面之前,我们需要更新体系结构。由于CategoriesTableAdapter的主查询并不返回Picture列,因此自动生产的Insert方法只包含了CategoryName, Description和BrochurePath列。我们需要在TableAdapter里创建新的方法以包括Categories的4个列。同时业务逻辑层的的Cat
3、egoriesBLL类也需要更新。 第1步:在CategoriesTableAdapter添加一个InsertWithPicture方法 在前面的教程《创建一个数据访问层》里我们创建了CategoriesTableAdapter,并设置其自动生成了基于主查询的INSERT, UPDATE和DELETE命令。此外,我们设置该TableAdapter启用DB Direct方法,它将创建Insert, Update和Delete方法。这些方法执行自动生成的INSERT, UPDATE和DELETE命令,自然而然的,其接受的输入参数基于主查询所返回的那些列。在教程《使用FileUpload上传
4、文件》里,我们扩展了 CategoriesTableAdapter的主查询以包含BrochurePath列。 因为CategoriesTableAdapter的主查询并为引用Picture,在添加新记录或更新记录时不能涉及Picture值。为了获取Picture信息,我们要么在TableAdapter里创建一个新方法以插入Picture的二进制数据;要么定制自动生成的INSERT命令。但定制自动生成的INSERT命令有一个风险,即定制的INSERT命令有可能被向导覆盖。比如,假设我们定制INSERT命令使用Picture列,更新TableAdapter的Insert方法,使之多包含一个对
5、应picture二进制数据的参数。然后在业务逻辑层创建一个方法使用该 DAL方法,再在表现层调用该业务逻辑层方法。现在一切工作正常,但当下一次在TableAdapter设置向导里设置TableAdapter完成后,我们定制的INSERT命令马上就会被向导重写,回归到定制前的状态。其结果是我们的代码将无法编译! 注意:如果使用存储过程而不用SQL语句的话,就不存在这个问题。在以后的教程里,我们将探讨在数据访问层用存储过程替代SQL语句。 为避免这个头痛的问题,我们为TableAdapter添加新的方法,而不定制自动生成的SQL命令。我们为添加的方法命名为InsertWithPictu
6、re,它接受 CategoryName, Description, BrochurePath和Picture值;执行INSERT命令将上述值添加进一条记录。 在CategoriesTableAdapter的顶部点右键,选择“添加查询”。进入TableAdapter 查询设置向导,首先询问我们TableAdapter查询如何访问数据库,选择“使用SQL语句”,点Next,因为我们要为表Categories添加新记录,选“INSERT”,点Next。 图1:选“INSERT”选项 现在,我们需要指定INSERT SQL语句。向导自动地生成一个基于主查询的INSERT语句。此时,它
7、只插入CategoryName, Description和BrochurePath值。对其更新,包括Picture列和参数@Picture ,如下: ? 1 2 3 4 INSERT INTO [Categories] ([CategoryName], [Description], [BrochurePath], [Picture]) VALUES (@CategoryName, @Description, @BrochurePath, @Picture) 最后,向导要我们为方法命名,取名为InsertWithPicture,点Finish。 图2:为新方法命名为
8、InsertWithPicture 第2步:更新业务逻辑层 由于一般来说表现层将引用业务逻辑层,而不是绕过它直接引用数据访问层,我们需要创建一个业务逻辑层方法,以调用刚才创建的数据访问层方法(InsertWithPicture),本节,我们在CategoriesBLL里创建一个名为InsertWithPicture方法,它接受3个字符串和一个byte数组,字符串参数对应name, description和brochure文件地址;byte数组对应于图片的二进制内容。就像下面的代码所显示的那样,BLL方法调用相应DAL方法: ? 1 2 3 4 5 6 7 [Syst
9、em.ComponentModel.DataObjectMethodAttribute (System.ComponentModel.DataObjectMethodType.Insert, false)] public void InsertWithPicture(string categoryName, string description, string brochurePath, byte[] picture) { Adapter.InsertWithPicture(categoryName, description, brochurePath, picture);
10、} 注意:在为BLL添加InsertWithPicture方法前,确保已经保存了数据集(Typed DataSet ),因为CategoriesTableAdapter类的代码是基于Typed DataSet自动生成的。如果最开始没有把对Typed DataSet所进行的修改保存的话,Adapter属性将不认同InsertWithPicture方法。 第3步:列出现有的种类及其二进制数据 本教程我们将创建一个页面,允许用户添加新的类,包含其图片和说明小册子。在上一节,我们用一个包含TemplateField和ImageField的GridView控件来展示每个类的名称、描述,并包
11、含一个下载说明小册子的链接。在本教程,我们实现相同的功能,创建一个页面,即展示现有的类,还可以添加新的类。
打开BinaryData文件夹的DisplayOrDownload.aspx页面,切换到源模式,复制GridView和ObjectDataSource控件的声明代码,粘贴在UploadInDetailsView.aspx页面的
12、ploadInDetailsView.aspx 完成以后,在浏览器里查看该页面,确保一切正常。GridView控件里列出了8个类,每个类包含一张图片以及一个下载说明小册子的链接。 图4:你应该看到每个类及其相应二进制数据 第4步:设置CategoriesDataSource以支持添加功能 那个ID为Categories的GridView控件所使用的名为CategoriesDataSource的 ObjectDataSource控件目前还不支持添加数据。为实现该功能,我们要设置该控件的Insert方法引用类CategoriesBLL的某个方法。具体的讲,我们要用到在第2步里添
13、加的InsertWithPicture方法。 在ObjectDataSource控件的智能标签里,点“设置数据源”。照原样一直点到“Define Data Methods”界面。再点INSERT标签,从下拉列表里选方法“InsertWithPicture”,点Finish完成设置。 图5:设置ObjectDataSource控件使用InsertWithPicture方法 注意:当完成设置后,Visual Studio会问你是否“刷新Fields and Keys”,选择No,因为如果选Yes的话,将重新构造data Web controls fields,那样将重写所有我们已
14、经定制好的列(field)。
完成设置后,ObjectDataSource控件将会为InsertMethod属性赋值,同时包含一个 15、nsertMethod="InsertWithPicture">
16、ataSource>
第5步:创建一个插入界面
在教程16《概述插入、更新和删除数据》里我们谈到,当DetailsView控件的数据源控件支持添加功能时,便可以启用DetailsView内置的添加界面。让我们在页面上添加一个DetailsView控件,置于GridView控件之上,并处于添加模式。当在DetailsView控件里添加一个新种类时,其下的GridView控件将自动发生刷新,并将刚添加的类显示出来。
从工具箱拖一个DetailsView控件到页面,置于GridView之上,设其ID为NewCategory,清空其Height和Width属性。 其智能标签里,设置它绑定 17、到名为CategoriesDataSource的数据源,并启用“插入”功能。
图6:将DetailsView控件绑定到CategoriesDataSource,并启用插入功能。
为使DetailsView呈现为插入界面,设其DefaultMode属性为Insert
我们注意到,尽管DetailsView控件有5个BoundFields——CategoryID, CategoryName, Description, NumberOfProducts和BrochurePath,但插入界面并不包含CategoryID,因为CategoryID列的InsertVisible属性为fa 18、lse。为什么会显示这4个列呢?因为ObjectDataSource调用的GetCategories()方法返回的就是这些列。当添加新类时,我们不希望用户为NumberOfProducts列指定值,此外,我们还希望让用户为新类上传图片和相关的PDF小册子。
在DetailsView里将NumberOfProducts列完成删除,再分别CategoryName列和BrochurePath列的HeaderText属性设置为“Category”和“Brochure”。将BrochurePath 转换为TemplateField,再添加一个TemplateField,设其HeaderText属性 19、为“Picture”,把它放置在BrochurePath列和CommandField列之间。
图7:将DetailsView控件绑定到CategoriesDataSource,并启用插入功能(注:图片说明有误)
当你在“编辑列”对话框里将BrochurePath BoundField 转换为一个TemplateField后,该TemplateField将包含3个模板:ItemTemplate,EditItemTemplate和InsertItemTemplate,由于我们只需要InsertItemTemplate模板,将另外2个模板删除。如此,你的DetailsView控件的声明代 20、码看起来应该像下面的这样:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22、tion" />
23、p:TemplateField>
24、lsView控件的智能标签选择“编辑模板”,从下拉列表选择BrochurePath TemplateField的InsertItemTemplate模板,将模板里的TextBox删除,从工具箱拖一个FileUpload控件到页面,设其ID为BrochureUpload。类似的,为Picture TemplateField的InsertItemTemplate模板添加一个ID为PictureUpload的FileUpload控件。
图8:在InsertItemTemplate模板里添加一个FileUpload控件
完成添加后,这2个TemplateField的声明代码应该和下面的差不多: 25、
?
1
2
3
4
5
6
7
8
9
10
26、 27、ContentType将这些文件发送到客户端。由于表Categories现在并没有这样的列,我们只有限制用户上传指定为某种类型的image文件。表Categories里现有的images为位图,不过使用JPG类型或许更恰当。
当用户上传的文件类型不正确时,我们将取消插入操作,并显示一个提示信息。在DetailsView控件下添加一个Label Web控件,设ID为UploadWarning,清除Text属性,设CssClass属性为“Warning”, 再将Visible和EnableViewState属性都设为false。Warning CSS定义在Styles.css里,作用是将文 28、字显示为粗斜体,红色大号字。
注意:最理想的情况是将CategoryName和Description BoundFields都转换为TemplateFields,达到定制插入界面的目的。比如,对Description插入界面来说,使用一个允许分行的文本框或许更好;对CategoryName插入界面,因为CategoryName不允许为NULL值,我们应该添加一个RequiredFieldValidator控件,以确保输入类的名称。这些步骤都留给读者做练习,更深入的探讨请参考前面的教程之20《定制数据修改界面》
第6步:将上传的小册子保存在服务器的文件系统
但用户键入相关的类别信息 29、点Insert按钮后,发生页面回传,接着发生一连串的插入流程。首先,DetailsView控件的ItemInserting event事件发生;接着,调用ObjectDataSource控件的Insert()方法,它将导致Categories表添加新记录;最后,发生DetailsView控件的ItemInserted event事件。
在调用ObjectDataSource控件的Insert()方法以前,我们必须确保用户已经上传了恰当的文件并保存在服务器的文件系统。为此,我们为DetailsView控件的ItemInserting事件创建一个事件处理器,添加如下的代码:
?
1
30、2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Reference the FileUpload control
FileUpload BrochureUpload =
(FileUpload)NewCategory.FindControl("BrochureUpload");
if (BrochureUpload.HasFile)
{
// Make sure that a PDF has been uploaded
if (string.Compare(System.IO.Path.GetExte 31、nsion
(BrochureUpload.FileName), ".pdf", true) != 0)
{
UploadWarning.Text =
"Only PDF documents may be used for a category's brochure.";
UploadWarning.Visible = true;
e.Cancel = true;
return;
}
}
代码首先引用DetailsView控件模板里名为BrochureUpload的FileUpload控件,如果已经上传了文件,就检查FileUpload控件的exten 32、sion是否为“.PDF”, 如果不是则取消插入操作并退出。
注意:通过检查文件的扩展名(extension)来确保用户上传的为PDF文件的做法并不是万全之策。比如,可能用户的确上传的是PDF文件,只不过其扩展名为.Brochure;或者用户提供的并不是PDF文件,却使用.pdf的扩展名。保险的做法是通过编程对文件内容做最后一次检查。如此一来,虽然彻底,但稍嫌过头(overkill)。在绝大多数情况下,检查文件扩展名就已经足够了。
就像在教程《使用FileUpload上传文件》里讨论的那样,将文件保存在文件系统里时要特别小心,以免覆盖别人上传的文件。本节,我们尝试对上传文件使用一个 33、已经使用的名字,在名字末尾添加一个数字,以示区别。举例,如果在文件夹~/Brochures里存在一个名为Meats.pdf的文件,上传文件时我们取名为Meats-1.pdf,如果文件夹里恰好也存在一个Meats-1.pdf文件,我们就取名为Meats-2.pdf,以此类推,直到文件名唯一为止。
下面的代码使用File.Exists(path)方法来判断是否已经存在同名文件,如果存在,就重新命名,直到名字唯一为止:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
const string BrochureDirectory = "~/Brochur 34、es/";
string brochurePath = BrochureDirectory + BrochureUpload.FileName;
string fileNameWithoutExtension =
System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName);
int iteration = 1;
while (System.IO.File.Exists(Server.MapPath(brochurePath)))
{
brochurePath = string.Concat 35、BrochureDirectory,
fileNameWithoutExtension, "-", iteration, ".pdf");
iteration++;
}
一旦找到唯一的文件名后,立即将文件保存在文件系统,同时更新ObjectDataSource控件的InsertParameter参数brochurePath的值,以便将文件名写入数据库。就像在教程《使用FileUpload上传文件》里看到的一样,可以使用FileUpload控件的SaveAs(path)方法来保存文件。使用e.Values集合来更新ObjectDataSource控件的参数brochurePat 36、h。
?
1
2
3
// Save the file to disk and set the value of the brochurePath parameter
BrochureUpload.SaveAs(Server.MapPath(brochurePath));
e.Values["brochurePath"] = brochurePath;
第7步:将上传的图片保存到数据库
为了把上传的图片保存在新添加的记录里,我们需要在DetailsView控件的ItemInserting事件里,用上传的数据对ObjectDataSource控件的picture参数赋值。然 37、而,在此之前,我们需要确保上传的文件为JPG而不是其它的什么格式。就象在第6步中探讨的一样,我们用文件的扩展名来检查其类型。
虽然Categories表允许Picture列为NULL值,但所有的种类都应该有一张图片。在本页面,我们强制用户添加记录时提供图片。下面的代码确保已经上传图片,且为恰当的类型。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Reference the FileUpload controls
FileUplo 38、ad PictureUpload = (FileUpload)NewCategory.FindControl("PictureUpload");
if (PictureUpload.HasFile)
{
// Make sure that a JPG has been uploaded
if (string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName),
".jpg", true) != 0 &&
string.Compare(System.IO.Path.GetExtension(PictureU 39、pload.FileName),
".jpeg", true) != 0)
{
UploadWarning.Text =
"Only JPG documents may be used for a category's picture.";
UploadWarning.Visible = true;
e.Cancel = true;
return;
}
}
else
{
// No picture uploaded!
UploadWarning.Text =
"You must provide a picture for the new c 40、ategory.";
UploadWarning.Visible = true;
e.Cancel = true;
return;
}
这些代码应放在第6步中的代码前面,如果上传的文件有问题,事件处理器在文件保存到文件系统前就结束了。
假设上传的文件没有问题,然后我们用下面的代码将上传文件的数据分配给参数picture:
?
1
2
// Set the value of the picture parameter
e.Values["picture"] = PictureUpload.FileBytes;
完整的ItemInserting事件处理器
下面是It 41、emInserting事件处理器的完整代码:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected 42、void NewCategory_ItemInserting(object sender, DetailsViewInsertEventArgs e)
{
// Reference the FileUpload controls
FileUpload PictureUpload = (FileUpload)NewCategory.FindControl("PictureUpload");
if (PictureUpload.HasFile)
{
// Make sure that a JPG has been uploaded
if (string.Compare(S 43、ystem.IO.Path.GetExtension(PictureUpload.FileName),
".jpg", true) != 0 &&
string.Compare(System.IO.Path.GetExtension(PictureUpload.FileName),
".jpeg", true) != 0)
{
UploadWarning.Text =
"Only JPG documents may be used for a category's picture.";
UploadWarning.Visible = true;
e 44、Cancel = true;
return;
}
}
else
{
// No picture uploaded!
UploadWarning.Text =
"You must provide a picture for the new category.";
UploadWarning.Visible = true;
e.Cancel = true;
return;
}
// Set the value of the picture parameter
e.Values["picture"] = PictureUpload. 45、FileBytes;
// Reference the FileUpload controls
FileUpload BrochureUpload =
(FileUpload)NewCategory.FindControl("BrochureUpload");
if (BrochureUpload.HasFile)
{
// Make sure that a PDF has been uploaded
if (string.Compare(System.IO.Path.GetExtension(BrochureUpload.FileName),
46、 ".pdf", true) != 0)
{
UploadWarning.Text =
"Only PDF documents may be used for a category's brochure.";
UploadWarning.Visible = true;
e.Cancel = true;
return;
}
const string BrochureDirectory = "~/Brochures/";
string brochurePath = BrochureDirectory + BrochureUpload.FileNa 47、me;
string fileNameWithoutExtension =
System.IO.Path.GetFileNameWithoutExtension(BrochureUpload.FileName);
int iteration = 1;
while (System.IO.File.Exists(Server.MapPath(brochurePath)))
{
brochurePath = string.Concat(BrochureDirectory, fileNameWithoutExtension,
"-", iteration, 48、 ".pdf");
iteration++;
}
// Save the file to disk and set the value of the brochurePath parameter
BrochureUpload.SaveAs(Server.MapPath(brochurePath));
e.Values["brochurePath"] = brochurePath;
}
}
第8步:更新DisplayCategoryPicture.aspx页面
让我们花几分钟测试我们在上几步创建的插入界面和ItemInserting事件处理器。在浏览器查 49、看UploadInDetailsView.aspx页面 ,尝试添加一个类,忽略picture或指定一个非JPG的图片或非PDF的小册子。以上任何一种情况下,都会显示一个错误信息,并取消插入操作。
图9:当上传的文件不对时将显示一个警告信息
确认页面要求上传一张图片,且不接受非PDF或非JPG文件。添加一个包含JPG格式图片的新类别,将Brochure列置空,点击Insert按钮后,页面回传,将为Categories表添加一个新记录,同时上传的图片数据直接存储进数据库。GridView控件更新后,将新添加的类显示出来。但是,就像图10所示的那样,类的图片没有正确的显示出来。
图 50、10:新类的图片没有显示出来
图片没有显示出来的原因是因为用来返回特定类的图片的页面DisplayCategoryPicture.aspx被设置为处理带OLE报头的位图。当Picture列的数据被返回到客户端前已经把那78字节的报头剥离掉。而且上传的JPG文件并没有OLE报头,因此,必需的字节已经从图片的二进制数据移除了。
由于现在表Categories里既有JPG文件又有带OLE报头的位图,我们需要对页面DisplayCategoryPicture.aspx做调整,使它对原来的8个类剥离OLE报头,而不对新添加的类进行剥离。在后面的教程,我们探讨如何更新现有记录的image文件,






