Мой дядя самых честных правил программ исходники за так ...

28 февр. 2009 г.

Пара интересных трюков для C# 3.0

В заметке я хочу обсудить применение двух трюков, ставших возможными после введения новых синтаксических конструкций в C# 3.0, таких как анонимные типы и лямбда-выражения (анонимные функции).

Эти трюки многим, конечно же, уже известны.

Анонимные типы как словарь

Итак, у нас появилась возможность задавать в области действия функций локальные анонимные типы:

var obj = new { Name = "Object", Color = "Black" };

Анонимный тип, по вполне понятным причинам, можно передать другой функции только нетипизировано, в виде объекта типа System.Object. Что делает его довольно бесполезным вне функции.

Идея состоит в том, чтобы преобразовать анонимный объект в словарь типа ключ-значение. И использовать такие объекты как альтернативный вариант для функций принимающих параметры в виде такого словаря.

Реализуем небольшой метод-расширение, который принимает произвольный объект и возвращает словарь со свойствами и их значениями из объекта.

ObjectExtensions.cs

using System.Collections.Generic;

 

namespace Home.Andir.Examples

{

    static class ObjectExtensions

    {

        public static Dictionary<string, object> ToDictionary(this object obj)

        {

            return obj.ToDictionary<object>();

        }

 

        public static Dictionary<string, T> ToDictionary<T>(this object obj)

        {

            var dict = new Dictionary<string, T>();

 

            foreach (var prop in obj.GetType().GetProperties())

            {

                dict.Add(prop.Name, (T)prop.GetValue(obj, null));

            }

 

            return dict;

        }

    }

}

Применение этого “трюка” можно найти в ASP.Net MVC в виде экземпляра объекта HtmlHelper. Напишем примерную реализацию метода HtmlHelper.Link, который сгенерирует содержимое произвольного тега “a”.

Вначале применение:

HtmlHelper.Link("Ссылка",

    new { href = "http://example.org/", title = "Нажмите, чтобы перейти далее." }

    );

Неправда ли, немного элегантнее и чуть меньше символов, чем:

HtmlHelper.Link("Ссылка",

    new Dictionary<string, string>{

        { "href", "http://example.org/" },

        { "title", "Нажмите, чтобы перейти далее." } }

    );

А теперь реализация:

HtmlHelper.cs

using System;

using System.Collections.Generic;

using System.Linq;

 

namespace Home.Andir.Examples

{

    class HtmlHelper

    {

        public static string Tag(string name, string text, object attrs)

        {

            return Tag(name, text, attrs.ToDictionary<string>());

        }

 

        public static string Tag(string name, string text,

            Dictionary<string, string> attrs)

        {

            var attrString = String.Join(" ",

                attrs.Select(x => String.Format("{0}=\"{1}\"", x.Key, x.Value))

                     .ToArray()

                    );

 

            return String.Format(

                "<{0} {1}>{2}</{0}>", name, attrString, text);

        }

 

        public static string Link(string text, object attrs)

        {

            return Tag("a", text, attrs);

        }

 

        public static string Link(string text, Dictionary<string, string> attrs)

        {

            return Tag("a", text, attrs);

        }

    }

}

 

Статически-типизированный Reflection

Основная проблема, которая возникает при использовании механизма отражения (Reflection) – это использование строк в качестве аргументов при извлечении данных. Это сильно мешает использованию инструментов для автоматического рефакторинга.

Рассмотрим объект:

class NamedObject

{

    [DisplayName("Имя")]

    public string Name { get; set; }

}

Пусть, у нас имеется нетипизированный экземпляр этого объекта и требуется узнать значение атрибута “DisplayName”.

ComponentHelper.cs

using System;

using System.ComponentModel;

using System.Reflection;

 

namespace Home.Andir.Examples

{

    public static class ComponentHelper

    {

        public static string GetPropertyDisplayName(Type type, string propName)

        {

            return GetDisplayName(type.GetProperty(propName));

        }

 

        private static string GetDisplayName(PropertyInfo propInfo)

        {

            var displayName = propInfo.Name;

 

            foreach (var attr in propInfo.GetCustomAttributes(true))

                if (attr is DisplayNameAttribute)

                    displayName = (attr as DisplayNameAttribute).DisplayName;

 

            return displayName;

        }

    }

}

