Работаем с LINQ to XML

    В первой статье в блоге .NET «Работаем с XML» в комментариях народ потребовал статьи LINQ to XML. Что же, попробуем раскрыть принципы работы этой новой технологии от Microsoft.

    Создадим базу для ведения каталога аудиозаписей. База будет состоять из треков:
    • Код
    • Название
    • Исполнитель
    • Альбом
    • Продолжительность
    • Жанр

    Мы научимся добавлять, редактировать, удалять и делать различные выборки из нашей базы.

    Для начала создадим консольное приложение (я пишу свои проекты на C#, но суть в общем-то понятна будет всем) и подключим необходимое пространство имен

    using System.Xml.Linq;


    Создание файлов XML



    Создадим XML файл нашей базы содержащий несколько тестовых записей уже при помощи LINQ:

    //задаем путь к нашему рабочему файлу XML
    string fileName = "base.xml";
    
    //счетчик для номера композиции
    int trackId = 1;
    //Создание вложенными конструкторами.
    XDocument doc = new XDocument(
    	new XElement("library",
    		new XElement("track",
    			new XAttribute("id", trackId++),
    			new XAttribute("genre", "Rap"),
    			new XAttribute("time", "3:24"),
    			new XElement("name", "Who We Be RMX (feat. 2Pac)"),
    			new XElement("artist", "DMX"),
    			new XElement("album", "The Dogz Mixtape: Who's Next?!")),
    		new XElement("track",
    			new XAttribute("id", trackId++),
    			new XAttribute("genre", "Rap"),
    			new XAttribute("time", "5:06"),
    			new XElement("name", "Angel (ft. Regina Bell)"),
    			new XElement("artist", "DMX"),
    			new XElement("album", "...And Then There Was X")),
    		new XElement("track",
    			new XAttribute("id", trackId++),
    			new XAttribute("genre", "Break Beat"),
    			new XAttribute("time", "6:16"),
    			new XElement("name", "Dreaming Your Dreams"),
    			new XElement("artist", "Hybrid"),
    			new XElement("album", "Wide Angle")),
    		new XElement("track",
    			new XAttribute("id", trackId++),
    			new XAttribute("genre", "Break Beat"),
    			new XAttribute("time", "9:38"),
    			new XElement("name", "Finished Symphony"),
    			new XElement("artist", "Hybrid"),
    			new XElement("album", "Wide Angle"))));
    //сохраняем наш документ
    doc.Save(fileName);


    Теперь в папке с нашей программой после запуска появится XML файл следующего содержания:
    <?xml version="1.0" encoding="utf-8"?>
    <library>
      <track id="1" genre="Rap" time="3:24">
        <name>Who We Be RMX (feat. 2Pac)</name>
        <artist>DMX</artist>
        <album>The Dogz Mixtape: Who's Next?!</album>
      </track>
      <track id="2" genre="Rap" time="5:06">
        <name>Angel (ft. Regina Bell)</name>
        <artist>DMX</artist>
        <album>...And Then There Was X</album>
      </track>
      <track id="3" genre="Break Beat" time="6:16">
        <name>Dreaming Your Dreams</name>
        <artist>Hybrid</artist>
        <album>Wide Angle</album>
      </track>
      <track id="4" genre="Break Beat" time="9:38">
        <name>Finished Symphony</name>
        <artist>Hybrid</artist>
        <album>Wide Angle</album>
      </track>
    </library>


    Для создания подобного файла средствами XmlDocument кода понадобилось где-то раза в 2 больше. В коде выше мы воспользовались конструктором класса XDocument, который принимает в качестве параметра перечень дочерних элементов, которыми мы изначально хотим инициализировать документ. Используемый конструктор XElement принимает в качестве параметра имя элемента, который мы создаем, а так же перечень инициализирующих элементов. Удобно то, что мы в этих элементах можем задавать как новые XElement, так и XAttribute. Последние отрендретятся в наш файл как атрибуты самостоятельно. Если вам не нравится использоваться такую вложенность конструкторов и вы считаете такой код громоздким, то можно переписать в более традиционный вариант. Код ниже даст на выходе аналогичный XML файл:

    XDocument doc = new XDocument();
    XElement library = new XElement("library");
    doc.Add(library);
    
    //создаем элемент "track"
    XElement track = new XElement("track");
    //добавляем необходимые атрибуты
    track.Add(new XAttribute("id", 1));
    track.Add(new XAttribute("genre", "Rap"));
    track.Add(new XAttribute("time", "3:24"));
    
    //создаем элемент "name"
    XElement name = new XElement("name");
    name.Value = "Who We Be RMX (feat. 2Pac)";
    track.Add(name);
    
    //создаем элемент "artist"
    XElement artist = new XElement("artist");
    artist.Value = "DMX";
    track.Add(artist);
    
    //Для разнообразия распарсим элемент "album"
    string albumData = "<album>The Dogz Mixtape: Who's Next?!</album>";
    XElement album = XElement.Parse(albumData);
    track.Add(album);
    doc.Root.Add(track);
    
    /*
    *остальные элементы добавляем по аналогии
    */
    
    //сохраняем наш документ
    doc.Save(fileName);
    


    Естественно выбирать необходимый способ нужно по ситуации.

    Чтение данных из файла



    Сейчас попробуем просто прочитать данные из уже полученного файла и вывести их в удобном для восприятия виде в консоль:

    //задаем путь к нашему рабочему файлу XML
    string fileName = "base.xml";
    //читаем данные из файла
    XDocument doc = XDocument.Load(fileName);
    //проходим по каждому элементу в найшей library
    //(этот элемент сразу доступен через свойство doc.Root)
    foreach (XElement el in doc.Root.Elements())
    {
    	//Выводим имя элемента и значение аттрибута id
    	Console.WriteLine("{0} {1}", el.Name, el.Attribute("id").Value);
    	Console.WriteLine("  Attributes:");
    	//выводим в цикле все аттрибуты, заодно смотрим как они себя преобразуют в строку
    	foreach (XAttribute attr in el.Attributes())
    		Console.WriteLine("    {0}", attr);
    	Console.WriteLine("  Elements:");
    	//выводим в цикле названия всех дочерних элементов и их значения
    	foreach (XElement element in el.Elements())
    		Console.WriteLine("    {0}: {1}", element.Name, element.Value);
    }


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

    track 1
      Attributes:
        id="1"
        genre="Rap"
        time="3:24"
      Elements:
        name: Who We Be RMX (feat. 2Pac)
        artist: DMX
        album: The Dogz Mixtape: Who's Next?!
    track 2
      Attributes:
        id="2"
        genre="Rap"
        time="5:06"
      Elements:
        name: Angel (ft. Regina Bell)
        artist: DMX
        album: ...And Then There Was X
    track 3
      Attributes:
        id="3"
        genre="Break Beat"
        time="6:16"
      Elements:
        name: Dreaming Your Dreams
        artist: Hybrid
        album: Wide Angle
    track 4
      Attributes:
        id="4"
        genre="Break Beat"
        time="9:38"
      Elements:
        name: Finished Symphony
        artist: Hybrid
        album: Wide Angle


    Изменение данных



    Попробуем пройтись по всем узлам library и увеличить аттрибут Id элемента track на 1.
    (дальше писать объявление пути к файлу и результат вывода в консоль я приводить не буду, чтобы не перегружать лишней информацией статью, все компилировал, все работает:) ):

    //Получаем первый дочерний узел из library
    XNode node = doc.Root.FirstNode;
    while (node != null)
    {
    	//проверяем, что текущий узел - это элемент
    	if (node.NodeType == System.Xml.XmlNodeType.Element)
    	{
    		XElement el = (XElement)node;
    		//получаем значение аттрибута id и преобразуем его в Int32
    		int id = Int32.Parse(el.Attribute("id").Value);
    		//увеличиваем счетчик на единицу и присваиваем значение обратно
    		id++;
    		el.Attribute("id").Value = id.ToString();
    	}
    	//переходим к следующему узлу
    	node = node.NextNode;
    }
    doc.Save(fileName);


    Теперь попробуем это сделать более правильным способом для наших задач:

    foreach (XElement el in doc.Root.Elements("track"))
    {
    int id = Int32.Parse(el.Attribute("id").Value);
    	el.SetAttributeValue("id", --id);
    }
    doc.Save(fileName);


    Как видим – этот способ нам подошел больше.

    Добавление новой записи



    Добавим новый трек в нашу библиотеку, а заодно вычислим средствами LINQ следующий уникальный Id для трека:

    int maxId = doc.Root.Elements("track").Max(t => Int32.Parse(t.Attribute("id").Value));
    XElement track = new XElement("track",
    	new XAttribute("id", ++maxId),
    	new XAttribute("genre", "Break Beat"),
    	new XAttribute("time", "5:35"),
    	new XElement("name", "Higher Than A Skyscraper"),
    	new XElement("artist", "Hybrid"),
    	new XElement("album", "Morning Sci-Fi"));
    doc.Root.Add(track);
    doc.Save(fileName);


    Вот таким подним запросом ко всем элементам вычисляется максимальное значение аттрибута id у треков. При добавлении полученное максимальное значение инкрементируем. Само же добавление элемента сводится к вызову метода Add. Обратите внимание, что добавляем элементы в Root, так как иначе нарушим структуру XML документа, объявив там 2 корневых элемента. Так же не забывайте сохранять ваш документ на диск, так как до момента сохранения никакие изменения в нашем XDocument не отразятся в XML файле.

    Удаление элементов



    Попробуем удалить все элементы исполнителя DMX:

    IEnumerable<XElement> tracks = doc.Root.Descendants("track").Where(
    				t => t.Element("artist").Value == "DMX").ToList();
    foreach (XElement t in tracks)
    	t.Remove();


    В этом примере мы вначале выбрали все треки у который дочерний элемент artst удовлетворяет критерии, а потом в цикле удалили эти элементы. Важен вызов в конце выборки ToList(). Этим самым мы фиксируем в отдельном участке памяти все элементы, которые хотим удалить. Если же мы надумаем удалять из набора записей, по которому проходим непосредственно в цикле, мы получим удаление первого элемента и последующий NullReferenceException. Так что важно помнить об этом.
    По совету xaoccps удалять можно и более простым способом:
    IEnumerable<XElement> tracks = doc.Root.Descendants("track").Where(
    				t => t.Element("artist").Value == "DMX");
    tracks.Remove();

    В этом случае приводить к списку наш полученный результат вызовом функции ToList() не нужно. Почему этот способ не использовал изначально описал в комментарии :)


    Немного дополнительных запросов к нашей базе треков



    Отсортируем треки по продолжительности в обратном порядке:

    IEnumerable<XElement> tracks = from t in doc.Root.Elements("track")
    				   let time = DateTime.Parse(t.Attribute("time").Value)
    				   orderby time descending
    				   select t;
    foreach (XElement t in tracks)
    	Console.WriteLine("{0} - {1}", t.Attribute("time").Value, t.Element("name").Value);


    Отсортируем элементы по жанру, исполнителю, названию альбома, названию трека:

    IEnumerable<XElement> tracks = from t in doc.Root.Elements("track")
    				   orderby t.Attribute("genre").Value,
    						t.Element("artist").Value,
    						t.Element("album").Value,
    						t.Element("name").Value
    				   select t;
    foreach (XElement t in tracks)
    {
    	Console.WriteLine("{0} - {1} - {2} - {3}", t.Attribute("genre").Value,
    					t.Element("artist").Value,
    					t.Element("album").Value,
    					t.Element("name").Value);
    }


    Простенький запрос, выводящий количество треков в каждом альбоме:

    var albumGroups = doc.Root.Elements("track").GroupBy(t => t.Element("album").Value);
    foreach (IGrouping<string, XElement> a in albumGroups)
    	Console.WriteLine("{0} - {1}", a.Key, a.Count());


    Выводы


    После того как вы освоили пространство имен System.Xml для работы с XML на более низком уровне, смело переходите на использование System.Xml.Linq, надеюсь, написанная статья поможет это сделать быстрее, ведь не так страшен черт, как его рисуют. Как видно из примеров выше — многие вещи делать значительно проще, количество строк кода сокращается. Это дает нам очевидные преимущется, начиная со скорости разработки, заканчивая более легким сопровождением кода, написанного ранее.
    Share post

    Similar posts

    Comments 66

      0
      познавательно)
        0
        Уже кому-то пригодилось :)
        0
        Спасибо!
        Будем почитать. :)
          –1
          У меня раньше ник Poison был, а свой компьютер по сей день так называю по старой привычке :)
            –1
            Это мой ник лет уже 15 как.. :)
          +1
          Отличное описание! Одно замечание, почему бы в примере удаления не поступить проще:

          var tracks2Del = doc.Root.Descendants("track").Where(t => t.Element("artist").Value == "DMX");
          tracks2Del.Remove();

          Так нагляднее и с идеологией LINQ больше согласуется.
            0
            Да, вы правы. Я как-то попробовал сделать через Remove() у коллекции - записи не удалились, поэтому пошел другим путем.
            Наверное что-то не так пробовал, сейчас все ок :)
            0
            new XElement("album", "Wide Angle"))));doc.Save(fileName);

            сохранение 2 раза
              0
              Спасибо, поправил. Это я боролся с тем как лучше в хабр код вставлять..
              PS. В сети столько хороших конвертеров C# to HTML, а на хабре ними и не воспользуешься.. (
              0
              В примере id у трэков почему-то совпадают.
                0
                Поправил, это был вставлен из "тестовой" версии результат по ошибке.
                0
                Хабр начинает приносить мне пользу) Спасибо, Анатолий.
                  0
                  Хорошая статья. Может попробовать вставлять скриншоты кода?
                  Будет не удобно при копировании, но зато читабельно.
                    0
                    Спасибо.
                    Не знаю, может что-нить придумаю, скриншотами неудобно будет, а читабельность действительно никакая сейчас.. Да и на форматирование всего этого много времени уходит. Несовсем удобно сделано..
                      0
                      Помучился я немного, но похоже код на хабр никак красиво не запостить, сильно ограничение наложено на HTML. + непонятно почему если ставишь флаг "отключить форматирование HTML", то оно все равно половину заменяет на свое усмотрение.. :(
                        0
                        Жаль :( ждём завтра, может на суперхабре это будет удобно :)
                          +1
                          Не знаю всех тонкостей Хабра, но в голове крутится мысль о ссылке на исходники. Ну или внешние пэйджы с отформатированным кодом.
                          Но сразу же проблема — хостинг. Если в первом случае достаточно какого-нибудь ifolder'a, то во втором — сложнее.
                            0
                            Так тоже не совсем удобно будет, прийдется постоянно переключаться между окнами, что с мысли сбивает.
                            Нужно действительно до завтра подождать, вдруг что изменится в эту сторону.
                              0
                              Ну ссылка с исходниками и примером была мы действительно кстати.
                      0
                      Спасибо! Очень хотелось бы увидеть развитие темы в LINQ To SQL! :)
                        0
                        Подумаю над этим детальнее.
                        +3
                        Очень познавательно :) Когда наконец большинство хабролюдей поймёт что сишарп это гут :)
                          0
                          Если убрать в авторском коде new перед конструкторами, то получится практически Lisp. Ждем окончательной победы функционального программирования в C# 4.0 (и Java 2.0) :)
                            +2
                            Мне кажется существует большое количество .net разработчиков, которые читают хабр, но не понимая ничего в php, python, apache и ubuntu c linux, темах которые главенствуют на сайте, молчат в тряпочку. Конкретный пример - я. Хабр читаю, что-то около полугода. Решил зарегистрироваться только сегодня, когда увидел новый блог ".NET". Теперь, когда накоплю достаточно кармы, намерен пописывать опусы про интересные вещи в .net.
                              0
                              Это замечательно. А какие темы вам больше всего интересны?
                                0
                                На сегодня, пожалуй, asp.net в связке с javascript/css. Скоро появится MVC pattern, Data Entity Framework, .Net Data Services и станет еще интереснее.
                                  0
                                  А помимо MVC pattern'а еще и сам ASP.NET MVC Framework.
                                  Astoria Data Services - это действительно мощная вещь.
                                  0
                                  Мне лично было бы интересно про WPF...
                                  0
                                  Практически тот же случай..
                                  Проголосую за вашу карму. :)
                                0
                                Три замечания. Во-первых, в C# 3.0 есть "var". Во-вторых, — анонимные типы. И, наконец,
                                foreach(var track in tracks) читается лучше, нежели foreach (XElement t in tracks)
                                  0
                                  Статья ведь о том, как с XML работать, и я попытался её не перегрузить проекцией на типы, трансформацией и т.д. Именно поэтому и использовался XElement, для наглядности.
                                    0
                                    В таком случае нагляднее было бы так: foreach (XElement track in tracks)
                                      0
                                      Не совсем понял, вам не понравилось однобуквенное название переменной? Это уж на вкус и цвет. У меня циклы все на одну две строчки кода, в таких случаях абсолютно не вижу никакой необходимости писать длинные названия переменных. Если код итерации более объемный, то, безусловно, с вами соглашусь.
                                    0
                                    Не соглашусь насчет того, что var в данном контексте читается лучше. Считаю, что указание конкретного типа в foreach - это признак хороший тона хотя бы потому, что тогда контекст итераций будет самодостаточным. Разбирая в дальнейшем тело цикла не нужно будет искать определение tracks. Особенно это актуально для тяжелых циклов. Это мое личное мнение, дело вкуса. Я сам стараюсь использовать var в основном для промежуточного буфера. Например, когда LINQ-строка уже слишком длинная, но необходимо все же произвести еще какие-то действия типа ToList или Remove.
                                    0
                                    Хм. Xml в дотнете всегда меня в ступор вводит. Буду иметь в виду теперь и такой подход.

                                    Вот теперь я понимаю, что карма на самом деле вещь полезная для самопроявления =)
                                      0
                                      Ввела в ступор дата регистрации ваша. Подумал, что из будущего — не обратил внимание на год. :)
                                      Вообще с XML и старыми методами довольно таки неплохо работать, только более объемно как-то получалось.

                                      PS. А карма.. не знаю.. было бы когда материал готовить.
                                        0
                                        Кстати, предлагаю вам продолжить и заделать материал про агрегацию RSS средствами LINQ to XML. С LINQ это вообще пустяковое дело.
                                          0
                                          Наверно дело в том, что в боевых условиях я полноценно с xml не работал. Сейчас использую только как хранилище алиасов для enum'ов — а там сериализация. А вот как-то с ходу написать (или по крайней мере придумать как написать) helper для xml файлов не получается.

                                          Про будущее — сам пугаюсь в этом месяце =)

                                          А про карму: поделиться про логирование в экстэншенах хочется. =)
                                        0
                                        Сбило с толку не подключенная System.Linq в начале кода.
                                        Пожалуйста, явно указывайте новые пространства имён, а то я с 3.5 фреймворком только начинаю разбираться :).
                                          0
                                          А в какой среде вы разбираетесь? 2008 студия при указании .net 3.5 сама создает проект с подключенным неймспейсом System.Linq.
                                            0
                                            2008, тольк неймспейсы которые мне не нужны я удаляю по привычке :)
                                              0
                                              Кстати, по теме: в студии есть прикольная фича "Sort and Remove", я ей пользуюсь, когда страница приобретает законченные очертания.
                                            0
                                            Если вы создали консольное приложение под 3.5 фреймворк, то это просторанство имен подключено по умолчанию. Впредь буду указывать все что имеется.
                                            0
                                            Да, C# неплохо развивается. Для любознательных: в динамических языках можно создать XML ИМХО ещё проще и красивее. Например в Groovy с помощью builders (прошу прощения за некрасивый вид - code не работает):


                                            writer = new StringWriter()
                                            builder = new groovy.xml.MarkupBuilder(writer)
                                            invoices = builder.invoices {
                                            for(day in 1..3) {
                                            invoice(date: new Date(106,0,day)){
                                            item(count:day){
                                            product(name:'ULC', dollar:1499)
                                            }
                                            }
                                            }
                                            }

                                            На выходе получаем:
                                            <invoices>
                                            <invoice date='Sun Jan 01 00:00:00 CET 2006'>
                                            <item count='1'>
                                            <product name='ULC' dollar='1499' />
                                            </item>
                                            </invoice>
                                            <invoice date='Mon Jan 02 00:00:00 CET 2006'>
                                            <item count='2'>
                                            <product name='ULC' dollar='1499' />
                                            </item>
                                            </invoice>
                                            <invoice date='Tue Jan 03 00:00:00 CET 2006'>
                                            <item count='3'>
                                            <product name='ULC' dollar='1499' />
                                            </item>
                                            </invoice>
                                            </invoices>


                                            Вызов метода = создание элемента, параметры метода = атрибуты.
                                              0
                                              Да, сейчас в эту сторону активно все движется. В VB вообще XML поддерживается прямо из кода. Чем-то это программирование напомнило старый добырй ASP, только наоборот :)
                                              0
                                              Спасибо, отличная статья
                                              • UFO just landed and posted this here
                                                  0
                                                  Уже готовлю материал по LINQ to SQL ;) Не знаю правда насколько меня хватит во время рабочей недели, но радует, что грядут выходные снова :)

                                                  У меня тоже старые проекты под 2.0. В общем-то ничего, живут потихоньку, и я вместе с ними. В 2.0 очень жизнь облегчает по работе с данными Enterprice Library Data Application Block. Очень советую, если ещё не пользуетесь.
                                                    +1
                                                    Что-то я только тут и делаю, что с вами спорю.

                                                    DAB — это, простите, жуть и тот факт, что он входит во "фреймворк" в названии которого фигурирует магическое "Enterprise", лучше его не делает. Посмотрите лучше в сторону NHibernate/BLToolkit.
                                                      0
                                                      Да нестрашно :)
                                                      Мое мнение ведь не единственно правильное.

                                                      Вообще я DAAB изначально начал использовать как только перешел от ручного кодирования, мои задачи эта библиотека выполняла вполне нормально. Насколько я понял предложенные вами - это ORM решения, а это для меня было немного с избытком. У DAAB и NHibernate, на мой взгляд, немного разные сферы применения изначально.
                                                        0
                                                        BLToolkit — это не ORM, а просто маппер из всего во всё (в том числе и в/из DataSet'ов и DataTable'ов). Удобнее DAAB'а на несколько порядков.
                                                          0
                                                          BLToolkit может, а NHibernate — ORM.
                                                          Возможно мне не повезло, что оно мне первым не попалось. Говорю без иронии. Просто все время разбираться с новыми библиотеками тоже не правильно. По NHibernate в свое время не нашел какой-то хорошей статьи с обзором возможностей, в общем не зацепило тогда когда смотрел.

                                                          Не хотите написать краткий обзор? :) Я думаю тема интересна многим будет.
                                                            0
                                                            Посмотрите на IdeaBlade DevForce, http://ideablade.com/.
                                                            Отлично докумментированный фреймворк. Их ORM показался мне очень удобным
                                                              0
                                                              Спасибо большое, посмотрю, но, думаю, уже .NET стал более самодостаточным. Буду пробовать обходиться "своими" силами в последующий проектах.
                                                    0
                                                    А почему приходится???
                                                    Уж что что, а .NET-то обновляется вообще запросто! И он обратно совместим.
                                                    • UFO just landed and posted this here
                                                    0
                                                    Воо, здорово! LINQ - это однозначно будущее .NET. Спасибо за статью, я надеюсь многих .NET-чиков, которые еще не пользуются, заставит задуматься о том, что стоит попробовать LINQ ;)
                                                      0
                                                      Можно еще удалить все записи и в функциональном стиле: :-)
                                                      <pre>
                                                      doc.Root.Descendants ( "track" ).Where (
                                                      t => t.Element("artist").Value == "DMX").ToList ( ).ForEach (
                                                      t => t.Remove ( ) );
                                                      <pre></code>
                                                        0
                                                        Ну для удаления это уже совсем жестоко :)
                                                        0
                                                        А можно узнать как использовать свою функцию сравнения двух элементов в сортировке?(только попроще - только начинаю изучать XML и LINQ)
                                                          0
                                                          В работе как-то не приходилось писать такое, но для сортировки треков по длине названия получилось следующее:
                                                          1. Нужно создать свой класс, реализующий интерфейс IComparer<>:

                                                          class MyComparer<T> : IComparer<string>
                                                          {
                                                          public int Compare(string x, string y)
                                                          {
                                                          return x.Length.CompareTo(y.Length);
                                                          }
                                                          }

                                                          2. Отсортировать треки, используя наш компарер:
                                                          var tracks = doc.Root.Elements("track").OrderBy(x => x.Element("name").Value, new MyComparer<string>());

                                                          Первым параметров в OrderBy передаем, используя лямбда выражения, то, по чему мы хотим отсортировать наши записи. Вторым - экземпляр класса MyComparer, который мы только что описали.

                                                          Уж сори, что пишу код простым текстом, но ни poison.qsh.ru корректно не работает, и хабр че-то начал теги жевать. Думаю вы разберетесь что к чему ;)
                                                            0
                                                            Спасибо, изложил очень доступно; респект за статьи, побольше таких! :)
                                                              0
                                                              Спасибо за отзыв :)
                                                              Ещё пару тем есть, которые хотел бы осветить, но нету времени совсем.. Как появится, так сразу продолжу что-то писать.
                                                          0
                                                          В избранное!
                                                            0
                                                            Образцовый туториал. Спасибо большое, Анатолий! Надо было мне сначал гуглить на русском… какая редкость))
                                                              0
                                                              Наконец-то нормальная статья! Давно такое искал!

                                                              Only users with full accounts can post comments. Log in, please.