domingo, 30 de março de 2008

[ASP.NET] Componentizando o upload de imagens na camada de negócios

Um problema usual em aplicações ASP.NET é o envio de imagens. Esta necessidade é plenamente suprida pelo componente nativo ASP.NET FileUpload. Tal "WebControl", apresenta um TextBox e um botão "Browse", que encapsula a manipulação da stream da imagem durante o PostBack.

A figura ao lado apresenta-se um aplicação "mock" para envio de imagens. Abaixo segue o código em C# para o delegate do evento OnClick() do botão “Enviar”.

protected void btnEnviar_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
// Recupera dados da imagem
string fileName = FileUpload1.FileName;
string filePath = Page.MapPath("~") + Path.DirectorySeparatorChar +
"images" + Path.DirectorySeparatorChar + fileName;
byte[] data = FileUpload1.FileBytes;
// Escreve a imagem em disco
FileStream writer = new FileStream(filePath, FileMode.Create);
writer.Write(data, 0, data.Length);
writer.Close();
// Apresenta a imagem salva na tela
lblFileName.Text = "Arquivo enviado: " + fileName;
Image1.ImageUrl = "~/images/" + fileName;
Image1.Visible = true;
}
else
{
lblFileName.Text = string.Empty;
Image1.Visible = false;
}
}


No entanto, pode-se criticar esta implementação por quebrar a separação em camadas, uma vez que a lógica de manipulação da imagem está inserida no modelo da camada de apresentação (o arquivo .aspx.cs representa o model MVC). Desta forma se propõe o serviço abaixo a ser encapsulado em um assembly (dll) a parte para fins de reuso.

interface IUploader

using System;
namespace net.jorgealbuquerque.Services.Uploader
{
public interface IUploader
{
string BaseDir { get; set; }
System.Drawing.Size ThurmbnailSize { get; set; }
bool CreateThumbnails { get; set; }
string GetImageDirectory();
void Upload(string fileName, System.IO.Stream data);
void Upload(string fileName, byte[] data);
void Delete(string fileName);
void CreateThumbnail(string fileName);
}
}


classe UploaderService
using System;
using System.IO;
namespace net.jorgealbuquerque.Services.Uploader
{
public class UploaderService: IUploader
{
private bool _CreateThumbnails = true;
public bool CreateThumbnails
{
get { return _CreateThumbnails; }
set { _CreateThumbnails = value; }
}
private Size _ThurmbnailSize = new Size(64, 64);
public Size ThurmbnailSize
{
get { return _ThurmbnailSize; }
set
{
Size size = value;
if (size == null)
throw new ArgumentNullException("O tamanho do thurmbnail não pode ser nulo.");
if (size.Height < 5 || size.Width < 5)
throw new ArgumentException("O tamanho do thurmbnail inválido!");
_ThurmbnailSize = size;
}
}
private string _baseDir = "images";
public string BaseDir
{
get { return _baseDir; }
set
{
string dir = value;
if (string.IsNullOrEmpty(dir))
throw new ArgumentException("Nome de diretório base inválido!");
dir = dir.Replace('/', ' ');
dir = dir.Replace('\\', ' ');
dir = dir.Trim();
_baseDir = dir;
}
}
public string GetImageDirectory()
{
return AppDomain.CurrentDomain.BaseDirectory +
_baseDir + Path.DirectorySeparatorChar;
}
protected void Validate(string fileName)
{
if (string.IsNullOrEmpty(fileName))
throw new ArgumentException("nome de arquivo inválido!");
string ext = Path.GetExtension(fileName).ToLower();
if (ext != ".jpg" && ext != ".gif" &&
ext != ".bmp" && ext != ".png" && ext != ".tif")
throw new ArgumentException("extensão de arquivo inválida!");
string dir = GetImageDirectory();
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
}
public void Upload(string fileName, byte[] data)
{
Validate(fileName);
FileStream writer = new FileStream(GetImageDirectory() + fileName,
FileMode.Create);
writer.Write(data, 0, data.Length);
writer.Close();
if (_CreateThumbnails)
CreateThumbnail(fileName);
}
public void Upload(string fileName, Stream data)
{
byte[] buffer = new byte[data.Length];
data.Write(buffer, 0, (int)data.Length);
Upload(fileName, buffer);
buffer = null;
GC.SuppressFinalize(buffer);
if (_CreateThumbnails)
CreateThumbnail(fileName);
}
public void Delete(string fileName)
{
string filePath = GetImageDirectory() + fileName;
if (File.Exists(filePath))
File.Delete(filePath);
string thumbnailPath = GetImageDirectory() + "thumb_" + fileName;
if (File.Exists(thumbnailPath))
File.Delete(thumbnailPath);
}
public void CreateThumbnail(string fileName)
{
string thumbnailName = "thumb_" + fileName;
string filePath = GetImageDirectory() + fileName;
string thumbnailPath = GetImageDirectory() + thumbnailName;
System.Drawing.Image img = System.Drawing.Image.FromFile(filePath);
System.Drawing.Image thumbnail = img.GetThumbnailImage(
_ThurmbnailSize.Width,
_ThurmbnailSize.Height,
new System.Drawing.Image.GetThumbnailImageAbort(ThumbnailCallback),
IntPtr.Zero);
thumbnail.Save(thumbnailPath);
}
protected bool ThumbnailCallback()
{
return true;
}
}
}