Теперь вызываем:

Program.cs

using System;

 

namespace Home.Andir.Examples

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("DisplayName = \"{0}\"",

                ComponentHelper.GetPropertyDisplayName(typeof(NamedObject), "Name")

                );

 

            Console.ReadKey();

        }

    }

}

Вот тут мы и обнаруживаем проблему. Если использовать автоматический рефакторинг и переименовать свойство NamedObject.Name, например, в NamedObject.Name1, придётся ещё вручную переименовывать и имя свойства в вызове метода GetPropertyDisplayName.

Вот тут нам приходят на помощь, как это не странно, лямбда выражения, с их помощью предыдущий вызов можно переписать так:

Program.cs

using System;

 

namespace Home.Andir.Examples

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("DisplayName = \"{0}\"",

                ComponentHelper.GetPropertyDisplayName<NamedObject>(x => x.Name)

                );

 

            Console.ReadKey();

        }

    }

}

Как видно, теперь у нас нет использования строки с именем свойства, и автоматический рефакторинг будет работать безопасно.

Реализация такого варианта следующая:

ComponentHelper.cs

using System;

using System.ComponentModel;

using System.Reflection;

using System.Linq.Expressions;

 

namespace Home.Andir.Examples

{

    public static class ComponentHelper

    {

        public static string GetPropertyDisplayName<T>(

            Expression<Func<T, object>> propertyExpression

            )

        {

            return GetDisplayName(

                StaticReflection.GetPropertyInfo<T>(propertyExpression));

        }

 

        public static string GetPropertyDisplayName(Type type, string propName)

        {

            return GetDisplayName(type.GetProperty(propName));

        }

 

        private static string GetDisplayName(PropertyInfo propInfo)

        {

            var displayName = propInfo.Name;

 

            foreach (var attr in propInfo.GetCustomAttributes(true))

                if (attr is DisplayNameAttribute)

                    displayName = (attr as DisplayNameAttribute).DisplayName;

 

            return displayName;

        }

    }

}

StaticReflection.cs

using System;

using System.Linq.Expressions;

using System.Reflection;

 

namespace Home.Andir.Examples

{

    public static class StaticReflection

    {

        public static PropertyInfo GetPropertyInfo<T>(

            Expression<Func<T, object>> propertyExpression)

        {

            var memberExpression = propertyExpression.Body as MemberExpression;

            if (memberExpression == null)

                throw new InvalidOperationException(

                    "Only member access allowed in lambda expression.");

 

            var propInfo = memberExpression.Member as PropertyInfo;

            if (propInfo == null)

                throw new InvalidOperationException(

                    "Only property access allowed in lambda expression.");

 

            return propInfo;

        }

    }

}

Эпилог

Применение этих трюков можно найти в ASP.Net MVC, Rhino Mocks, Autofac, AutoMapper и многих других фреймворках. Уверен, что можно найти применение им и в своём коде ;-).

Интересный эффект Silverlight-плеера в Firefox

Если создать пустой xaml-файл и открыть его в Firefox 3, то можно, с удивлением, увидеть …:

Скриншот: Пустой XAML-файл внутри Firefox

как внутри окна откроется Internet Explorer 7 :-). Ай да, Microsoft!

27 февр. 2009 г.

Отладка маршрутов для System.Web.Routing

В глубинах интернета обнаружился интересный инструмент “Url Routing Debugger”, предназначенный для отладки маршрутов задаваемых с помощью System.Web.Routing.

Достаточно добавить RouteDebug.dll в референсы проекта, и прописать в Global.asax сразу после регистрации маршрутов следущий код:

using System;

using System.Web.Routing;

 

namespace Home.Andir.Examples

{

    public class Global : System.Web.HttpApplication

    {

        protected void Application_Start(object sender, EventArgs e)

        {

            RegisterRoutes(RouteTable.Routes);

 

            RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);

        }

 

        private void RegisterRoutes(RouteCollection routes)

