O controlo LAFileUpload
O upload de ficheiros foi (e continua a ser) uma das funcionalidades mais requisitadas pelos programadores de ASP.NET. Apesar de a plataforma oferecer alguns controlos que permitem efectuar este tipo de operações, a verdade é que este tipo de elementos deixa muito a desejar já que, como é do conhecimento geral, os controlos limitam-se a encapsular os seus congéneres HTML que não fornecem nenhum tipo de feedback ao utilizador.
Ao longo deste artigo, vamos ilustrar os principais passos associados à construção de um controlo capaz de efectuar o upload de um ficheiro e fornecer feedback ao utilizador em relação à quantidade de informação transferida entre o cliente e o servidor. O interface do controlo (constituído por uma barra de progresso) foi construído à custa de um UserControl Windows Forms. A utilização de um componente windows forms oferece vantagens e desvantagens: por um lado, a utilização de um controlo de windows forms permite-nos construir facilmente um interface rico; por outro, obriga a que o cliente utilize exclusivamente o Internet Explorer, já que este tipo de elementos apenas pode ser carregado através do IE.
A construção deste controlo permitir-nos-á analisar detalhadamente algumas das novas funcionalidades disponibilizadas pela nova versão da plataforma aos construtores de controlos.
Construção do UserControl de Windows Forms
O UserControl apresenta uma barra de progresso e é responsável pelo upload e actualização da barra. O controlo permite definir vários aspectos importantes relacionados com as funcionalidades disponibilizadas:
- Definir o caminho até ao ficheiro que deve ser transferido do cliente para o servidor.
- Indicar o nome do ficheiro transferido.
- Indicar a pasta do servidor onde o ficheiro deve ser colocado.
- Definir o tamanho de cada pacote enviado do cliente para o servidor.
Para além disso, deve ainda ser possível notificar o utilizador acerca de algumas alterações efectuadas sobre o componente em runtime:
- Início e fim de upload.
- Alteração dos valores atribuídos ás propriedades.
A utilização de eventos torna o nosso controlo um pouco mais complexo. Nos velhos tempos do COM/ActiveX, os eventos eram definidos através de source interfaces. Estas interfaces definiam os contratos que permitiam a comunicação entre dois componentes. Tipicamente, o componente cliente implementava a interface e registava-se junto ao componente servidor através do método Advise da interface IConnectionPoint (note-se que o objecto servidor tinha de implementar este interface). A definição de eventos em .NET é completamente diferente do que acontecia no mundo COM, já que em .NET este tipo de elementos é definido à custa de delegates.
Infelizmente para nós, o IE continua a necessitar das interfaces COM para gerar eventos que possam ser consumidos por eventuais scripts que usam o controlo colocado na página. Logo, vamos ter de recorrer a COM interop para construir um interface que representa cada evento gerado através de um método. Para além da interface que define os eventos gerados pelo componente, vamos aproveitar para definir também uma interface que indica as operações que podem ser efectuadas sobre o nosso UserControl.
Após criarmos um novo projecto do tipo Class Library, começamos por adicionar um novo UserControl designado de DummyCtl. Em seguida, vamos redimensionar a área do nosso controlo e adicionar um controlo ProgressBar que será responsável por fornecer feedback acerca do upload efectuado e vamos configurá-lo de forma a que este ocupe toda a área do UserControl (atribuir o valor Fill à propriedade Dock).

