воскресенье, 25 ноября 2012 г.

Хранение однотипных групп пользовательских настроек, созданных во время выполнения приложения [Visual Studio, .NET]

[Using of the user scope settings for the storing the similar setting groups in run-time]

Платформа .NET и Visual Studio предлагает относительно развитые возможности для простого использования настроек приложения. Это, например, разделение настроек на пользовательские (UserScope) и программные (AppScope), их статическая типизация, наличие встроенного в Visual Studio дизайнера и прочие вкусности. Неплохой обзор для VS 2005, но все еще актуальный, можно посмотреть здесь:
http://msdn.microsoft.com/en-us/library/aa730869(v=vs.80).aspx

Всего этого достаточно, чтобы покрыть большинство потребностей небольшого приложения. Но если вам когда-нибудь понадобиться выйти за границы статического объявления настроек и/или захочется хранить данные в своем формате и тем не менее оставаться в рамках стандартных механизмов .NET, вам, скорее всего, придется разбираться с классами ConfigurationManager и ConfigurationSection:
http://msdn.microsoft.com/en-us/library/system.configuration.configurationmanager(v=vs.100).aspx

Так случилось и со мной, мне понадобилось хранить группы определенных настроек, количество которых определялось во время выполнения программы. Группы нужно было идентифицировать по некоторому ключу, в моем случае им была уникальная строка. Но в своем решении я избежал сложностей испльзования ConfigurationManager, а просто “вложил” свою группу настроек в одну пользовательскую установку. Решение получилось достаточно простым, но не совсем очевидным, поэтому с удовольствием спешу поделиться этим знанием :)

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


Здесь-то мы и добавим наш “контейнер” групп, но для начала нужно его объявить.

Для многократного использования общей конструкции я использовал общий (generic) класс. В моей задаче мне нужно было ко всему прочему иметь “глобальное” значение, которое бы использовалось как стандартное для всех новых и/или неопределенных значений. Ниже код:

public class SettingSetBase<T>
{
   public SettingSetBase() { // ctor
     KeyValList = new List<KeyVal<T>>();
     DefaultValue = default(T);
   }

   public SettingSetBase(T defVal) { // ctor
     KeyValList = new List<KeyVal<T>>();
     DefaultValue = defVal;
   }

   public T DefaultValue { get; set; }
   public List<KeyVal<T>> KeyValList { get; set; }

   public T GetValue(string key) {
     KeyVal<T> keyVal = KeyValList.FirstOrDefault(x => String.Equals(x.Key, key, StringComparison.CurrentCultureIgnoreCase));
     if (keyVal == null) return DefaultValue;
     else return keyVal.Val;
   }

   public void SetValue(String key, T val) {
     KeyVal<T> keyVal = KeyValList.FirstOrDefault(x => String.Equals(x.Key, key, StringComparison.CurrentCultureIgnoreCase));
     if (keyVal == null)
       KeyValList.Add(new KeyVal<T>() { Key = key, Val = val });
     else
       keyVal.Val = val;
   }
}

public class KeyVal<T>
{
   public String Key { get; set; }
   public T Val { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

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

Теперь этот generic-класс можно “конкретизировать” любым сериализуемым типом:

// Например для String:
public class SettingSetString : SettingSetBase<String>
{
   public SettingSetString() { }
   public SettingSetString(String defVal) : base(defVal) { }
}

// Или для булевых значений:
public class SettingSetBool : SettingSetBase<bool>
{
   public SettingSetBool() { }
   public SettingSetBool(bool defVal) : base(defVal) { }
}

// Или для глобального уникального идентификатора (GUID):
public class SettingSetGuid : SettingSetBase<Guid>
{
   public SettingSetGuid() { }
   public SettingSetGuid(Guid defVal) : base(defVal) { }
}

// Или даже для его обнулямого варианта:
public class SettingSetGuidNullable : SettingSetBase<Guid?>
{
   public SettingSetGuidNullable() { }
   public SettingSetGuidNullable(Guid? defVal) : base(defVal) { }
}


* This source code was highlighted with Source Code Highlighter.

Конечно же, элементарными типами дело не ограничивается и можно использовать комплексные xml-сериализуемые типы, как например этот для хранения параметров окон:
public class WinPosSett
{
  public Point Location { get; set; }
  public int Width { get; set; }
  public int Height { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

Теперь все готово, можно объявить контейнеры настроек в дизайнере. Ограничусь для демонстрации двумя видами - SettingSetString и SettingSetWinPosSett:

Замечание! Что бы эти типы стали доступными для выбора, необходимо пересоздать проект.

Как вы может быть уже заметили, для таких типов в дизайнере невозможно определить стандартное значение (колонка “Value”, или “Wert“ у меня в немецкой студии). Поэтому просто, не усложняя, расширим созданный генератором класс настроек следующим методом:

internal sealed partial class Settings
{
   public void InitDefaults() {
     if (MyStringSet == null)
       MyStringSet = new SettingSetString("DefaultValue");
     if (MyWinPosSet == null)
       MyWinPosSet = new SettingSetWinPosSett(new WinPosSett() { Height = 300, Width = 300, Location = new Point(100, 100) });
   }
}


* This source code was highlighted with Source Code Highlighter.

Ну все, можно пользоваться:
Properties.Settings.Default.InitDefaults();
String strGroup1 = Properties.Settings.Default.MyStringSet.GetValue("Group1"); // "DefaultValue"
Properties.Settings.Default.MyStringSet.SetValue("Group1", "My new Value");
Properties.Settings.Default.Save();
String strGroup1NewValue = Properties.Settings.Default.MyStringSet.GetValue("Group1"); // "My new Value"
String strGroup2 = Properties.Settings.Default.MyStringSet.GetValue("Group2"); // "DefaultValue"


* This source code was highlighted with Source Code Highlighter.

Вот как выглядит часть файла user.config после выполненных операций:

<setting name="MyStringSet" serializeAs="Xml">
  <value>
    <SettingSetString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <DefaultValue>DefaultValue</DefaultValue>
      <KeyValList>
        <KeyValOfString>
          <Key>Group1</Key>
          <Val>My new Value</Val>
        </KeyValOfString>
      </KeyValList>
    </SettingSetString>
  </value>
</setting>


* This source code was highlighted with Source Code Highlighter.

Неплохо! Теперь попробуем использовать комплексный тип WinPosSett:
WinPosSett WinPosMainWindow = Properties.Settings.Default.MyWinPosSet.GetValue("MainForm");
// .. используем значения для установки размеров и позиции окна
// ..
// .. окно закрывается, сохраняем значения:
Properties.Settings.Default.MyWinPosSet.SetValue("MainForm", new WinPosSett() { Height = 400, Width = 200, Location = new Point(10, 10) });
Properties.Settings.Default.Save();


* This source code was highlighted with Source Code Highlighter.

Сохраненные настройки выглядят так:
<setting name="MyWinPosSet" serializeAs="Xml">
<value>
   <SettingSetWinPosSett xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
     <DefaultValue>
       <Location>
         <X>100</X>
         <Y>100</Y>
       </Location>
       <Width>300</Width>
       <Height>300</Height>
     </DefaultValue>
     <KeyValList>
       <KeyValOfWinPosSett>
         <Key>MainForm</Key>
         <Val>
           <Location>
             <X>10</X>
             <Y>10</Y>
           </Location>
           <Width>200</Width>
           <Height>400</Height>
         </Val>
       </KeyValOfWinPosSett>
     </KeyValList>
   </SettingSetWinPosSett>
</value>
</setting>


* This source code was highlighted with Source Code Highlighter.

Пример приложения можно скачать здесь:
https://sites.google.com/site/selosite/GroupUserSettings.zip

воскресенье, 30 сентября 2012 г.

Добавление авто-подписи к каждому сообщению на blogspot

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

В blogspot можно создать шаблон, который используется при создании новых сообщений. Но если вы когда-нибудь захотите изменить авто-подпись, придется править ВСЕ сообщения вручную. Других стандартных способов для решения этой проблемы я не нашел и .. изобрел свой :)

Для этого было решено воспользоваться JavaScript для динамического изменения DOM (Document Object Model) страницы. JavaScript можно добавить на блог как гаджет в его дизайнере:



Подсмотрев код HTML блога установил, что все посты блога заключены в элемент DIV с классом "post-body entry-content". Этого абсолютно достаточно знать, что бы как я, не зная JavaScript (лишь смутные представления) при помощи поисковика накодить следующий скрипт:

<script type='text/javascript'>
  // Тест, что DIV элементы скриптом находятся:
  // var posts = document.getElementsByClassName("post-body entry-content");
  // document.write("Posts: " + posts.length);

  var posts = document.getElementsByClassName("post-body entry-content");
  for (var i=0; i<posts.length; i++) {
    newDiv = document.createElement("div");

    // a) Содержимое DIV можно наполнить так:
    // newLine1 = document.createTextNode("Этот текстовый элемент создан при помощи createTextNode()");
    // newDiv.appendChild(newLine1);

    // b) но мы воспользуемся другим способом: newDiv.innerHTML = '<li>text</li>';
    str = "<span style='font-size: smaller;'>Это первая строка HTML-кода.</span><BR/><span style='font-size: smaller;'>Вторая строка.</span>";
    newDiv.innerHTML = str;

    // Добавим созданный DIV после DIV-сообщения.
    // К сожалению, не существует insertAfter-функции, поэтому эмулируем ее с помощью "parentElement" и "nextSibling":
    posts[i].parentElement.insertBefore(newDiv, posts[i].nextSibling);
  }
</script>


* This source code was highlighted with Source Code Highlighter.


Название в окне настроек гаджета я оставил пустым - теперь о наличии скрипта можно только догадываться.


Результат вы можете посмотреть на моем клипарт-блоге

Ссылки по теме:
Document Object Model (http://en.wikipedia.org/wiki/Document_Object_Model)
document.getElementsByClassName (https://developer.mozilla.org/en-US/docs/DOM/document.getElementsByClassName)
Adding elements to the DOM (http://www.javascriptkit.com/javatutors/dom2.shtml)

понедельник, 24 сентября 2012 г.

Автоматическое изменение размеров FlowLayoutPanel в XtraLayoutControl (DevExpress)


[FlowLayoutPanel with autosize in XtraLayoutControl]

Работая с библиотекой элементов управления для WinForms от DevExpress в ходе практики накопилась небольшая тележка различных решений, обходных путей и просто интересных подходов. Со временем планирую их здесь опубликовать. Ну а пока, покажу, как я реализовал автоматическое изменение размеров FlowLayoutPanel в XtraLayoutControl, так чтобы все элементы, помещенные в FlowLayoutPanel, были всегда видны.

На рисунках отображен уже конечный результат, которого я хотел добиться:






XtraLayoutControl отлично управляет размерами и расположениями “своих” элементов управления из библиотеки DevExpress, но совершено не справляется со стандартными элементами управления, к которым относится FlowLayoutPanel. Дело в том, что XtraLayoutControl взаимодействует с элементами своеобразным образом и требует от элементов реализации специфичных интерфейсов. В нашем случае достаточно использовать интерфейс IXtraResizableControl, который я реализовал в унаследованном от панели классе:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using DevExpress.XtraLayout;
using System.Drawing;
using DevExpress.Utils.Controls;

namespace WindowsFormsApplication9
{
    public class MyFlowLayoutPanel : FlowLayoutPanel, IXtraResizableControl
    {
        public MyFlowLayoutPanel() { // ctor
            
        }

        LayoutControl rootLayoutControl = null;
        LayoutControlItem parentControlItem = null;

        protected override void OnHandleCreated(EventArgs e) {
            base.OnHandleCreated(e);

            if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime)
                return;

            LayoutControl parent = this.Parent as LayoutControl;
            if (parent != null) {

                this.rootLayoutControl = parent;
                this.parentControlItem = FindItemByContentControl(parent.Root, this);

                CalcAndUpdateSize();
                this.rootLayoutControl.Resize += (x, y) => { CalcAndUpdateSize(); };
                this.ControlAdded += (x, y) => { CalcAndUpdateSize(); };
                this.ControlRemoved += (x, y) => { CalcAndUpdateSize(); };
            }
        }

        void layoutControl_Changing(object sender, EventArgs e) {
            CalcAndUpdateSize();
        }

        // REM. http://www.devexpress.com/Support/Center/p/Q396747.aspx (Hot to find a conatner LayoutcontrolItem of a control)
        LayoutControlItem FindItemByContentControl(LayoutControlGroup layoutGroup, Control contentControl) {

            foreach (BaseLayoutItem baseLI in layoutGroup.Items) {
                LayoutControlItem lci = baseLI as LayoutControlItem;
                if (lci != null && lci.Control == contentControl)
                    return lci;

                LayoutControlGroup lcg = baseLI as LayoutControlGroup;
                if (lcg != null)
                    return FindItemByContentControl(lcg, contentControl);
            }
                
            return null;
        }


        private void CalcAndUpdateSize() {
            if (parentControlItem == null || parentControlItem.ViewInfo == null) return;

            this.MaximumSize = new Size(parentControlItem.ViewInfo.ClientArea.Width, 0);
            RaiseSizeChanged();
        }

        #region IXtraResizableControl Member

        event EventHandler ChangedCore;
        event EventHandler IXtraResizableControl.Changed {
            add { ChangedCore += value; }
            remove { ChangedCore -= value; }
        }

        protected void RaiseSizeChanged() {
            if (ChangedCore != null) ChangedCore(this, EventArgs.Empty);
        }

        bool IXtraResizableControl.IsCaptionVisible { get { return false; } }

        Size IXtraResizableControl.MaxSize { get { return new Size(0, this.PreferredSize.Height); } }

        Size IXtraResizableControl.MinSize { get { return new Size(0, this.PreferredSize.Height); } }

        #endregion
    }
}

Ссылки по теме:

IXtraResizableControl Interface:
http://documentation.devexpress.com/#WindowsForms/clsDevExpressUtilsControlsIXtraResizableControltopic
FlowLayoutPanel with autosize and layout control:
http://www.devexpress.com/Support/Center/p/Q292593.aspx

Вот собственно и все. Исходный код можете скачать здесь

среда, 15 августа 2012 г.

Алфавитное упорядочивание файлов, созданных цифровой камерой Canon

[Alphabetical ordering of files created by digital cameras Canon]

Моя зеркальная камера Canon EOS 600D присваивает, в некоторых случаях, именам файлов префикс “_MG_” вместо ”IMG_”. Данная “проблема” известна также владельцам многих других моделей зеркальных камер от Canon. Это происхоит в том случае, если в установках для Цветового пространства выбрано значение “Adobe RGB” (у меня доступно в творческих режимах). Согласно документации, “Adobe RGB” используется для промышленной обработки фотографии и часто в при распечатке на несовместимых с этим форматом принтерах цвета выглядят несколько приглушенными. При съемке в сюжетных режимах автоматически используется значение “sRGB”.

Так или иначе, если вы пользуетесь обоими режимами (или пользовались ранее и теперь не будете :) ) на выходе получается коллекция фотографий, которую тяжело обрабатывать. Для упорядочивания файлов в порядке их съемки, их нужно соответственно переименовать. Для пользователей линукс есть отличное решение - это программа rename, которая является частью многих дистрибутивов. Чтобы, в конце концов, отличать используемые установки по имени файла, я добавил эту информацию в его конец, выразив ее пост-фиксом “_AdobeRGB”:

rename -v 's/^(_)(MG_.*)\.JPG/I$2_AdobeRGB\.JPG/g' *


Файлы вида “_MG_1234.JPG” переименовываются в “IMG_1234_AdobeRGB.JPG”.

Для предварительного просмотра результатов операции без фактического переименования, можно использовать параметр “-n”. В общем, смотрите документацию к команде, я сам лично обожаю подробные описания wiki на ubuntuusers.de [http://wiki.ubuntuusers.de/rename] на немецком языке.

понедельник, 23 июля 2012 г.

UnityContainer. Передача параметра конструктора в метод Resolve()

[UnityContainer. Pass constructor parameters to Resolve() method]

В одном своем проекте использую UnityContainer - расширяемый DI-контейнер (dependency injection container) от Misrosoft. И понадобилось мне передавать в конструкторы создаваемых контейнером сервисов объекты (DataContext), временем жизни которых я управлял сам. То есть мне понадобилось примерно следующее:

// работаем с одним контекстом данных
DataContext dx1 = container.Resolve();
container.Resolve(/*dx1*/); // конструктор сервиса: public Service1(DataContext dx)
container.Resolve(/*dx1*/); // конструктор сервиса: public Service2(DataContext dx)

// работаем с другим контекстом
DataContext dx2 = container.Resolve();
container.Resolve(/*dx2*/);

Оказалось, что для этой довольно типичной задачи UnityContainer не предлагает однозначного решения. Это обстоятельство порождает множество подходов, которые можно найти в интернете. Одним из таких решений является разрешение (resolve) объекта с переопределением его параметра:

DataContext dx1 = container.Resolve();
Service1 srv1 = container.Resolve(new ParameterOverride("dx", dx1));

Но здесь нужно быть крайне осторожным, т.к. при изменении имени параметра, контейнер без какого-либо предупреждения инициализирует сервис им вновь созданным объектом DataContext, а наш объект будет попросту проигнорирован.

Многие другие найденные в интернете решения немного модернизированные базировались на указанном подходе. При этом имена параметров извлекались при помощи рефлексии, а реализация заворачивалась в метод расширение (extension).

Другой, приемлемый, на мой взгляд, подход - вынос инициализации сервисов контекстом в отдельный метод:

DataContext dx1 = container.Resolve();
Service1 srv1 = container.Resolve();
Service2 srv2 = container.Resolve();
srv1.Init(dx1);
srv2.Init(dx1);

Я же задействовал потомка контейнера (ChildContainer), что, как мне показалось, является наиболее простым и естественным способом для решения поставленной задачи. Вот что у меня получилось:

DataContext dx1 = container.Resolve();
IUnityContainer childContainer = container.CreateChildContainer();
childContainer.RegisterInstance(dx1, new ExternallyControlledLifetimeManager()); // !!!
Service1 srv1 = childContainer.Resolve();
Service2 srv2 = childContainer.Resolve();
childContainer.Dispose();

Идея проста: В потомке контейнера регистрируем необходимую инстанцию DataContext, создаем с его помощью объект сервиса (Resolve) и в конце заметаем за собой следы (Dispose). Обратите внимание на указание внешнего контроллера времени жизни объекта dx1 (ExternallyControlledLifetimeManager) - тем самым мы исключаем DataContext из утилизации при вызове метода childContainer.Dispose().

Этот код лег в основу метода-расширения, которое я использовал в своем проекте:

public static class UnityExtensions
{
  public static T xResolve(this IUnityContainer container, DataContext dx) {
    using (IUnityContainer childContainer = container.CreateChildContainer())
    {
      childContainer.RegisterInstance(dx, new ExternallyControlledLifetimeManager());
      return childContainer.Resolve();
    }
  }
}

Теперь вызовы выглядят так:

DataContext dx1 = container.Resolve(); // Первая инстанция
Service1 srv1 = container.xResolve(dx1);
Service2 srv2 = container.xResolve(dx1);

DataContext dx3 = container.Resolve(); // Вторая инстанция
Service1 srv3 = container.xResolve(dx3);


PS: К концу написания статьи натолкнулся на еще один способ реализации необходимого функционала - использование переопределения зависимости DependencyOverride:

container.Resolve(new DependencyOverride(dx1));

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

понедельник, 2 июля 2012 г.

Исключение файлов из списка копирования xcopy в build событиях Visual Studio (2010)

[Exclude files using xcopy in the build events of Visual Studio (2010)]

Для копирования файлов папки по pre-/post build событиям студии можно использовать команду командной строки - xcopy. Команда имеет множество параметров для самых различных сценариев, полный список которых можно посмотреть, набрав в интерпретаторе cmd “xcopy /?” или, например, здесь (http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/xcopy.mspx?mfr=true). Я, в процессе использования этой команды не имея большого опыта работы с командной строкой и ее инструкциями, столкнулся с рядом не совсем очевидных для меня проблем. Ниже их краткое описание и их решения.

Задача 1: Копирование обновленных файлов одной директории в другую после  построения проекта.

Моя команда копирования в “командной строке события после построения” (согласен, звучит сложно, но из песни слов не выкинешь - это официальное название  командной строки post-build событий в VisualStudio) с использованием макросов выглядела так:

xcopy "$(ProjectDir)SourceFiles" "$(ProjectDir)TargetFiles" /d /e /i

Здесь все выглядит довольно очевидно, но все же краткое описание используемых параметров:
/d - для копирования измененных файлов,
/e - копирование директорий и поддиректорий, даже если они пусты,
/i - будет создана папка назначения, если она не существует.

Замечание: Команда xcopy “чувствительна” к символу “\” в конце имени источника. Не стоит его подставлять, если источником копирования является папка. Команда завершится ошибкой:

xcopy "$(ProjectDir)SourceFiles\" "$(ProjectDir)TargetFiles" /d /e /i REM   (НЕВЕРНО)

Но для копирования всех файлов и под-папок директории можно использовать маску “*.*”. Следующая команда сработает как надо и эквивалентна первому рабочему варианту:

xcopy "$(ProjectDir)SourceFiles\*.*" "$(ProjectDir)TargetFiles" /d /e /i

Задача 2. Исключение определенных файлов из списка копирования

Согласно справке для исключения файлов можно использовать параметр “/exclude” и его синтаксис в справке выглядит довольно просто:

/exclude:filename1[+[filename2]][+[filename3]] : Specifies a list of files containing strings.

Но, к сожалению, использование этой инструкции не так уж тривиально и вызвало у меня ряд затруднений. Например здесь filename1..N не имена исключаемых файлов, а имена файлов, которые содержат описания исключаемых файлов (по одному на каждую строку). Т.е. чтобы исключить файл “a.txt” нужно создать текстовый файл “excludeList.txt” в папке проекта и внести в него строку “a.txt”. Команда копирования файлов приобретает вид:

xcopy "$(ProjectDir)SourceFiles\*.*" "$(ProjectDir)TargetFiles" /d /e /i /exclude:$(ProjectDir)excludeList.txt

Но эта команда в ряде случаев не может быть выполнена и возвращает ошибку. Эта проблема часто связана с пустыми символами в пути к файлу, которые “разрывают” команду так, что она не может быть правильно интерпретирована. Поэтому мы и обрамили пути источника и цели в кавычки (“), но параметр /exclude не допускает их использования. Чтобы обойти эту проблему можно использовать относительный путь без кавычек, предварительно перейдя в нужную папку при помощи команды cd:

cd "$(ProjectDir)"
"$(ProjectDir)SourceFiles\*.*" "$(ProjectDir)TargetFiles" /d /e /i /exclude:excludeList.txt

Теперь команда будет выполнятся, но исключаемый файл “a.txt” все равно окажется в целевой папке. Немножко помучившись выяснил (= случайно обнаружил), что первая строка файла excludeList.txt игнорируется интерпретатором и для успешного выполнения команды необходимо сместить описания на одну строчку ниже. Файл “excludeList.txt”:

[CR][LF]
a.txt”

На этом можно было бы поставить точку, но я не был удовлетворен тем, что описание исключаемых файлов скрыто от глаз в отельном файле. Мне  захотелось все иметь в одном месте. Для этого файл с описанием исключений можно создавать прямо в скрипте (здесь исключаются файлы “a.txt” и “b.txt”):

cd "$(ProjectDir)"
echo a.txt>EXCLUDE_LIST
echo b.txt>>EXCLUDE_LIST
REM или одной строкой: (echo a.txt&& echo b.txt)>EXCLUDE_LIST
xcopy "$(ProjectDir)SourceFiles\*.*" "$(ProjectDir)TargetFiles" /d /e /i /exclude:EXCLUDE_LIST

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

Замечание. В списке исключаемых файлов не допускается использование символов маски (такие как “*” или “?”), но сами значения, если выражаться “языком шаблонов поиска файлов” как бы обрамляются звездочкой. Например для записи “a.txt” будет происходить исключение файлов соответствующих маске “*a.txt*”.

воскресенье, 24 июня 2012 г.

Генерация Assembly Version при помощи T4 шаблонов

[Generation of Assembly Version using the T4 templates]

Assembly Version используется .NET фреймворком во время построения и во время исполнения приложения для определения загружаемой сборки. Другими словами, Assembly Version является важной частью имени сборки, ее однозначно определяющей.

Assembly File Version является номером версии файла (отображается в Windows Explorer) и НЕ ИСПОЛЬЗУЕТСЯ .NET-ран-таймом для локализации сборки.

Эти версии указываются в атрибутах сборки и как правило находятся в файле AssemblyInfo.cs:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]


Если сборки создаются очень часто, ручное изменение значений указанных атрибутов становится накладным. Для автоматического увеличения номера построения (Build Number) и ревизии (Revision) можно использовать символ звездочки (*):
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.0.*")]


Компилятор сгенерирует номера самостоятельно, и этого в большинстве случаев будет достаточно. Но выдаваемые номера трудно поддаются интерпретации , и если хочется наделить числа большим смыслом, например, привязать к дате/времени, придется делать это при помощи сторонних средств. Для этих целей есть множество утилит и надстроек к Visual Studio в виде AddIn-ов, я же воспользуюсь встроенными средствами Visual Studio - генерацей кода по текстовым шаблонам: Text Template Transformation Toolkit, или коротко T4. При помощи этого инструмента можно генерировать заветные строки самостоятельно, наделив нумерацию необходимой логикой. У меня в примере нумерация привязана к году, месяцу, числу, и часу, когда создается сборка. Добавьте новый файл шаблона в проект, и назовите его, например, “AssemblyVersion.tt”:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
// Assembly Version
// Generated at <#= DateTime.Now #>
//
// [major version].[minor version].[build number].[revision number]
// build number = yy MM = Year Month (e.g.: 1102 = 2011 February)
// revision number = ddHH = Day Hour (e.g.: 1411 = 14 "February" at 11 )
//
// to generate for every build in BuildEvents:
// "%CommonProgramFiles%\Microsoft Shared\TextTemplating\10.0\TextTransform.exe" "$(ProjectDir)AssemblyVersion.tt" -out "$(ProjectDir)AssemblyVersion.cs"

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyVersion("1.0.<#= DateTime.Now.ToString("yyMM") #>.<#= DateTime.Now.ToString("ddHH") #>")]
[assembly: AssemblyFileVersion("1.0.<#= DateTime.Now.ToString("yyMM") #>.<#= DateTime.Now.ToString("ddHH") #>")]


Генерация кода по файлам шаблонов (*.tt) происходит автоматически при сохранении изменений в шаблоне или при явном вызове соответствующей команды (например из контекстного меню). Также можно попросить Visual Studio обработать все шалоны проекта. Но нам нужно добиться генерации при каждом построении (build) проекта. Для этого можно воспользоваться этим (http://msdn.microsoft.com/ru-ru/library/ee847423.aspx) руководством от MS "Создание кода в процессе построения".  Я же вызываю генератор шаблонов из командной строки в Prebuild-событии:

"%CommonProgramFiles%\Microsoft Shared\TextTemplating\10.0\TextTransform.exe" "$(ProjectDir)AssemblyVersion.tt" -out "$(ProjectDir)AssemblyVersion.cs"


Не забудьте удалить или закомментировать ненужные уже строки в файле AssemblyInfo.cs!


Замечания:
- Не уверен, но кажется, что генерация по шаблонам T4 не поддерживаются в Visual Studio Express - версии.
- Вам не обойтись от изменения версии файла (AssemblyFileVersion) если вы в проекте установки (Setup Project) используете параметр "RemovePreviousVersion=true". Файлы обновляются, только если имеют большую версию, чем уже установленые.
- Для проекта типа Class Library (Visual Basic) в Visual Studio 2010 автоматическая генерация версии при помощи знака (*) не срабатывает. Это известная проблема описана здесь (https://connect.microsoft.com/VisualStudio/feedback/details/588047), поэтому для автоматической генерации вам, наверное, придется использовать "внешние" средства, такие как T4.
- Если необходимо, чтобы все сборки проекта имели одинаковый номер версии, можно воспользоваться следующим способом:
    -- Удалите определяющие версию атрибуты во всех проектах (файлы AssemblyInfo).
    -- В одном из проектов создайте файл с атрибутами версии (AssemblyVersion, AssemblyFileVersion). Можно воспользоваться описанной в статье кодо-генерацией.
    -- Добавьте этот файл к каждому из проектов, но КАК ССЫЛКУ (as link).

------------------------------------------

Ресурсы:
How to use Assembly Version and Assembly File Version
http://support.microsoft.com/kb/556041/en-us

Создание кода и текстовые шаблоны T4
http://msdn.microsoft.com/ru-ru/library/bb126445

Official T4 team blog - официальный блог команды разработчиков t4
http://blogs.msdn.com/b/t4/
И их официальный блог примеров
http://t4talk.codeplex.com/

среда, 13 июня 2012 г.

Visual Studio Setup Project. Сохранение и восстановление установочного пути в реестре

[Visual Studio Setup Project. Reading and writing the installation path in the registry]

Setup Project в VisualStudio можно настроить так, чтобы перед установкой программы предыдущая версия удалялась: RemovePreviousVersion = true. На всякий случай напомню, что для корректной работы этого параметра необходимо увеличивать версию установочной программы - свойство Version.


Но есть одна неприятная вещь: путь, предлагаемый в диалоге выбора пути установки, полностью игнорирует выбор, сделанный пользователем в предыдущий раз. Установка удаляет старую версию программы и начинает новую практически с чистого листа. О сохранении каких-либо параметров нужно беспокоится самому. К счастью, есть возможность сделать это относительно просто, не написав, при этом, ни строчки кода. Решение проверено в немецкой Visual Studio 2010 (соответствующие “немецкие” скриншоты, не пугайтесь), уверен будет работать в VS 2008, и наверняка в VS 2005. Для этого нужно выполнить следующие шаги.

1. Установка пути по умолчанию. Во время первой установки необходимо предложить путь и здесь для вас ничего не меняется - определите DefaultLocation как вы это делали раньше (обычно это пусть к каталогу "Program Files" и далее в папку вашей фирмы и/или вашего продукта). Значение этого пути сохраняется во внутренней служебной переменой [TARGETDIR], пользователь может изменить его в диалоге выбора пути установки.


 2. Сохраняем путь в реестре. Для этого нужно определить создаваемую во время установки в реестре строку, например, как у меня под именем “HKEY_LOCAL_MACHINE\Software\[Manufacturer]\InstallPath”. Программа установки присвоит ей значение переменной “[TARGETDIR]”, которая будет содержать установочный путь.


3. Чтение пути из реестра. Для этого нужно определить "условие для запуска" (мой вольный перевод, лучше смотрите скриншот), которое читает реестр и сохраняет значение в определенной переменной. Установите в полях RegKey, Root и Value значения, которые вы задали для сохраняемого пути. И, наконец, трюк, который позволяет этому работать - в поле Property задайте имя переменной TARGETDIR.


PS. До этого решения мне помогла додуматься статья http://support.microsoft.com/kb/827026. Основное отличие от моего заключается в том, что в ней строковое значение реестра читается в пользовательскую переменную, которая присваивается полю “DefaultLocation” (свойство “пути приложения”). Но если ключа еще не существует (самая первая установка), путь остается не определен, и пользователь увидит, скорее всего, в диалоге выбора пути “C:\”.

вторник, 1 мая 2012 г.

Ubuntu, File Roller и крякозяблы в именах файлов

В Ubuntu для работы с RAR-архивами традиционно используются две программы: rar и unrar. Обе поддерживаются стандартным гномовским менеджером архивов - File Roller-ом. Первая, rar, предназначена для создания и распаковывания rar-архивов, но, к сожалению, не всегда справляется должным образом со второй задачей. В частности проблемы возникают при распаковке архивов созданных в Windows, где имена файлов/папок содержат русские буквы. Также, судя по постам в интернете, проблемы возникают и с зашифрованными архивами. Поэтому для распаковки, лучше всего воспользоваться программой unrar, вторым коммерческим продуктом создателя формата. Эта утилита прекрасно распаковывает rar-архивы, но, как следует из ее названия не предназначена для их создания. Для любителей всего свободного существует другая, но также несколько ограниченная версия распаковщика - unrar-free. Под свободной здесь понимается то, что проприетарный формат RAR и его основной закрытый функционал был исследован и реализован при помощи реверс-инжиниринга.


Для тех, кто, как я, предпочитает пользоваться графическим интерфейсом, в частности менеджером File Roller одновременное использование rar для упаковки и unrar для распаковки не представляется возможным. Если в системе установлен rar, наличие unrar просто игнорируется. Я даже глянул в исходники File Roller-а, чтобы убедиться в этом. Во всех местах, связанных с извлечением данных присутствует такой код:

if (have_rar ())
       fr_process_begin_command (comm->process, "rar");
else
       fr_process_begin_command (comm->process, "unrar");


Итог: Для приемлемой работы с RAR-архивами можно воспользоваться полумерой: установить unrar и удалить rar. Нужно лишь отказаться от создания RAR-архивов (в мире Linux это чуждый формат), в пользу другого, например zip.


суббота, 7 апреля 2012 г.

Меняем кодировку ID3 из cp1251 в Unicode. Ubuntu

Установка:
sudo apt-get install python-mutagen 

Создаем Id3v2 в кодировке utf8 из тега id3v1 и кодировки cp1251 для одного файла. Тег id3v1 удаляется:
mid3iconv -e CP1251 -d bad.mp3

То же, что и выше, но для всех файлов коллекции:
find /media/Music/ -iname "*.mp3" -print0 | xargs -0 mid3iconv -e CP1251 -d

Источник:

Вебсервер в Ubuntu в VirtualBox

Данное HowTo описывает процесс создания веб-сервера Ubuntu в виртуальной машине VirtualBox. Сервер был поднят мной для разработки и тестирования веб-приложений. Описание не претендует на полноценное руководство, описаны лишь основные шаги и признано лишь служить небольшой опорой, в первую очередь для меня самого, если этим когда-нибудь придется заняться еще раз.

У меня хост-система тоже Ubuntu (12.04 beta), но думаю, что это не играет какую-то важную роль, упомяну лишь, что VirtualBox 4.1.10 был установлен из репозиториев дистрибутива.

1. Веб-сервер - Ubuntu 10.04 LTS, новая установка.
2. Установка LAMP через Synaptic-Пакет-менеджер (Меню Edit=>установка пакетов по задачам=>LAMP server.
3. После установки заменил режим сети в VirtualBox с NAT на Bridged networking
4. Для создания нескольких виртуальных хостов на одном сервере использовал следующие руководства:
    - http://blog.axshavan.ru/2010/11/lamp-ubuntu.html
    - http://ubuntuforums.org/showthread.php?t=794248
    - http://wiki.ubuntuusers.de/Apache/Virtual_Hosts
    - http://barrierefrei.e-workers.de/know_virtual.php
    - ftp://ftp.ua.openwall.com/pub/docs/mirrors/pm4u.opennet.ru/apache.htm

- Скопировал файла /etc/apache2/sites-available/default в ту же папку
- Новый файл переименовал во что-то вроде этого “mysites” и отредактировал следующим образом:
<VirtualHost *:80>
  ServerName mysites 
  #ServerAlias можно указать альтернативные имена хоста
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/mysites/
  <Directory />
    Options FollowSymLinks
    AllowOverride None
  </Directory>
  <Directory /var/www/mysites>
    Options Indexes FollowSymLinks MultiViews
    AllowOverride None
    Order allow,deny
    allow from all
  </Directory>
  ErrorLog /var/log/apache2/error.log
  # Possible values include: debug, info, notice, warn, error, crit,
  # alert, emerg.
  LogLevel warn
  CustomLog /var/log/apache2/access.log combined
</VirtualHost>


* This source code was highlighted with Source Code Highlighter.


- Создал папку “/var/www/mysites/” и в ней index-файл (html, php...)
- Прописал новый хост в файле “/etc/hosts”
    127.0.0.1     mysites

- Для активации виртуального хоста выполнил команду: “sudo a2ensite”, после ввода пароля спросит какой файл подключить.

На заметку:
Для деактивации хоста можно использовать смежную команду: sudo a2dissite
Активированные хосты можно узнать по содержимому папки “/etc/apache2/sites-enabled”

- Для вступления изменений в силу: “sudo /etc/init.d/apache2 reload”, можно и перезапустить apache: “sudo /etc/init.d/apache2 restart”
- Локальную страницу можно вызвать задав в браузере имя сервера, в примере это “mysites”

5. Описанным в 4-ом пункте образом можно создать множество виртуальных хостов, но снаружи (моя главная система, в которой запущена виртуальная машина) будет видна только дефольтная страница. Ее можно использовать для показа каталога доступных страниц, например так:

<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
<p/>
<p><a href="/mysites/">mysites</a></p>
<p><a href="/xxx/">xxx</a></p>
</body></html>


* This source code was highlighted with Source Code Highlighter.


Все!