        {

            routes.Add(

                new Route(

                    "route/{action}",

                    new StopRoutingHandler())

                );

 

            routes.Add(

                new Route(

                    "test/id={id}",

                    new StopRoutingHandler())

                );

 

            routes.Add(

                new Route(

                    "{controller}/{action}/{id}",

                    new RouteValueDictionary {

                        { "controller", "Home" },

                        { "action", "Index" },

                        { "id", "" } },

                    new StopRoutingHandler())

                );

 

        }

    }

}

Отладчик, подменяет все заданные IRouteHandler на свой DebugRouteHandler и теперь, если перейти на любой заданный маршрут, то можно увидеть следующую картину:

 Скриншот: RouteDebugger в действии

Здесь можно увидеть список всех зарегистрированных маршрутов и порядок их срабатывания (в первой колонке).

Как сохранить информацию на клиенте. Часть 1.

В этой заметке рассматриваются способы сохранения информации с помощью Cookies, Internet UserData Behavior, DOM Storage.

Введение

Количество веб-приложений растёт с немыслимой скоростью каждый день. Всё больше функциональности предоставляется пользователям прямо через браузер (а ведь это ещё недавно было прерогативой лишь только Интранет-приложений). А сами браузеры, тем временем, всё больше “стандартизируются” и пытаются угнаться друг за другом в скорости работы рендеринга DOM, скорости исполнения ява-скриптов (JavaScript) и оценкой при прохождении теста ACID3, и всё для того, чтобы позволить разработчикам ещё больше функциональности поместить прямо в веб-приложение, да так, чтобы пользователю было это удобно и незаметно.

Где же обычно хранят свою информацию веб-приложения?

Стандартный вариант – это, конечно же, файловая система на веб-сервере и/или база данных. При использовании асинхронных вызовов (AJAX) пользователи могут даже не знать и не замечать, что вводимая ими информация автоматически попадает на удалённый веб-сервер и сохраняется в виде записи в базе данных веб-приложения.

Что же это получается?

На сервере веб-приложения сохраняется большой набор персонифицированной информации (то есть связанной с конкретным человеком). И счастливый пользователь с любого компьютера в мире подключенного к Интернет может получить к ней доступ и внести в неё изменения (порой даже не осознавая этого).

А что если надо сохранить информацию на клиентском компьютере?

На сегодняшний день, благодаря, большим компаниям, появилось множество вариантов как это можно сделать. Но тут предварительно надо задать ещё один вопрос: а зачем нам это может быть надо?

Примечание: Не будем здесь вспоминать про специфику протокола HTTP и поддержку сессии пользователей, пусть это будет само-собой разумеющемся и скрытом в инфраструктуре приложений.

  • Хранение промежуточных данных, ввод которых ещё не окончен,

При заполнении формы, возникают ошибки ввода, сбои веб-приложения, обрывы соединений, перерывы в работе, случайные нажатия кнопок – чтобы не потерять работу, проделанную пользователем, хотелось бы сохранить её и позволить пользователю продолжать даже при критических сбоях.

  • Хранение персонифицированной информации для анонимного пользователя,

Пользователь не предоставляет веб-приложению никакой информации о себе и, соответственно, приложению не получится сохранить данные и связать их с конкретным пользователем. Зато сам анонимный пользователь сможет сохранять свои настройки, предпочтения, делать выбор товаров и т.п.

  • Возможность приложения работать в режиме Offline,

Если работа с приложением может быть достаточно долгой, то можно отключиться от сети и продолжать работать с приложением. А при следующем подключении, приложение определит все произведённые изменения и сохранит их на сервере.

Это может быть особенно удобным в мобильной среде, когда начать работать над документом можно возле одной точки Wi-Fi в гостинице, а закончить возле другой в аэропорту.

На этом ответов, думаю, хватит, так как вариантов необходимости сохранения некоторой информации на клиенте можно придумать множество. Пора рассмотреть технологии, которые позволяют нам это делать.

Http Cookies

Начнём, конечно, с самого древнего и испытанного средства: “печеньки”.

Браузеры все существующие, поддерживают в той или иной мере.
Макс. объём 4 * 50 KiB (4 KiB в IE) для одного домена
Безопасность Данные привязаны к домену, или даже к определённому пути на сайте.

Они представляют собой, в общем случае, строку из соединённых последовательно наборов типа ключ-значение, которые привязаны к определённому доменному имени:

param1=value1&param2=value2&param3=value3