Em seguida, vamos construir as duas interfaces que definem as operações que podem ser efectuadas pelo controlo e os eventos gerados por este:
[
Guid("13EDD5C5-6464-45e0-8ACE-9412E9AD17AE"),
InterfaceType( ComInterfaceType.InterfaceIsIDispatch )
]
public interface IProgressBarEvents
{
[DispId(0x60020000)]
void UploadComplete();
[DispId(0x60020001)]
void BeginUpload();
[DispId(0x60020002)]
void DestinationFileNameChanged();
[DispId(0x60020003)]
void FileToUploadChanged();
[DispId(0x60020004)]
void DestinationFolderChanged();
[DispId(0x60020005)]
void FilePacketSizeChanged();
}
public interface IProgressBarCtl
{
string FileToUpload
{
get;set;
}
string DestinationFileName
{
get;set;
}
string DestinationFolder
{
get;set;
}
int FilePacketSize
{
get;set;
}
void UploadFile();
}
A primeiro interface (IProgressBarEvents) define os eventos gerados pela nossa classe (note-se como os eventos são definidos através de métodos); por sua vez, a segunda define as propriedades e métodos expostos pelo nosso controlo. A primeira interface é anotada com os atributos GUID (permite definir explicitamente o GUID associado a esta interface COM) e InterfaceType (serve para indicar o tipo de interface COM que está a ser construída - neste caso, estamos a falar de uma dispinterface que permite efectuar apenas late binding; note-se que as interfaces associados a eventos COM devem ser sempre deste tipo). Ao contrário da primeira interface, a segunda não recorre a nenhum atributo COM para definir o tipo de interface ou o GUID.
Como é possível verificar, cada evento é representado por um método. Para despoletar um evento, a classe .NET tem apenas de disponibilizar eventos com os mesmos nomes e com delegates que possuem a mesma assinatura que os métodos definidos na interface. Neste caso, o nosso delegate tem de ter a seguinte assinatura:
public delegate void UploadHandler();
Apesar do nome não ser o mais correcto, a assinatura é adequada a todos os eventos que o nosso UserControl deve gerar. Agora que já temos os tipos definidos, falta apenas definir os eventos da classe:
public class ProgressBarCtl : System.Windows.Forms.UserControl, IProgressBarCtl
{
public event UploadHandler BeginUpload;
protected void OnBeginUpload()
{
if( this.InvokeRequired )
{
this.Invoke( new UploadHandler( OnBeginUpload ), null );
}
else
{
if( this.BeginUpload != null )
{
new SecurityPermission( SecurityPermissionFlag.UnmanagedCode ).Assert();
BeginUpload();
CodeAccessPermission.RevertAssert();
}
}
}
//outros eventos não apresentados, mas com codigo semelhante
}
Tal como é sugerido, o evento é sempre gerado por um método do tipo OnNomeEvento. Neste caso, temos de garantir que o nosso método é capaz de chamar unmanaged code (como o nosso componente será colocado numa página HTML, é normal ser tratado por código javascript que, do ponto de vista da plataforma, representa unmanaged code): daí a necessidade de efectuar um Assert de forma a parar o stack walk efectuado para garantir que o nosso componente é capaz de notificar eventuais interessados(mais informações nos parágrafos seguintes).
O código responsável pelo upload de ficheiros foi escrito à la .NET 1.1 (a sua melhoria para a versão 2.0 fica a cargo do leitor). A ideia é lançar uma thread secundária que efectua a cópia do ficheiro da máquina cliente para o servidor. O código usado foi o seguinte:
public void UploadFile( )
{
try
{
//check values...
if( this.FileToUpload == null || this.FileToUpload.Length == 0 )
{
throw new ApplicationException( "FileToUpload não pode ser nulo ou vazio" );
}
if( this.DestinationFileName == null || this.DestinationFileName.Length == 0 )
{
throw new ApplicationException( "DestinationFileName não pode ser nulo ou vazio" );
}
if( this.DestinationFolder == null || this.DestinationFolder.Length == 0 )
{
throw new ApplicationException( "DestinationFolder não pode ser nulo ou vazio" );
}
Thread uploadThread = new Thread( new ThreadStart( StartUploadFile ) );
uploadThread.Start();
}
catch( Exception ex )
{
MessageBox.Show( ex.ToString() );
}
}
private void StartUploadFile()
{
try
{
string httpDestination = this.DestinationFolder + "/" + this.DestinationFileName;
WebClient webClient = new WebClient( );
webClient.Credentials = CredentialCache.DefaultCredentials;
using( Stream stream = webClient.OpenWrite( httpDestination, "PUT" ) )
{
//gerar evento de inicio de upload
OnBeginUpload();
//efectuar assert da stack walk
new FileIOPermission( PermissionState.Unrestricted ).Assert( );
using( FileStream fileStream = File.OpenRead( this.FileToUpload ) )
{
int counter = 0;
long fileSize = fileStream.Length;
long packetSize = fileSize < ( long )this.FilePacketSize ? fileSize : ( long )this.FilePacketSize;
while( counter < fileSize )
{
byte [] bytes = new byte [packetSize];
int readBytes = fileStream.Read( bytes, 0, Convert.ToInt32( packetSize ) );
stream.Write( bytes, 0, bytes.Length );
counter += readBytes;
UpdateProgress( (float)counter / (float)fileSize * 95.0f );
}
}
//reverter assert
CodeAccessPermission.RevertAssert( );
}
UpdateProgress( 100 );
OnUploadComplete();
}
catch( Exception ex )
{
MessageBox.Show( ex.ToString() );
}
}
A utilização de asserts é novamente necessária já que, se deixarmos a plataforma efectuar a stack walk normal, iremos deparar-nos com falta de permissões para efectuar esta operação (por predefinição, todos os componentes utilizados no IE não podem aceder directamente ao disco). No final do upload, revertemos o assert para garantir o normal funcionamento das stack walks (a secção Segurança 101 apresenta as noções básicas acerca do hosting de controlos no IE).
Utilização de UserControl de windows forms em páginas apresentadas pelo IE
Tal como acontece com os ActiveX, um UserControl de windows forms é embebido numa página através da tag <object>. A diferença reside no valor atribuído à propriedade classid da tag: em vez de usarmos o GUID que identificava o ID do objecto registado na máquina, usamos uma string especial que identifica o componente. Por exemplo, se colocarmos a dll com o nosso UserControl na mesma pasta de uma página, então podemos embeber o UserControl através da seguinte sintaxe:
<object classid="Dummy.dll#UILAControlsLib.ProgressBarCtl" ...>
</object>
Como é possível observar, o UserContorl é identificado através do nome do assembly e do nome do UserControl. É importante salientar que podemos, tal como acontecia com os ActiveXs, utilizar os elementos <param> para atribuirmos valores às propriedades expostas pelo UserControl. Outra observação interessante prende-se com o facto do atributo codebase já não ser usado para indicar a localização da dll que contém o componente.
Ao vermos a simplicidade da utilização de UserControls no IE, podemos perguntar-nos porque não embeber directamente a barra de progresso nas nossas páginas. Infelizmente, os controlos mantidos na GAC não podem ser directamente embebidos na página. Se o desejarmos, temos mesmo de construir um UserControl com esse componente.
Segurança 101
A plataforma .NET introduziu o CAS (Code Access Security) de forma a limitar eventuais operações efectuadas pelo código. Assim, todas as aplicações .NET podem gerar excepções de segurança se não tiverem as permissões adequadas à execução das tarefas desejadas. Por exemplo, ao contrário do que acontecia com os ActiveX construídos na era pré-.NET, o acesso aos ficheiros existentes na máquina de um utilizador deixou de ser permitido quando um componente .NET é carregado numa página apresentada pelo IE. Por outro lado, esse componente consegue aceder ao sistema de ficheiros se for carregado a partir de uma aplicação que está a ser executada na própria máquina. Esta diferença de comportamentos é explicada devido ao CAS!

