Показаны сообщения с ярлыком Visual Studio. Показать все сообщения
Показаны сообщения с ярлыком Visual Studio. Показать все сообщения

воскресенье, 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

понедельник, 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:\”.

суббота, 28 января 2012 г.

Visual Studio 2010 Color Schema. Моя темная цветовая схема

Моя цветовая схема, которой я пользуюсь уже достаточно продолжительное время.

Редакотор c# кода:


XAML:


XML:


Основной каркас этой схемы когда-то накопал на сайте studiostyl.es, как она называлась изначально уже и не вспомню. Постепенно, по мере надобности, оптимизировал ее до существующей. Цвета, на мой взгляд, достаточно мягкие, что бы смотреть на них долгое время, глаза не устают.

Скачать схему

вторник, 4 мая 2010 г.

Изменение имени проекта (сборки) в Visual Studio 2008

Вообще-то есть уже куча руководств, как можно проделать сие.

Например здесь.
http://social.msdn.microsoft.com/Forums/en-US/csharpide/thread/d4133fb7-57e1-44f2-a319-a63e7921ff0a

Но я решил запостить последовательность действий, которую выполнил я. При этом я не "поломал" проект и не потерял историю изменений для TFS. Итак VS 2008, TFS.

1. Выбрал проект в Експлорере проектов -> переименовал;
3. Свойства проекта -> Установил имя сборки и имя пространств имен. Тут же заглянул в информацию сборки и изменил название сборки (AssemblyInfo.cs);
4. В окне Quellcodeverwaltuns-Explorer переименовал папку. При этом проект закрылся и имя папки было на диске физически переименованно;
5. После того как открыл проект, студия ругнулась, что не нашла того самого проекта (или не смогла открыть). В эксплорере проектов он выделен другим цветом и пустой.
6. В свойствах проекта ранее доступное только для чтение свойство Projektordner становится доступным для изменения. Указал новый путь.

Все.