Http Cookies передаются браузером при каждом запросе от пользователя к серверу с помощью http-заголовка “Cookies”. Выглядит это, примерно, так:

GET /index.html HTTP/1.1
 HOST: example.org
 Cookie: support=yes&value=1

Хранятся Cookies по-разному, в зависимости от реализации в браузере. Например, в Mozilla Firefox 3.0 они хранятся в специальной базе данных cookies.sqlite, а в Internet Explorer 7.0 используется отдельный набор файлов, по одному на доменное имя (Host).

Вот как выглядит содержимое файла  c сохранённой кукой для Internet Explorer:

MyNote
This%20is%20IE%20cookie!
localhost/examples/storages/
1088
1746404352
29989188
110582720
29989180
*

Чтобы управлять куками прямо в браузере, без необходимости делать раунд-трип на сервер, в объектной модели DOM существует свойство document.cookie, которое позволяет как получать значения всех установленных кукиз, так и добавлять/удалять их.

В качестве примера использования, реализуем небольшое приложение, которое будет сохранять некоторый текст в cookie с именем “MyNote”:

CookiesTest.htm

<html>

<head>

    <title>Тестируем Cookies</title>

    <style type="text/css">

    /* headings */

    h1, h2 { margin-bottom: 4px; }

    h1 { font-size: 16pt; }

    h2 { font-size: 12pt; color: darkred; }

 

    /* element styles */

    div

    {

        width: 600px;

        border: 1px dashed darkgray;

        padding: 5px;

    }

 

    #data

    {

        font-style: italic;

    }

 

    .normal { color: darkgreen; }

    .empty { color: gray; }

    </style>

    <script type="text/javascript">

    // UI utilities

    $ = function(id) { return document.getElementById(id); }

    if (window.attachEvent)

    {

        window.addEventListener = function(eventName, func, args)

        {

            window.attachEvent('on' + eventName,

        function() { func(window.event, args); })

        }

    }

 

    // cookies utilities

    getCookies = function()

    {

        var cookies = {};

 

        var items = document.cookie.split("; ");

        for (var ii = 0; ii < items.length; ii++)

        {

            var keyValuePair = items[ii].split("=");

 

            cookies[keyValuePair[0]] = decodeURIComponent(keyValuePair[1]);

        }

 

        return cookies;

    }

 

    setCookie = function(key, value, expires)

    {

        document.cookie =

            key + "=" + (value ? encodeURIComponent(value) : "")

                + ";expires=" + expires.toGMTString();

    }

 

    // main

    var storageKey = "MyNote";

 

    function TryLoadData()

    {

        var data = $("data");

 

        var cookies = getCookies();

 

        var value = cookies[storageKey];

        if (value != null)

        {

            data.className = "normal";

            data.innerHTML = value;

        }

        else

        {

            data.className = "empty";

            data.innerHTML = "(пусто)";

        }

    }

 

    function Window_Load()

    {

        TryLoadData();

    }

 

    function Save_Click()

    {

        var t = $("textbox");

 

        var dt = new Date();

        dt.setHours(dt.getHours() + 1);

 

        setCookie(storageKey, t.value, dt);

 

        TryLoadData();

    }

 

    window.addEventListener("load", Window_Load, null);

    </script>

</head>

<body>

    <h1>Тестируем Cookies</h1>

 

    <h2>Введите данные:</h2>

    <div>

        <textarea id="textbox" rows="6" cols="40"></textarea>

        <br />

        <input type="button" value="Сохранить" onclick="Save_Click()" />

    </div>

 

    <br />

    <h2>Сохраненные данные:</h2>

    <div id="data"></div>

</body>

</html>

Теперь, если запустить получившееся приложение, то получится:

Скриншот: Сохранение заметки в Cookies.

На сегодняшний день, практически все браузеры позволяют сохранить в куках до 50 различных записей, объёмом до 4 килобайт каждая.

Где читать далее:

 

Internet Explorer UserData Behavior

Браузеры Только Internet Explorer
Макс. объём 640-1024 KiB для одного домена
Безопасность Данные привязаны к домену, на котором находится страница.

Ещё один, довольно старый механизм сохранения данных у пользователя, это предопределённый behavior в браузере Internet Explorer.