Assim, podemos considerar o CAS como sendo um mecanismo que ajuda a limitar o acesso de código .NET a recursos e operações. Na prática, os administradores das máquinas recorrem às ferramentas de administração da plataforma para definirem as permissões que cada componente possui. Todas as operações "especiais" efectuam uma verificação da stack de forma a garantir que o código responsável pela operação tem as permissões necessárias: este mecanismo de verificação é designado demand. Se algums dos excertos de código existentes na stack não possuem permissões para efectuar essa operação, então a operação "especial" termina com a geração de uma excepção.
As permissões atribuídas a um componente são dividas em zonas, como podemos ver através da figura seguinte:

Quando um componente .NET é carregado, a plataforma recorre à evidência disponibilizada pelo assembly para definir as permissões atribuídas ao assembly. Por exemplo, quando carregamos uma página de um site mantido na intranet e essa página possui um componente .NET, esse componente será carregado na zona LocalIntranet_Zone e possui todas as permissões definidas nessa zona (conforme podemos verificar na figura seguinte).

Portanto, cmo vimos, os componentes são associados a uma zona de acordo com a evidência relacionada com esse componente. Existem vários tipos de evidência; como vimos, o local a partir de onde o componente é carregado constitui um tipo de evidência; contudo, existem outros, como, por exemplo, o strong name do assembly. Para garantir o correcto funcionamento do nosso UserControl, vamos ter de modificar as definições de segurança de forma a fornecer os privilégios necessários para este poder ser executar as acções necessárias (neste caso, temos de fornecer-lhe direitos suficientes para aceder aos ficheiros existentes no disco do PC). Para simplificar, vamos fornecer controlo total a todos os componentes carregados a partir de um URL conhecido (neste caso, vamos usar localhost). A figura seguinte ilustra o novo code group criado como filho directo do grupo AllCode existente no nó LocalMachine:

Infelizmente, esta alteração nas definições de segurança da máquina cliente não é suficiente para conseguirmos colocar o nosso componente a funcionar correctamente. Como vimos, O CAS percorre toda a stack de forma a verificar que todos os elementos aí existentes possuem as permissões correctas para executar a acção pretendida. O nosso UserControl será carregado através do componente IEHost que não possui permissões para aceder ao sistema de ficheiros e fará com que todo a abertura do ficheiro resulte na geração de uma excepção (note-se que isto acontece apesar do nosso componente ter realmente permissões para efectuar essa operação). Este tipo de comportamente faz sentido já que, se assim não fosse, qualquer código sem permissões poderia utilizar o nosso componente para aceder ao sistema de ficheiros. Para resolver este problema, temos de efectuar um ASSERT a partir do nosso código. Sempre que a plataforma encontra um ASSERT, interrompea stack walk nessa altura. É devido a isto que praticamente todos os métodos/propriedades expostos pelo UserControl possuem ASSERTS (ver excertos anteriores); se tal não acontecesse, a plataforma iria acabar a stack walk com a análise do componente IEHost que, como afirmámos, não possui permissões para efectuar as operações desejadas pelo nosso componente. Como é óbvio, os ASSERTs devem ser usados com muito muito cuidado!
Para terminar esta breve análise à definição de permissões, convém ainda referir que, se quisermos, podemos exportar as definições de uma determinada área através da criação de um pacote de instalação. A geração de um pacote deste tipo facilita a distribuição das políticas de segurança pelas várias máquinas que necessitam de usar o componente em causa.
Construção do control ASP.NET
Após construirmos o UserControl e configurarmos as políticas de segurança, resta-nos apenas construir o controlo ASP.NET que será usado nas páginas que necessitam de efectuar upload de ficheiros. Este controlo deve ser capaz de:
- Expor todas as propriedades do UserControl.
- Gerar a tag object e respectivos elementos param usados para passar os valores às propriedades expostas pelo UserControl.
- Permitir o correcto funcionamento do controlo sem ViewState.
- Ser facilmente distribuído por várias aplicações.
- Permitir a interacção de scripts cliente com o componente de forma a permitir a modificação de algumas propriedades no lado cliente sem recorrer a postbacks.
Alguns destes objectivos eram extremamente dificeis de alcançar durante a versão 1.X da plataforma. Contudo, as novidades introduzidas pela versão 2.0 permitem efectuar todas estas operações facilmente. Aqui apenas vamos referir os passos necessários à fácil distribuição do controlo servidor. Como vimos, um dos passos necessários ao correcto funcionamento do componente passava pelo carregamento do UserControl no IE. Na versão 1.X, isto implicaria a distribuição de uma dll com o controlo servidor e de outra dll com o UserControl que deveria ser colocada num local predefinido de forma a simplificar a geração do valor atribuído à propriedade classid. Felizmente para nós, a nova versão da plataforma permite-nos embeber o recurso e recuperá-lo em runtime através da utilização da handler webresource.axd.
O controlo ASP.NET começa por embeber a dll que contém o user control (no VS basta adicionar a dll ao projecto e escolher a opção Embedded Resource nas propriedades da dll) e configura o recurso como recuperável através da utilização do atributo WebResourceAttribute:
[assembly: WebResource("Dummy.dll", "application/x-msdownload")]
A geração do HTML do controlo ASP.NET obriga-nos a efectuar o override de alguns dos métodos herdados da classe WebControl:
- A propriedade TagKey é modificada de forma a retornar o valor HtmlTextWriterTag.Object.
- O método AddAttributesToRender é modificado de forma a adicionar o atributo classid à tag object.
- O método RenderChildren é modificado de forma a gerar os elementos param com os valores das propriedades.
- O método OnPreRender é modificado de forma a gerar os scripts necessários à correcta utilização do componente a partir de script cliente.
Em seguida, apresentamos dois dos métodos anteriores que permitem ilustrar a construção do valor aplicado à propriedade classid do controlo HTML object usado para representar o objecto.
private string GetClassID( )
{
string pathToDll = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), "Dummy.dll");
int pos = pathToDll.IndexOf("WebResource.axd");
if (pos != -1)
{
pathToDll = pathToDll.Substring(pos);
}
return pathToDll + "#UILAControlsLib.ProgressBarCtl";
}
protected override void AddAttributesToRender( HtmlTextWriter writer )
{
base.AddAttributesToRender(writer);
writer.AddAttribute("CLASSID", this.GetClassID() );
}
Como é possível observar, o caminho até ao ficheiro embebido é obtido à custa da utilização do método GetWebResourceUrl. A introdução dos métodos script usados no lado cliente é feita através de um ficheiro js que também foi embebido na dll que contém os controlos servidor da aplicação (a recuperação do caminho até ao ficheiro também é feita através do método GetWebResourceUrl).
É importante referir ainda que o controlo recorre ao control state para manter o seu estado interno, fazendo assim com que o controlo funcione sem o view state. Para além disso, o controlo implementa a interface IPostBackDataHandler para recuperar o valor de um campo escondido mantido no cliente (este campo escondido existe para permitir a alteração das propriedades no lado cliente; durante um postback, o controlo consulta o campo para saber se os valores foram ou não alterados no lado cliente).
Conclusões finais
Ao longo deste artigo analisámos alguns dos aspectos tidos em atenção durante a construção do controlo LAFileUpload que permite efectuar o upload de um ficheiro e fornecer feedback gráfico ao utilizador. A construção deste controlo permite-nos demonstrar a utilização de vários principios associados à construção de controlos em ASP.NET e à utilização de UserControls no IE.
O código que acompanha este artigo pode ser obtido a partir daqui.
Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções
para progC@netmadeira.com.
Fiquem bem e boa programação! Até à próxima.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu