четверг, 5 ноября 2009 г.

RouteHandler для IHttpHandler

Сегодня я увидел ошибку в Connect в которой некто предлагает PageRouteHandler’у (новинка в ASP.NET 4) обрабатывать IHttpHandler так же как и Page. Вообще, я не согласен с этим предложением, потому что Page является IHttpHandler, а не наоборот. Что если человек действительно хочет новый обработчик? Дадим ему такое языковыворачивающее название IHttpHandlerRouteHandler.
К сожалению, уже совсем поздно добавлять такое в ASP.NET 4, но оно оказывается тривиально простым для написания. По сути вот оно:

 public class HttpHandlerRouteHandler<THandler>   
   : IRouteHandler where THandler : IHttpHandler, new()   
 {  
      public IHttpHandler GetHttpHandler(RequestContext requestContext)   
      {  
        return new THandler();  
      }  
 }  

Конечно, оно само по себе не особо полезно. Нам нужен метод-расширитель (“extension method”) для того чтобы действительно было просто регистрировать маршруты для HttpHandler’ов.Я написал целый ряд их, но только два примера тут в моем блоге. Остальные вы можете получить скачав проект внизу этого поста.

 public static class HttpHandlerExtensions   
 {  
      public static void MapHttpHandler<THandler>(this RouteCollection routes, string url) where THandler : IHttpHandler, new()   
      {  
           routes.MapHttpHandler<THandler>(null, url, null, null);  
      }  
  //...  
      public static void MapHttpHandler<THandler>(this RouteCollection routes, string name, string url, object defaults, object constraints) where THandler : IHttpHandler, new()   
      {   
           var route = new Route(url, new HttpHandlerRouteHandler<THandler>());  
           route.Defaults = new RouteValueDictionary(defaults);  
           route.Constraints = new RouteValueDictionary(constraints);  
           routes.Add(name, route);  
      }  
 }  

Это дает нам очень простую возможность регистрировать маршрут, который обрабатывается IHttpHandler'ом. В этом случае, я регистрирую маршрут, использующий мой SimpleHttpHandler для обработки любого двухсегментного URL.

 public static void RegisterRoutes(RouteCollection routes)  
 {  
      routes.MapHttpHandler<SampleHttpHandler>("{foo}/{bar}");  
 }  

И здесь код SampleHttpHandler для завершения. Все что он делает, так это выводит значения маршрута.

 public class SampleHttpHandler : IHttpHandler   
 {  
      public bool IsReusable   
      {  
           get { return false; }  
      }  
      public void ProcessRequest(HttpContext context)   
      {  
           var routeValues = context.Request.RequestContext.RouteData.Values;  
           string message = "I saw foo='{0}' and bar='{1}'";  
           message = string.Format(message, routeValues["foo"], routeValues["bar"]);  
           context.Response.Write(message);  
      }  
 }  

Затем я делаю запрос для /testing/yo и вижу сообщение
“I saw foo='testing' and bar='yo'”
в моем браузере.
Здесь одно ограничение – мой http handler должен иметь конструктор без параметров. Это впринципе неплохое ограничение, так как ранее регистрируя маршрут необходимо было вводить проверку, что обработчик имеет пустой конструктор.
Вольный перевод статьи Phil Haack
Там же пример для Visual Studio 2010.

вторник, 3 ноября 2009 г.

Использование C# dynamic для упрощения доступа к ADO.NET данным

Сценарий таков, что вы не используете какой-либо ORM такие как LINQ to SQL или Entity Framework, но вы напрямую используете ADO.NET для выполнения SQL-команд. Это вообще не принятый мною способ, но много народа предпочитает его более высокому уровня доступа к данным.
Давайте посмотрим на пример, который мы собираемся улучшить. Возьмем пример из MSDN о SqlCommand:

 string commandText = "SELECT OrderID, CustomerID FROM dbo.Orders;";  
 using (var connection = new SqlConnection(Settings.Default.NorthwindConnectionString)) {  
   using (var command = new SqlCommand(commandText, connection)) {  
     connection.Open();  
     using (SqlDataReader reader = command.ExecuteReader()) {  
       while (reader.Read()) {  
         Console.WriteLine(String.Format("{0}, {1}", reader[0], reader[1]));  
       }  
     }  
   }  
 }  

Давайте сейчас предположим, что мы мы заинтересованы только в одном select-запросе, который даст возможность нам несколько абстрагироваться от деталей в SQL. Написав несколько небольших хелперов, который используют dynamic, мы сможем написать довольно простой код:

 string commandText = "SELECT OrderID, CustomerID FROM dbo.Orders;";  
 foreach (var row in SimpleQuery.Execute(Settings.Default.NorthwindConnectionString, commandText)) {  
   Console.WriteLine(String.Format("{0}, {1}", row.OrderID, row.CustomerID));  
 }  

Некоторые примечания:
Нам достаточно сделать единственный вызов и прямо получить объекты с которыми мыможем работать. Сравните это с использованием SqlConnection, SqlCommand и SqlDataReader.
Мы используем стандартный шаблон перечисления, в котором SqlDataReader делает вызов reader.Read() на каждой итерации, что выглядит ужасно.
И еще одно важное: мы получаем прямой доступ к свойствам объекта строки благодаря динамическому объекту! Например мы можем написать row.OrderID вместо reader[0](или reader[“OrderID”])
Как же это работает? Первое, давайте посмотрим на SimpleQuery.Execute метод:

 public static IEnumerable<dynamic> Execute(string connString, string commandText) {  
   using (var connection = new SqlConnection(connString)) {  
     using (var command = new SqlCommand(commandText, connection)) {  
       connection.Open();  
       using (SqlDataReader reader = command.ExecuteReader()) {  
         while (reader.Read()) {  
           yield return new DataRecordDynamicWrapper(reader);  
         }  
       }  
     }  
   }  
 }  

Это собственно тоже самое что и в MSDN коде за исключением что оно оборачивает возвращаемый reader в DataRecordDynamicWrapper, который делает всю волшебную динамическую работу. Также заметьте что метод возвращает IEnumerable благодаря которому собственно мы и можем писать 'var row' в тестовом коде (который по-моему выглядит лучше чем 'dynamic row').
Так что сейчас все что осталось так это взглянуть на DataRecordDynamicWrapper, который невероятно прост:
 public class DataRecordDynamicWrapper : DynamicObject {  
   private IDataRecord _dataRecord;  
   public DataRecordDynamicWrapper(IDataRecord dataRecord) { _dataRecord = dataRecord; }  
   public override bool TryGetMember(GetMemberBinder binder, out object result) {  
     result = _dataRecord[binder.Name];  
     return result != null;  
   }  
 }  

Все что он делает, так это получает данные по индексу в _dataRecord для заданного имени свойства.
Единственная последняя вещь, которая ничего не стоит для того чтобы сделать реальной, мы должны добавить поддержку SQL параметров, которые дадут простоту написания SQL кода, не подверженного SQL-инъекциям. Это может быть просто сделано передавая параметры в SimpleQuery.Execute.
Вольный перевод статьи David Ebbo (Исходный код там же)

суббота, 31 октября 2009 г.

ASP.NET: Вызов метода страницы с помощью jQuery

В этом примере будет рассмотрено как можно вызывать метод ASP.net страницы используя библиотеку jQuery.
Добавляем на страницу следующий javascript код:
 $(function() {  
  $("#ZipCodeTextBox").bind('change', function(event) {  
   // If it doesn't look like a zip code, don't even bother with the request  
   if (/^\d{5}(-\d{4})?$/.test($(this).val()))  
    $.ajax({  
     type: "POST",  
     contentType: "application/json; charset=utf-8",  
     url: "Default.aspx/GetCityStateByZip",  
     data: "{'zip': '" + $(this).val() + "'}",  
     dataType: "json",  
     success: function(msg) {  
      $("#CityStateLabel").text(msg.d.City + ", " + msg.d.State);  
     }  
    });  
  });  
 });  

Для знакомых с jQuery этот код должен показаться довольно обычным. А новички могут быть удивлены насколько этот небольшой участок кода функционален.