Примечание: Если вы пользуетесь локально установленным MSDN, то знайте, что он сохраняет свои установки (фильтры по языку, технологии) именно с помощью UserData.

Представляют собой также наборы в виде ключ-значение, которые затем сохраняются в виде кусочка XML. Само содержимое хранится в папке [%APPDATA%\Microsoft\Internet Explorer\UserData].

Реализуем пример, который сохраняет данные в этом виде хранилища.

UserDataTest.htm

<html>

<head>

    <title>Тестируем UserData behavior</title>

    <style type="text/css">

    /* headings */

    h1, h2 { margin-bottom: 4px; }

    h1 { font-size: 16pt; }

    h2 { font-size: 12pt; color: darkred; }

 

    /* behavior */

    #storage { behavior: url(#default#userData); }

 

    /* element styles */

    div

    {

        width: 600px;

        border: 1px dashed darkgray;

        padding: 5px;

    }

 

    #area

    {

        border: 1px dashed darkgray;

    }

 

    #data

    {

        font-style: italic;

    }

 

    .normal { color: black; }

    .empty { color: gray; }

    </style>

    <script type="text/javascript">

    // UI utilities

    $ = function(id) { return document.getElementById(id); }

    if (window.attachEvent)

    {

        window.addEventListener = function(eventName, func, args)

        {

            window.attachEvent('on' + eventName,

            function() { func(window.event, args); })

        }

    }

 

    // main

    var storageName = "Home.Examples.Andir";

    var storageKey = "MyNote";

 

    function TryLoadData()

    {

        var s = $("storage");

        var data = $("data");

 

        s.load(storageName);

 

        var attr = s.getAttribute(storageKey);

        if (attr != null && attr != '')

        {

            data.className = "normal";

            data.innerHTML = attr;

        }

        else

        {

            data.className = "empty";

            data.innerHTML = "(пусто)";

        }

    }

 

    function Window_Load()

    {

        TryLoadData();

    }

 

    function Save_Click()

    {

        var s = $("storage");

        var t = $("textbox");

 

        s.setAttribute(storageKey, t.value);

        s.save(storageName);

 

        TryLoadData();

    }

 

    window.addEventListener("load", Window_Load, null);

    </script>

</head>

<body>

    <h1>Тестируем UserData Behavior</h1>

 

    <div id="storage">Этот &lt;div&gt; элемент используется

        как контейнер, к которому привязан UserData behavior.</div>

 

    <h2>Введите данные:</h2>

    <div class="area">

        <textarea id="textbox" rows="6" cols="40"></textarea>

        <br />

        <input type="button" value="Сохранить" onclick="Save_Click()" />

    </div>

 

    <br />

    <h2>Сохраненные данные:</h2>

    <div id="data"></div>

</body>

</html>

Запускаем:

Скриншот: Сохранение заметки в UserData

Заглянем туда, где сохраняются данные UserData, и там среди папок со случайными именами (которые на самом деле привязаны к домену) можно найти файл Home.Examples[1].xml со следующим содержимым:

<ROOTSTUB MyNote="То что называется &quot;Французским поцелуем&quot; в англоязычных странах, называется &quot;Английским поцелуем&quot; во Франции."/>

Вот в таком виде и хранятся все данные использующие UserData behavior.

Где читать далее:

 

DOM Storage

Среди новостей об активных разработках в области web-стандартов за последнее время уже не раз то и дело мелькает HTML5 от WhatWG (Web Hypertext Application Technology Working Group), который ранее был известен под названием Web Applications 1.0. Первый черновик стандарта был опубликован в январе прошлого года (2008).

Примечание: последний опубликованный стандарт HTML 4.01 в последний раз редактировался в 1999 году.

Не будем углубляться в сам стандарт, рассмотрим только один его раздел: Structured Client-Side Storage.

Несмотря на то, что на сегодняшний день существует только черновик стандарта, но часть про структурированное хранилище на клиентской стороне уже реализована в нескольких браузерах.

Браузеры Firefox 2.0+, Internet Explorer 8.0, Safari
Макс. объём 5 MiB для одного домена
Безопасность Данные привязаны к домену, на котором находится страница.

