
Загрузка изображений в базу через FileUpload и вывод через HttpHandler
Фото сотрудника или товара можно хранить прямо в базе и показывать в GridView без единого файла на диске. Собираем цепочку: FileUpload, массив байтов, SQL и HttpHandler.
План решения
Хранить изображения в файловой системе привычно, но это добавляет хлопот: права на папку, синхронизация между серверами фермы, осиротевшие файлы после удаления записей. Для небольших картинок вроде аватаров и фото сотрудников удобнее держать данные прямо в таблице. Тогда резервная копия базы автоматически включает и изображения.
Обратная сторона тоже есть: база растёт, а каждая картинка проходит через приложение, а не отдаётся веб-сервером напрямую. Поэтому подход хорош для изображений размером в десятки и сотни килобайт и для сотен или тысяч записей, но не для фотобанка.
В примере используется демонстрационная база Northwind, таблица Employees с полем Photo. Цепочка выглядит так:
- FileUpload в строке GridView принимает файл от пользователя.
- Содержимое читается в массив байтов, на диск ничего не пишется.
- Массив сохраняется в базу параметризованным UPDATE.
- В колонке GridView стоит Image, ссылающийся на обработчик .ashx с идентификатором.
- HttpHandler достаёт байты из базы и отдаёт их браузеру с нужным Content-Type.
Разметка GridView
В шаблонных колонках размещены картинка и форма загрузки. DataKeyNames хранит первичный ключ каждой строки, так что в обработчике события не придётся прятать идентификатор в скрытые поля. Кнопка передаёт номер строки через CommandArgument, а CommandName позволяет отличить её команду от других кнопок таблицы.
Важно: загрузка файлов работает только при полноценном PostBack. Если GridView лежит внутри UpdatePanel, кнопку нужно зарегистрировать как PostBackTrigger, иначе PostedFile на сервере окажется пустым.
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false"
DataKeyNames="EmployeeID">
<Columns>
<asp:BoundField DataField="LastName" HeaderText="Фамилия" />
<asp:BoundField DataField="FirstName" HeaderText="Имя" />
<asp:TemplateField HeaderText="Фото">
<ItemTemplate>
<asp:Image runat="server" Width="80"
AlternateText='<%# Eval("LastName") %>'
ImageUrl='<%# "~/DbImage.ashx?id=" & Eval("EmployeeID") %>' />
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField>
<ItemTemplate>
<asp:FileUpload ID="fuPhoto" runat="server" accept="image/*" />
<asp:Button runat="server" Text="Загрузить" CommandName="UPLOAD"
CommandArgument='<%# Container.DataItemIndex %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>Чтение файла и сохранение в базу
В событии RowCommand по номеру строки находим FileUpload через FindControl, а идентификатор берём из DataKeys. Свойство FileBytes возвращает содержимое файла сразу массивом, вручную читать InputStream не нужно. Заодно сохраняем тип содержимого: без него обработчик не узнает, отдавать ли JPEG или PNG.
Перед сохранением обязательно проверяем файл: размер, разрешённые типы и то, что это действительно изображение. Доверять расширению и ContentType из запроса нельзя, их подставляет клиент. Надёжная проверка: попробовать открыть данные через System.Drawing.Image.FromStream.
Protected Sub GridView1_RowCommand(sender As Object, e As GridViewCommandEventArgs) _
Handles GridView1.RowCommand
If e.CommandName <> "UPLOAD" Then Return
Dim rowIndex = Convert.ToInt32(e.CommandArgument)
Dim fu = CType(GridView1.Rows(rowIndex).FindControl("fuPhoto"), FileUpload)
Dim id = CInt(GridView1.DataKeys(rowIndex).Value)
If Not fu.HasFile OrElse fu.FileBytes.Length > 512 * 1024 Then
lblMsg.Text = "Выберите изображение до 512 КБ" : Return
End If
Const sql = "UPDATE Employees SET Photo = @Photo, PhotoType = @Type " &
"WHERE EmployeeID = @Id"
Using cn As New SqlConnection(ConnStr), cmd As New SqlCommand(sql, cn)
cmd.Parameters.Add("@Photo", SqlDbType.VarBinary, -1).Value = fu.FileBytes
cmd.Parameters.Add("@Type", SqlDbType.NVarChar, 50).Value = fu.PostedFile.ContentType
cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id
cn.Open()
cmd.ExecuteNonQuery()
End Using
LoadGrid() ' перечитываем таблицу, чтобы показать новое фото
End SubHttpHandler для вывода картинки
Страница .aspx для отдачи картинки избыточна: у неё есть жизненный цикл, ViewState и рендеринг разметки. Обработчик .ashx реализует интерфейс IHttpHandler всего с одним методом ProcessRequest и отдаёт байты напрямую в поток ответа. Это быстрее и проще.
Обработчик берёт идентификатор из строки запроса, проверяет, что это число, читает данные и пишет их через BinaryWrite. Если фото нет, лучше вернуть 404 или картинку-заглушку, чем пустой ответ: браузер тогда покажет значок битого изображения.
<%@ WebHandler Language="VB" Class="DbImage" %>
Imports System
Imports System.Data
Imports System.Web
Imports System.Data.SqlClient
Public Class DbImage : Implements IHttpHandler
Public Sub ProcessRequest(ctx As HttpContext) Implements IHttpHandler.ProcessRequest
Dim id As Integer
If Not Integer.TryParse(ctx.Request.QueryString("id"), id) Then
ctx.Response.StatusCode = 400 : Return
End If
Using cn As New SqlConnection(Config.ConnStr), _
cmd As New SqlCommand("SELECT Photo, PhotoType FROM Employees WHERE EmployeeID=@Id", cn)
cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id
cn.Open()
Using rd = cmd.ExecuteReader()
If Not rd.Read() OrElse rd.IsDBNull(0) Then
ctx.Response.StatusCode = 404 : Return
End If
ctx.Response.ContentType = If(rd.IsDBNull(1), "image/jpeg", rd.GetString(1))
ctx.Response.Cache.SetCacheability(HttpCacheability.Public)
ctx.Response.Cache.SetMaxAge(TimeSpan.FromHours(1))
ctx.Response.BinaryWrite(CType(rd(0), Byte()))
End Using
End Using
End Sub
Public ReadOnly Property IsReusable As Boolean Implements IHttpHandler.IsReusable
Get
Return True
End Get
End Property
End ClassПодводные камни
Фото в самой Northwind хранятся в старом формате OLE: перед данными картинки идёт заголовок длиной 78 байт. Если вывести их как есть, браузер покажет битое изображение. Для демонстрационной базы этот заголовок приходится отрезать, а для своих загрузок такой проблемы нет.
Ещё один момент касается размера страницы. Каждая картинка в GridView это отдельный запрос к обработчику и отдельное обращение к базе. На таблице в сотню строк это ощутимо, поэтому кэширование в браузере, как в примере, и разумный размер страницы (пейджинг) здесь обязательны. Большие фотографии стоит уменьшать при загрузке, а не масштабировать атрибутом Width.
- Ограничьте maxRequestLength в web.config, иначе большой файл даст ошибку до вашего кода.
- Не храните в базе файлы размером в десятки мегабайт, для них есть FILESTREAM или файловое хранилище.
- Добавляйте alt к картинкам: это нужно и для доступности, и для случаев, когда фото нет.
Как это делают сегодня
Вопросы и ответы
Почему PostedFile пустой после нажатия кнопки?
Скорее всего, GridView находится внутри UpdatePanel, а асинхронный запрос не передаёт файлы. Зарегистрируйте кнопку как PostBackTrigger.
Почему фото из Northwind не отображаются?
В них есть 78-байтный OLE-заголовок от старого Access. Пропустите первые 78 байт при выводе, для собственных загрузок это не требуется.
Что выбрать: хранить картинки в базе или на диске?
Небольшие изображения удобно держать в базе ради целостности и бэкапов. Крупные файлы и большие объёмы лучше выносить в файловое или объектное хранилище.