Mais acessados

quarta-feira, 20 de fevereiro de 2013

ASP.NET WebForms - Urls simplificadas usando o FriendlyUrls

No dia 18/02/2012 saiu mais um patch de atualização ASP.NET and Web Tools 2012.2 update. Dentre as várias melhorias contidas nesse patch a que mais chamou minha atenção foi o suporte nativo a FriendlyUrls, que torna possível usar o sistema de rotas introduzido no ASP.NET v4.0 de maneira fácil com o WebForms.

Conceito


Para aqueles que já programaram para a plataforma ASP.NET MVC/WebApi notaram a estrutura bem definida das Urls organizadas por diretórios, actions e controllers. Com o lançamento deste novo patch ficou simples disponibilizar em sua aplicação Urls que independem da extensão de seu arquivo WebForm. Isso, graças a uma nova classe helper, FriendlyUrls que disponibiliza uma série de métodos que fazem o "trabalho sujo por trás dos panos".

Implementação


Se você estiver criando um novo projeto ASP.NET WebForms depois de instalar o ASP.NET and Web Tools 2012.2 update as configurações do FriendlyUrls já vem habilitadas por padrão. Caso contrário basta adicionar via NuGet, abrindo o Pack Mananger Console, usando o comando "Install-Package Microsoft.AspNet.FriendlyUrls -Pre".
Com o FriendlyUrls instalado no seu projeto resta somente programação. Criei uma classe chamada RouteConfig para habilitar o FriendlyUrls e no Global.asax usei essa classe para registrar as rotas na tabela.
    public static class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.EnableFriendlyUrls();
        }
    }
 
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterOpenAuth();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
O método EnableFriendlyUrls() é um extension method da classe FriendlyUrls que faz todo o trabalho duro. Agora basta digitar o endereço de página sem se preocupar com a extensão .aspx. Uma observação é que se a página estiver continda dentro de um diretório com o mesmo nome desta, ele não consegue resolver a rota.
A classe FriendlyUrls trás alguns métodos interessantes. Na imagem abaixo em amarelo estão a Url que acessei e o caminho virtual obtido com a chamada do método.
 //Recupera o caminho virtual da página
 Request.GetFriendlyUrlFileVirtualPath();
 //Recupera os segmentos da url
 Request.GetFriendlyUrlSegments();
Com esse método é possível recuperar os segmentos da Url. Veja que tudo que vem após o nome da página é considerado como segmento.
 //Ajuda a criar os links para as páginas
 FriendlyUrl.Href("~/Pages/QueryStringPage", "ID", "1001");
Grifado na imagem abaixo está uma Url criada com um método utilitário da classe.
Espero que a dica seja útil! Não deixem de baixar o ASP.NET and Web Tools 2012.2 update. O exemplo que criei pode ser baixado aqui.



segunda-feira, 18 de fevereiro de 2013

ASP.NET Web API / HTML5 - Streaming de áudio e vídeo assincronamente

Muita gente acha que a WebApi é apenas um Framework para desenvolver outros Frameworks. Mas na verdade é uma maneira de usar o máximo de desempenho e controle sobre aplicativos que utilizam HTTP. Neste breve texto irei mostrar como fazer streaming de áudio e vídeo assincronamente usando ASP.NET WebApi.

Conceito básico


Para enviarmos dados assincronamente tiraremos proveito da nova classe PushStreamContent contida no namespace System.Net.Http. Essa classe permite que possamos enviar pacotes por demanda para o cliente. A referência que tive veio do blog Andru's WebLog. Em nosso caso leremos o arquivo do disco do servidor e o enviaremos pouco a pouco para o cliente que visualizará ou escutará a mídia por meio de controles HTML5 de áudio e vídeo.

Implementação


//A classe recebe como construtor uma Action
public PushStreamContent(Action onStreamAvailable);

Criei uma classe de ajuda que lê a mídia do disco do servidor e assincronamente a salva em um buffer e libera para o cliente de acordo com a demanda. Usei as novas técnicas de programação contidas no C# 5.0 async/await para realizar a tarefa.
    public class MediaStream
    {
        private readonly string _filename;

        public MediaStream(string filename)
        {
            this._filename = filename;
        }

        public async void WriteToStream(Stream outputStream, HttpContent content, TransportContext context)
        {
            try
            {
                var buffer = new byte[65536];

                using (var media = File.Open(_filename, FileMode.Open, FileAccess.Read))
                {
                    var length = (int)media.Length;
                    var bytesRead = 1;

                    while (length > 0 && bytesRead > 0)
                    {
                        bytesRead = media.Read(buffer, 0, Math.Min(length, buffer.Length));
                        await outputStream.WriteAsync(buffer, 0, bytesRead);
                        length -= bytesRead;
                    }
                }
            }
            catch (HttpException ex)
            {
                return;
            }
            finally
            {
                outputStream.Close();
            }
        }
    }
Note que a assinatura do método WriteToStream corresponde a assinatura do métodos PushStreamContent. Logo depois criei duas classes, uma para Vídeo e outra para Áudio ambas filhas da classe MediaStream. Fiz dessa maneira para promover uma certa extensibilidade caso seja necessário fazer algo mais complexo mais tarde.
    public class VideoStream : MediaStream
    {
        public VideoStream(string filename) : base(filename)
        {

        }
    }
    public class AudioStream : MediaStream
    {
        public AudioStream(string filename) : base(filename)
        {

        }
    }
Agora vamos para a classe Controller para áudio e vídeo, que ficaram bastante simples após a separação nas classes anteriores.
    public class AudiosController : ApiController
    {
        // GET api/music/5
        public HttpResponseMessage Get(string filename, string ext)
        {
            filename = @"\Content\Audio\" + filename + "." + ext;

            var audio = new AudioStream(filename);

            var response = Request.CreateResponse();
            response.Content = new PushStreamContent(audio.WriteToStream, new MediaTypeHeaderValue("audio/" + ext));

            return response;
        }
    }

    public class VideosController : ApiController
    {
        // GET api/music/5
        public HttpResponseMessage Get(string filename, string ext)
        {
            filename = @"\Content\Audio\" + filename + "." + ext;

            var video = new VideoStream(filename);

            var response = Request.CreateResponse();
            response.Content = new PushStreamContent(video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));

            return response;
        }
    }
O nome do arquivo deve conter o caminho completo do mesmo em seu servidor, logo quando for fazer o seu teste lembre-se disso. Agora para que o cliente possa passar um endereço e consumir os nossos serviços é necessário que este registre uma rota com a padrão correspondendo, neste exemplo estou usando api/extensão/nomeDoArquivo:
config.Routes.MapHttpRoute(
    name: "DefaultVideo",
    routeTemplate: "api/{controller}/{ext}/{filename}"
);

Consumindo os serviços


Basta criar uma página e adicionar nossos controles HTML5:
    

Música

Video

Agora você pode abrir alguma ferramenta como firebug e ver que o conteúdo está sendo transmitido por streaming e não sendo carregado como um todo. Uma observação é que você deve usar o Chrome para fazer seus testes e brincar com este exemplo, eu não consegui fazer funcionar em nenhum outro =). Espero que tenham gostado da dica, o código fonte se encontra abaixo:

quarta-feira, 19 de setembro de 2012

ASP.NET MVC – Tratando erros HTTP e Exceções

Nesse artigo mostrarei um exemplo prático e simples de como exibir mensagens amigáveis caso aconteça um erro HTTP ou alguma exceção em sua aplicação.

Tratando Erros HTTP



Passo 1 – Criando um NotFoundActionResult


Criaremos nosso próprio ActionResult que retornará nossos erros. Abaixo segue a implementação para o Erro 404, mas nada impede que implemente para outros =).
public class NotFoundActionResult : ActionResult
{
 private string _viewName;
 public NotFoundActionResult()
 {

 }
 public NotFoundActionResult(string viewName)
 {
  _viewName = viewName;
 }
 public override void ExecuteResult(ControllerContext context)
 {
  context.HttpContext.Response.StatusCode = 404;
  context.HttpContext.Response.TrySkipIisCustomErrors = true;

  new ViewResult { ViewName = string.IsNullOrEmpty(_viewName) ? "Error" : _viewName}.ExecuteResult(context);
 }
}
No método ExecuteResult, indicamos o StatusCode como 404 e setamos TrySkipIisCustomErrors para true para que o IIS não lance suas páginas de exceção.

Passo 2 – Criando nosso ErrorController


 public class ErrorController : Controller
    {
        public ActionResult NotFound()
        {
            return new NotFoundActionResult("NotFound");
        }
    }
Veja que passei como paramentro o nome da minha View para o NotFoundActionResult que acabamos de criar. </>

Passo 3 – Registrando Filtro



Para que nossas exceções sejam capturadas precisando registrar HandleErrorAttribute na lista de filtros do aplicativo. Dessa forma qualquer exception lançada será capturada.
 public static void RegisterGlobalFilters(GlobalFilterCollection filters)
 {
  filters.Add(new HandleErrorAttribute());
 }

Passo 4 – Criando uma View personalizada


Criaremos uma View tipada para HandleErrorInfo, assim caso aconteça uma exception no meio do caminho a mensagem será exibida, caso contrátio ela exibirá a mensagem de página não encontrada. Uma view amigável é muito melhor que a tela amarela da morte lançada pelo IIS =P, principalmente quando é exibida pra o usuário final.
@model System.Web.Mvc.HandleErrorInfo
@{
    ViewBag.Title = "Ops!";
}

Erro.

@if (@Model != null) {

@Model.Exception

} else {

Ops! 404 (Page Not Found)! A página não foi encontrada, provavelmente o endereço foi movido ou removido. Contate seu administrador de redes.

}

Passo 5 – Adicionando rotas


Precisamos registrar duas rotas: uma para tratar os erros para Actions ou Controllers que não existem (NotFound) e outra para tratar qualquer rota que fuja àquelas que foram definidas para nossa aplicação (NotFound-catchall).
public static void RegisterRoutes(RouteCollection routes)
{
 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 routes.MapRoute(
  name: "NotFound",
  url: "NotFound/{action}/{id}",
  defaults: new
  {
   controller = "Error",
   action = "NotFound",
   id = UrlParameter.Optional
  }
 );
 routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new
  {
   id = RouteParameter.Optional
  }
 );
 routes.MapRoute(
  name: "Default",
  url: "{controller}/{action}",
  defaults: new { controller = "Home", action = "Index"}
 );
 routes.MapRoute(
    name: "NotFound-catchall",
    url: "{*all}",
    defaults: new { controller = "Error", action = "NotFound" }
    );
}
Não se esqueça que a ordem em que as rotas são registradas é a ordem de precedência para validação, logo a útima rota deve ser a genérica (NotFound-catchall) que trata qualquer url inválida.

Tratando Exceções



Passo 1 – Criando uma View personalizada


Mais uma vez criaremos uma view tipada para HandleErrorInfo, e usaremos a mensagem de Erro e o Stack Trace capturados. Note que validamos se a requisicão é local para que o usuário final não veja o erro lançado, mas sim uma mensagem amigável.
@model System.Web.Mvc.HandleErrorInfo

@{
    ViewBag.Title = "Error";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

Erro

Erro

Erro ao executar a requisição

@{ if (Request.IsLocal) { if (@Model != null && @Model.Exception != null) {

Exception: @Model.Exception.Message

Stack Trace: @Model.Exception.StackTrace

} } else {

O erro foi reportado ao Administrador.

} }

Finalizando


É necessário ativar o customErrors para que nossos erros sejam redirecionados. Assim adicione ao arquivo as seguintes linhas:

 
      
 
Note que adicionei o erro 404 porque nosso ActionResult personalizado trata erros 404. Para cada Erro, você deve incluir uma linha no arquivo, caso deseje tratálos em páginas separadas.
O código fonte desse exemplo está disponível aqui.