Для начала добавляем обработчик события onchange к текстовому полю с id "#ZipCodeTextBox":

 $(function() {  
  $("#ZipCodeTextBox").bind('change', function(event) {  

Потом, jQuery ждет пока DOM страницы загрузится, поэтому мы можем быть уверенными, что textbox к которому мы хотим привязать событие будет найден. Используя супер-простую возможность jQuery в выборе элементов, мы передаем CSS-селектор #ZipCodeTextBox в функцию $(). Затем мы вызываем функцию bind() для textbox, передавая 'change' как тип события и вторым параметром саму функцию, которую мы хотим вызывать в ответ на вызов этого события. Эта функция принимает один параметр, event в данном примере, но это не будет использовано здесь.

Когда вызывается событие onchange нашего textbox, блок кода внутри функции bind выполнится. Сначала мы выполняем простую проверку значения textbox’a c помощью регулярного выражения. Для получения значения textbox, мы можем использовать $(this), который существенно расширяет наш textbox элемент, предоставляя доступ ко всем существующим возможностям jQuery. В этом случае, $(this).val()  это все что необходимо – val() возвращает значение хранящие элементы формы.

Наше регулярное выражение
/^\d{5}(-\d{4})?$/
используется для проверки, что значение текстового поля состоит из 5 цифр - \d{5} – за которыми может следовать дефис и еще 4 цифры  – (-\d{4})?. Очевидные примеры почтового индекса могут быть 01721-8582 или 95060. Если значение введенное в textbox не удовлетворяет этому формату, то вызов метода страницы не будет произведен.
Итого вся проверка такая:
/^\d{5}(-\d{4})?$/.test($(this).val())

Отсюда мы вызываем $.ajax(), другую jQuery функцию, которая создает AJAX запрос с заданными параметрами. ASP.NET требует чтобы наш запрос был выполнен методом POST (c GET не сработает) и с установленным content-type ‘application/json’. Url-параметр это просто имя нашей страницы содержащей PageMethod (Default.aspx в нашем примере) за которым следует прямой слеш '/‘ и имя метода нашей страницы в code-behind файле. Наш метод должен быть объявлен с аттрибутом WebMethod() для того чтобы успешно выполнить запрос:

 Imports System.Web.Services  
 ...  
 <WebMethod()> _  
 Public Shared Function GetCityStateByZip(zip As String) As CityState  
 ...  
 End Function  
Данные которые отправляются на страницу должны быть в JSON формате, который сводится к набору пар 'имя-значение’ заключенные в фигурные скобки. Так как наш PageMethod ожидает параметр 'zip’, мы строим наши данные по типу

{'zip':'95064'}

Затем мы устанавливаем datatype параметру запроса значение 'json'. Весь список AJAX параметров:
 type: "POST",  
 contentType: "application/json; charset=utf-8",  
 url: "Default.aspx/GetCityStateByZip",  
 data: "{'zip': '" + $(this).val() + "'}",  
 dataType: "json"  

Наконец, мы определяем success параметр нашего AJAX вызова, устанавливая его значение функцией параметр которой результат нашего запроса.
success: function(msg) {
  $("#CityStateLabel").text(msg.d.City + ", " + msg.d.State);
}


Мы можем производить любые манипуляции с данными  которые нам нужны тут, хотя некоторые будут спорить, что обработка данных должна быть помещена на серверной стороне. Я полагаю, что в действительности это зависит от конкретной задачи. Во всяком случае, мы собираемся проделать здесь несколько манипуляций с DOM, включая простое присваивание определенному asp:Label элементу значения города  и штата, которые возвращены нам из PageMethod. Опять, $() берет CSS селектор как параметр, возвращая нам элемент, который мы хотим модифицировать. И мы хотим только изменить текст метки, так что мы используем text() функцию, передавая ей значение, которое мы хотим установить.

Изначально, я ожидал что использование msg.City и msg.State подобно тому как оно работает в Microsoft AJAX вызове метода страницы. Было довольно запутанно до тех пор пока не проверил JSON возвращенный из PageMethod используя FireBug для Firefox (если у вас его нету, вы можете достать его здесь http://getfirebug.com/ ). Кажется, ASP.net обертывает весь JSON ответ и присваивает его к ключу с названием 'd'. Наш полученный JSON объект:
{'d': {'City': 'Santa Cruz', 'State': 'CA'}}

Так как наш JSON объект является параметром msg переданной в success функцию, то для того чтобы получить значения города мы должно просто включить d в наше выражение – msg.d.City.
На этом все! Нам удалось вызвать PageMethod с помощью jQuery.

Перевод статьи Brian Dobberteen (исходный код там же)