Finalmente, sugere-se uma fábrica concreta que permita um ponto de junção para a evolução do componente:

classe UploaderServiceFactory

using System;
namespace net.jorgealbuquerque.Services.Uploader
{
public class UploaderServiceFactory
{
private UploaderServiceFactory() { }
private static UploaderServiceFactory _instance;
public static UploaderServiceFactory GetInstance()
{
if (_instance == null)
_instance = new UploaderServiceFactory();
return _instance;
}
public IUploader GetUploader()
{
return new UploaderService();
}
}
}



Desta forma, o delegate do evento OnClick() seria reescrito da forma abaixo. Observa-se a separação entre lógica de negócios e apresentação.

protected void btnEnviar_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
UploaderServiceFactory().GetInstance().GetUploader().
Upload(FileUpload1.FileName, FileUpload1.FileBytes);
lblFileName.Text = "Arquivo enviado: " + FileUpload1.FileName;
Image1.ImageUrl = "~/imagens/" + FileUpload1.FileName;
Image1.Visible = true;
}
else
{
lblFileName.Text = string.Empty;
Image1.Visible = false;
}
}

Note ainda que o componente pode ser igualmente empregado em contexto Windows Forms sem qualquer adaptação, ampliando seu potencial de reuso. De forma a tornar mais evidente a separação em camadas obtida pelo componente descrito, propõe-se a persistência do cadastro de imagens. Neste contexto, definiu-se a camada de persistência conforme a figura ao lado através de um componente DataSet "tipado", de acordo com o modelo proposto pela Microsoft. Por sua vez, o serviço da camada de negócios também segue as recomendações da empresa, sendo apresentado abaixo:

using System;
using System.IO;
using System.Collections.Generic;
using System.Web.UI.WebControls;
using System.ComponentModel;
using net.jorgealbuquerque.Sample.Dao;
using net.jorgealbuquerque.Sample.Dao.DataSet1TableAdapters;
using net.jorgealbuquerque.Services.Uploader;
namespace net.jorgealbuquerque.Sample.Services
{
[System.ComponentModel.DataObject]
public class ManterImagesService
{
private static ImageDAO _DAO = null;
protected ImageDAO DAO
{
get
{
if (_DAO == null)
_DAO = new ImageDAO();
return _DAO;
}
}
public void Upload(FileUpload fileUpload, string comentario)
{
UploaderServiceFactory.GetInstance().GetUploader().
Upload(fileUpload.FileName, fileUpload.FileBytes);
Insert(fileUpload.FileName, DateTime.Now, comentario);
}
public void Upload(byte[] imageStream, string nome, string comentario)
{
UploaderServiceFactory.GetInstance().GetUploader().
Upload(nome, imageStream);
Insert(nome, DateTime.Now, comentario);
}
public void Upload(Stream imageStream, string nome, string comentario)
{
UploaderServiceFactory.GetInstance().GetUploader().
Upload(nome, imageStream);
Insert(nome, DateTime.Now, comentario);
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Select, true)]
public DataSetLisier.ImageDataTable GetAll()
{
return DAO.GetAll();
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Select, false)]
public DataSetLisier.ImageDataTable GetByID(int ID)
{
if (ID < -1)
throw new ArgumentException("ID inválido!");
return DAO.GetByID(ID);
}
public DataSetLisier.ImageRow GetRowByID(int ID)
{
DataSetLisier.ImageDataTable tbl = GetByID(ID);
if (tbl.Count != 1)
throw new ArgumentException("Imagem não encontrada!");
return (DataSetLisier.ImageRow)tbl.Rows[0];
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Delete, true)]
public void Delete(int ID)
{
if (ID < 0)
throw new ArgumentException("ID inválido!");
DataSetLisier.ImageRow img = GetRowByID(ID);
if (img == null)
throw new ArgumentException("imagem não cadastrada!");
UploaderServiceFactory.GetInstance().GetUploader().Delete(img.nome);
int linhas = DAO.Delete(ID);
if (linhas != 1)
throw new OperationCanceledException("Erro excluindo imagem!");
}
protected void ValidaDados(string nome, DateTime data, string comentario)
{
if (string.IsNullOrEmpty(nome))
throw new ArgumentException("nome de arquivo inválido!");
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Insert, true)]
public void Insert(string nome, DateTime data, string comentario)
{
ValidaDados(nome, data, comentario);
int linhas = DAO.Insert(nome, data, comentario);
if (linhas != 1)
throw new OperationCanceledException("Erro inserindo imagem!");
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Insert, false)]
public void Insert(DataSetLisier.ImageRow row)
{
if (row == null)
throw new OperationCanceledException("Imagem não pode ser nula!");
Insert(row.nome, row.data, row.comentario);
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Update, true)]
public void Update(string nome, DateTime data, string comentario, int ID)
{
// Regra de negocio: nome e data da imagem
// nao podem ser alterados pela interface!
DataSetLisier.ImageRow row = GetRowByID(ID);
if (row == null)
throw new OperationCanceledException("Imagem não cadastrada!");
int linhas = DAO.Update(row.nome, row.data, comentario, ID);
if (linhas != 1)
throw new OperationCanceledException("Erro alterando imagem!");
}
[System.ComponentModel.DataObjectMethod(DataObjectMethodType.Update, false)]
public void Update(DataSetLisier.ImageRow row)
{
if (row == null)
throw new OperationCanceledException("A imagem não pode ser nula!");
ValidaDados(row.nome, row.data, row.comentario);
int linhas = DAO.Update(row);
if (linhas != 1)
throw new OperationCanceledException("Erro alterando imagem!");
}
}
}

Vale resaltar novamente que a classe de serviço descrita acima deve ser encapsulada em um assembly (dll) a parte, sendo plenamente compatível com aplicações Windows Forms, o que demonstra a separação em camadas. Desta, pode-se evoluir o mock inicial para a pequena aplicação abaixo.



Onde novo delegate do botão "Enviar" é apresentado abaixo, onde fica ainda mais evidente a separação em camadas entre as lógicas de apresentação, negócios e persistência:


protected void btnEnviar_Click(object sender, EventArgs e)
{
new ManterImagesService().Upload(FileUploader1, txtComentario.Text);
ImageGridView.DataBind();
}

Um comentário:

.Net disse...

Teria como disponibilizar para gente o projeto deste Post, afim de estudos e implementações.