Прочитав указанную часть стандарта можно узнать, о том, что существует интерфейс Storage с методами getItem, setItem, removeItem. Реализации этого интерфейса представляются объектами localStorage и sessionStorage.

По сути Storage очень похож на механизм Cookies, за исключением того, что объём данных гораздо менее ограничен и нет привязки к HTTP-протоколу.

На сегодняшний день, в браузере Mozilla Firefox 3 реализован вариант sessionStorage. Данные при этом хранятся на время пользовательской сессии в браузере и после уничтожаются.

Посмотрим на это хранилище в действии:

DomStorageTest.htm

<html>

<head>

    <title>Тестируем DOM Storage</title>

    <style type="text/css">

    /* headings */

    h1, h2 { margin-bottom: 4px; }

    h1 { font-size: 16pt; }

    h2 { font-size: 12pt; color: darkred; }

 

    /* behavior */

    #storage { behavior: url(#default#userData); }

 

    /* element styles */

    div

    {

        width: 600px;

        border: 1px dashed darkgray;

        padding: 5px;

    }

 

    #area

    {

        border: 1px dashed darkgray;

    }

 

    #data

    {

        font-style: italic;

    }

 

    .normal { color: black; }

    .empty { color: gray; }

    </style>

    <script type="text/javascript">

    // UI utilities

    $ = function(id) { return document.getElementById(id); }

    if (window.attachEvent)

    {

        window.addEventListener = function(eventName, func, args)

        {

            window.attachEvent('on' + eventName,

            function() { func(window.event, args); })

        }

    }

 

    // main

    var storageKey = "MyNote";

 

    function TryLoadData()

    {

        var s = window.sessionStorage;

        var data = $("data");

 

        var attr = s[storageKey];

        if (attr != null && attr != '')

        {

            data.className = "normal";

            data.innerHTML = attr;

        }

        else

        {

            data.className = "empty";

            data.innerHTML = "(пусто)";

        }

    }

 

    function Window_Load()

    {

        TryLoadData();

    }

 

    function Save_Click()

    {

        var s = window.sessionStorage;

        var t = $("textbox");

 

        s[storageKey] = t.value;

 

        TryLoadData();

    }

 

    window.addEventListener("load", Window_Load, null);

    </script>

</head>

<body>

    <h1>Тестируем DOM Storage</h1>

 

    <h2>Введите данные:</h2>

    <div class="area">

        <textarea id="textbox" rows="6" cols="40"></textarea>

        <br />

        <input type="button" value="Сохранить" onclick="Save_Click()" />

    </div>

 

    <br />

    <h2>Сохраненные данные:</h2>

    <div id="data"></div>

</body>

</html>

Запускаем:

Скриншот: Сохранение заметки в sessionStorage

Если закрыть окно, то сохранённые данные, к сожалению, удалятся. Чтобы сохранить данные дольше, чем на время браузерной сессии, то в Firefox 3.0 нужно использовать объект globalStoragе, который хранит данные неограниченно долго и также реализует интерфейс Storage.

Примечание: Данные помещённые в globalStorage браузера Firefox сохраняются в базе данных webappsstore.sqlite.

Где читать далее:

Продолжение следует … (Google Gears, Silverlight Isolated Storage, Adobe Flash Storage).

23 февр. 2009 г.

Неприятности с web-сервисами после установки .Net Framework 3.5 SP1

Внезапно, после установки обновления .Net Framework 3.5 Service Pack 1, некоторые веб-сервисы с NTLM-аутентификацией начинают выдавать 401.1 - Unauthorized: Logon Failed.

Происходит это в тех нередких случаях, когда пытаешься вызвать веб-сервис, расположенный на том же IIS, но обращаясь при этом по длинному имени (FQDN). Причина возникновения ошибки кроется в том, что в новом сервис-паке при использовании аутентификации NTLM стали учитывать нововведение в серверной безопасности под названием “Loopback check” (kb896861). Полное описание изменений приведено в MSDN и там же есть способы исправления ситуации (аналогично статье из KB): Changes to NTLM authentication for HTTPWebRequest in Version 3.5 SP1.

Примечание: “Loopback check security feature” была добавлена ещё сервис-паками Windows XP Service Pack 2 и Windows 2003 Service Pack 1, но до этого не учитывалась механизмами аутентификации .Net Framework.