вторник, 17 сентября 2013 г.

Слияние двух выражений (Linq.Expression)

Постановка задачи

Работая с технологией Linq, я недавно уперся в следующую проблему. В рамках моей задачи, мне было необходимо выражение вида Expression<Func<T1,bool>>, которое я должен был передать в Linq-запрос. При этом, извне мне передали выражение вида Expression<Func<T2,bool>>. Кроме того, у меня есть преобразование вида Expression<Func<T1,T2>>. Если бы мы имели дело с простыми делегатами типа Func, то все решилось бы последовательным вызовом двух функций: f = x => f1( f2( x ) );. Однако, с выражениями, мы такой финт сделать не можем, а приведения выражений к делегатам методом Compile(), создадут нам анонимные классы и последующая свертка сведется, соответственно, к вызову методов этих классов. А это значит, что результирующее выражение уже можно будет использовать только, как делегат - полноценного дерева выражений (к примеру, для построения SQL запроса) уже не будет.

Решение

Что бы сохранить нормальную структуру наших выражений, будем использовать класс ExpressionVisitor, с помощью которого обойдем дерево первого выражения и вместо переменной типа T2 подставим выражение для T1=>T2. Для этого напишем класс-наследник ExpressionVisitor:
public class ReplacementVisitor : ExpressionVisitor
{
   readonly Expression fromExpr;
   readonly Expression toExpr;

   public ReplacementVisitor( Expression oldExpr, Expression newExpr )
   {
      fromExpr = oldExpr;
      toExpr = newExpr;
   }

   public override Expression Visit( Expression node )
   {
      if( node == fromExpr )
         return toExpr;
      return base.Visit( node );
   }
}
Используя этот класс, создадим статическую функцию, которая собственно и будет решать нашу задачу
public static class ExpressionHelper
{
   public static Expression<Func<TSrc, TRes>> Combine<TSrc, TMes, TRes>( this Expression<Func<TSrc, TMes>> firstExpr, Expression<Func<TMes, TRes>> secondExpr )
   {
      var param = Expression.Parameter( typeof( TSrc ), "x" );
      var visitor = new ReplacementVisitor( firstExpr.Parameters[0], param );
      var newParentBody = visitor.Visit( firstExpr.Body );
      visitor = new ReplacementVisitor( secondExpr.Parameters[0], newParentBody );
      var body = visitor.Visit( secondExpr.Body );
      return Expression.Lambda<Func<TSrc, TRes>>( body, param );
   }
}
Осталось только сказать, что класс ExpressionVisitor появился в .Net Framework 4.0. Если же вы используете более ранние версии .Net, то данный класс можно взять с сайта Microsoft. Например, отсюда http://msdn.microsoft.com/ru-ru/library/bb882521(v=vs.90).aspx.

воскресенье, 15 сентября 2013 г.

Использование Ninject в Windows Forms

На днях столкнулся с такой проблемой - как использовать Ninject совместно с дизайнером Windows Forms. Понятное дело, создать саму форму проблем нет. Для этого достаточно заменить строку new Form1() на kernel.Get<Form1>(). В частности, для главного окна программы, мы просто корректируем класс Program, заменяя строку Application.Run( new Form1() ); на:
var kernel = new StandardKernel( new MyInjectModule() );
Application.Run( kernel.Get<Form1>() );
Но что же делать с пользовательскими контролями и компонентами, размещенными в дизайнере? Ведь код для их создания генерируется автоматически. Тут нас выручит возможность произвести инъекцию в уже готовый экземпляр с помощью метода Inject, определенного в интерфейсе IKernel и специальный атрибут InjectAttribute, позволяющий делать инъекции в свойства и методы. Итак, предположим, мы создали наше окно через Ninject, указанным выше способом. В конструкторе нашего окна вызывается сгенерированный студией метод InitializeComponent, который отрабатывает код дизайнера, рекурсивно создавая контроли и компоненты, расположенные в окне. Далее, наша задача рекурсивно обойти все эти элементы, вызывая для каждого метод Inject. В общем-то, ничего сложного в этом нет. А, что бы было еще проще, я написал маленький класс-хелпер. Выглядит он следующим образом:
public static class NinjectHelper
{
 public static void InjectToComponent( this IKernel kernel, Component parent )
 {
  if( parent is Control )
   foreach( Control c in ((Control)parent).Controls )
   {
    kernel.Inject( c );
    kernel.InjectToComponent( c );
   }
  var cmpInfo = parent.GetType().GetField( "components", BindingFlags.NonPublic | BindingFlags.Instance );
  if( cmpInfo != null )
  {
   var cmp = cmpInfo.GetValue( parent ) as IContainer;
   if( cmp != null )
    foreach( Component c in cmp.Components )
    {
     kernel.Inject( c );
     kernel.InjectToComponent( c );
    }
  }
 }
}
Использовать его очень просто - для этого надо вызвать метод InjectToComponent сразу после метода InitializeComponent:
public Form1( IKernel kernel )
{
 InitializeComponent();
 kernel.InjectToComponent( this );
}
После этого мы можем получить инъекции в любом нашем подэлементе (контроле или компоненте) через атрибут InjectAttribute следующим образом:
[Inject]
public int A
{
 get { return a; }
 set
 {
  a = value;
  //do some things
 }
}
Или, аналогично, через метод:
[Inject]
public void SetA( int a )
{
 //do some things
}
P. S. Я уверен, что найдутся люди, которые скажут "Вай! Зачем такие сложности? Не проще положить kernel в статический класс и дергать его оттуда из любого места?". Не смотря, на очевидность ответа на этот вопрос, я все же его озвучу. Если вы пишете универсальные блоки кода, предполагая их использование из разных приложений, то вы не можете знать, где и как создается ядро kernel - ваш статический класс вполне может оказаться непроинициализированным. Если же вы пишете небольшое приложение, не предполагающее особой гибкости, то Ninject вам вообще не нужен - все можно получить с помощью статических хелперов.

понедельник, 15 июля 2013 г.

Как узнать, какие версии Framework установлены на компьютере

На днях передо мной встала задача определить программно, какие версии .Net Framework установлены на машине клиента. Покопавшись в реестре, я нашел нужную мне информацию в "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP". Однако, что бы правильно вытащить ее оттуда, пришлось немного повозиться и вылилось в следующий код:
public class NetVersions
{
   public static Collection<Version> InstalledDotNetVersions()
   {
      Collection<Version> versions = new Collection<Version>();
      RegistryKey NDPKey = Registry.LocalMachine.OpenSubKey( @"SOFTWARE\Microsoft\NET Framework Setup\NDP", true );
      if( NDPKey != null )
      {
         string[] subkeys = NDPKey.GetSubKeyNames();
         foreach( string subkey in subkeys )
         {
            GetDotNetVersion( NDPKey.OpenSubKey( subkey ), subkey, versions );
            GetDotNetVersion( NDPKey.OpenSubKey( subkey ).OpenSubKey( "Client" ), subkey, versions );
            GetDotNetVersion( NDPKey.OpenSubKey( subkey ).OpenSubKey( "Full" ), subkey, versions );
         }
      }
      return versions;
   }

   static void GetDotNetVersion( RegistryKey parentKey, string subVersionName, Collection<Version> versions )
   {
      if( parentKey != null )
      {
         string installed = Convert.ToString( parentKey.GetValue( "Install" ) );
         if( installed == "1" )
         {
            string version = Convert.ToString( parentKey.GetValue( "Version" ) );
            if( string.IsNullOrEmpty( version ) )
            {
               if( subVersionName.StartsWith( "v" ) )
                  version = subVersionName.Substring( 1 );
               else
                  version = subVersionName;
            }

            Version ver = new Version( version );

            if( !versions.Contains( ver ) )
               versions.Add( ver );
         }
      }
   }
}
Буду рад, если оно понадобится еще кому-нибудь.

понедельник, 8 июля 2013 г.

Как проверить, что мы в дизайнере (DesignMode) в Windows Form

Задача

Разрабатывая элементы управления в дизайнере Windows Form, нам часто необходимо проверить в каком режиме открыли наш элемент. Ведь в реальном режиме у нас может быть сложная логика чтения информации из БД, подключения в web-службам, взаимодействия с какими-то сторонними компонентами и т. д.. На момент редактирования визуального представления, вся эта логика должна быть отключена. А, для этого, нам необходим какой-то признак, по которому мы сможем отличить дизайнер от реального запуска программы:
if( !DesignMode )
{
    //...
}

Решения

Для начала посмотрим на то, что нам предлагают разработчики от Микрософт. В классе Component имеется свойство DesignMode. "Эврика! Это именно то, что нужно", подумал я. А, прочитав описание, я уж было совсем расслабился и начал писать свой код. Однако, первое же использование моего контроля в дизайнере, ввело меня в недоумение и растерянность - DesignMode был false. Всегда! Покурив форумы, я понял, что это свойство становится true после какого-то там обряда инициализации. Далее шли рассуждения о том, в какой момент времени это свойство показывает правильное значение, а в какой неправильное.. В общем-то рассуждения весьма бредовые, потому что код "иногда работающий правильно" - это как часы которые стоят - показывают правильное время ровно 2 раза в сутки, но толку от этого никакого. Кроме того, как я уже говорил, свойство находится в классе Component, а значит, до него невозможно добраться из вспомогательных классов, не имеющих ссылку на Component, что приводит к ограничениям в рефакторинге. А это значит, что свойство не просто бесполезно, но и вредно.
Продолжая курить форумы, я случайно наткнулся на класс LicenseManager. С его помощью проверку можно осуществить следующим образом:
if( LicenseManager.UsageMode != LicenseUsageMode.Designtime )
{
    //...
}
Однако, тут есть один маленький неприятный нюанс - этот класс писали те же "руки", что и предыдущий, а значит, работает все так же "глючно" и, в самый неожиданный момент, свойство UsageMode может выдать Runtime, не смотря на то, что мы находимся в дизайнере.
Еще одно интересное решение - это проверить название процесса:
if( Process.GetCurrentProcess().ProcessName != "devenv" )
{
    //...
}
Ограничения такого решения видны сразу - оно будет работать только из-под одной среды разработки - Microsoft Visual Studio. Однако, ничего не мешает расширить проверку, если ваша команда использует другие средства. По крайней мере, тут все прозрачно и предсказуемо.
Так как же сделать проверку правильно? Ок. Есть еще один вариант, который мне кажется самым правильным - завести где-то в общей библиотеке статическую переменную DesignMode, которая по умолчанию будет равна true. А, при запуске программы, сбрасывать ее на false. Это решение стабильно, прозрачно и надежно. Но! Это ужасное решение. Дело в том, что библиотека, содержащая такое решение, накладывает ограничения на свое использование -  подключая такую библиотеку к проекту, нам придется вспомнить о нужных строчках и вписать их, а при исключении библиотеки из проекта, строчку придется удалять. Конечно, ничего криминального в этом нет. Однако, стоит нам предположить, что подход со статистическими флажками приемлем, как мы начнем использовать его повсеместно и наши библиотеки приобретут совершенно непредсказуемое поведение - ведь очень скоро уже никто и не вспомнит какие флажки надо прописать, где, когда и сколько их вообще и какие для чего.

Выводы

Я так и не смог найти оптимального решения данной задачи. Сам я использую комбинации приведенных выше подходов, каждый раз вспоминая "добрым словом" авторов свойства DesignMode. Если кто-нибудь нашел решение лучше - буду рад получить обратную связь в комментариях.

воскресенье, 16 июня 2013 г.

Выборочный Binding в Ninject

Постановка задачи

Предположим, у нас есть некоторый интерфейс
interface IMyInterface
{
}
И несколько его реализаций, разбросанных по разным сборкам
//Где-то в в какой-то сборке
class ImplementA : IMyInterface
{
}

//Где-то в в какой-то другой сборке
class ImplementB : IMyInterface
{
}
Всем сборкам доступен общий Kernel Ninject-а, через который мы будем биндить наши реализации
//В первой сборке
kernel.Bind<IMyInterface>().To<ImplementA>();
//Во второй сборке
kernel.Bind<IMyInterface>().To<ImplementB>();
Запрашиваться реализации тоже могут в разных местах кода. В том числе и разными разработчиками. Для примера, будем запрашивать их так:
var res = kernel.GetAll<IMyInterface>();
foreach( var item in res )
{
   Console.WriteLine( item.GetType().Name );
}
А теперь, собственно, наша задача - как сделать так, что бы мы получили не все реализации, а только определенные, соответствующие определенным критериям?

пятница, 14 июня 2013 г.

Закладки из ToolStrip-а

Преамбула

Однажды, работая с классическим TabControl-ем, мне понадобилось разместить закладки слева от контента (сверху у меня был ToolBar и закладки там были не в тему). Продолбавшись пару часов Исследуя данный вопрос, я осознал, что TabControl работает без глюков только в стандартном режиме. Стоит хоть что-то поменять и баги лезут изо всех щелей. После чего я стал копаться по форумам и нашел интересное решение - ToolStrip. В самом деле, кнопочки ToolStripButton во многом похожи на закладки - достаточно слегка изменить их визуальную часть и прикрутить разработать логическую составляющую. Еще немного покопавшись в интернете, я нашел несколько готовых вариантов. Однако, все они были "сырыми" и требовали доработок. После чего я, взяв за основу то, что сделали до меня, стал писать свой вариант. В настоящий момент прошло уже более года с тех пор, как я успешно использую эту систему. А значит, ее можно считать более-менее отлаженной и пришло время сделать ее общественным достоянием.

Общее описание

воскресенье, 2 июня 2013 г.

Как сравнить строки с "*" и "?"

Зачастую встает задача сравнить две строки с использованием так называемых "wildcard" - спецсимволов "*" (произвольное количество любых символов) и "?" (один любой символ). Конечно, есть регулярные выражения. Но что если маску должен задавать пользователь, а в сложных выражениях просто нет необходимости. На этот случай я сделал себе маленький класс-помощник. Выкладываю тут - вдруг, еще кому пригодится.
public static class StringHelper
{
   static bool CompareWithPart( string source, string part )
   {
      if( source.Length != part.Length )
         return false;
      for( int i = 0; i < source.Length; i++ )
         if( source[i] != part[i] && part[i] != '?' )
            return false;
      return true;
   }

   static bool ConsumePart( ref string source, string part, bool first, bool last )
   {
      if( part == string.Empty )
      {
         if( last ) source = "";
         return !(first && last);
      }
      if( last )
      {
         var subStr = first ? source : source.Substring( Math.Max( 0, source.Length - part.Length ) );
         source = "";
         return CompareWithPart( subStr, part );
      }
      if( first )
      {
         var len = Math.Min( part.Length, source.Length );
         var subStr = source.Substring( 0, len );
         source = source.Substring( len );
         return CompareWithPart( subStr, part );
      }
      for( int i = 0; i <= source.Length - part.Length; i++ )
         if( CompareWithPart( source.Substring( i, part.Length ), part ) )
         {
            source = source.Substring( i + part.Length );
            return true;
         }
      return false;
   }

   public static bool CompareWildcard( this string source, string mask )
   {
      if( source == null )
         throw new ArgumentNullException( "source" );
      if( mask == null )
         throw new ArgumentNullException( "mask" );
      var parts = mask.Split( '*' );
      for( int i = 0; i < parts.Length; i++ )
         if( !ConsumePart( ref source, parts[i], i == 0, i == parts.Length - 1 ) )
            return false;
      return true;
   }

   public static bool CompareWildcard( this string source, string mask, bool ignoreCase )
   {
      if( source == null )
         throw new ArgumentNullException( "source" );
      if( mask == null )
         throw new ArgumentNullException( "mask" );
      return ignoreCase
         ? source.ToLower().CompareWildcard( mask.ToLower() )
         : source.CompareWildcard( mask );
   }

}
Использовать этот класс очень просто. Например так:
Console.WriteLine( "Мама моет раму".CompareWildcard( "*раму" ) );
Console.WriteLine( "Мама моет раму".CompareWildcard( "Мама*" ) );
Console.WriteLine( "Мама моет раму".CompareWildcard( "*моет*" ) );
Есть вопросы? Пишите в комментарии.

суббота, 11 мая 2013 г.

Ветвление проекта, конфигурации и AssemblyName

Постановка задачи

Рассмотрим классическую задачу. Предположим, вы ведете проект средних размеров на платформе .Net. Ведете достаточно давно и ваша команда проделала приличную работу. По сути, ваш проект уже на стадии тестирования - дописываются "фантики", исправляются "баги". И вдруг! приходит команда сверху - нужна демо-версия! Другими словами, вам теперь придется сопровождать два варианта проекта, отличающиеся между собой мелкими деталями. Естественно вас не устроит вариант поддерживать две совершенно независимые разработки. Рассмотрим два варианта решения данной задачи.

Классический вариант. Два Application

Предположим, что наше решение имеет более-менее классический вид. Т. е. состоит из N-го количества библиотек (Class Library) и одного собирающего проекта (Application). Наиболее естественным решением нашей задачи будет создание двух собирающих (Application) проектов, в которых и будет находиться вся логика ветвления. При этом все общие части будут располагаться в библиотеках. Однако, такой подход потребует существенного рефакторинга всего решения - придется четко разделить код на общий и зависимый от версии. При этом, в Application должен будет остаться только второй вариант. Не могу сказать, что данные сложности непреодолимы, но их наличие мотивирует рассматривать и другие подходы тоже.

Ветвление на уровне конфигураций (Configuration)

Предположим, что у нас обычная команда кодеров, а не супер-слаженная группа гениальных программистов. Т. е. код структурирован плохо, библиотеки ссылаются друг на друга, в главном окне программы откуда-то взялась библиотечная логика. А про внедрение зависимостей (DI) половина нашей группы вообще не слышала. К сожалению, такая ситуация встречается сплошь и рядом и отрефакторить такое решение весьма не просто.
Рассмотрим другой подход - конфигурации. Для начала откроем Configuration Manager и добавим конфигурацию
Затем, идем в свойства проекта, в секцию "Build" и добавляем константу компилятора для нашей конфигурации
Далее расставляем директивы процессора везде, где это требуется. Например, так:
#if DEMO
    Console.WriteLine( "Это демонстрационная версия" );
#endif
Или так
#if DEMO
[assembly: AssemblyTitle( "ConsoleTest.Demo" )]
[assembly: AssemblyDescription( "Это демонстрационная версия проекта" )]
#else
[assembly: AssemblyTitle( "ConsoleTest" )]
[assembly: AssemblyDescription( "Это основная версия проекта" )]
#endif
[assembly: AssemblyConfiguration( "" )]
Ну и последний штрих - это название конечного продукта. Естественно, название конечного exe-файла для демо-версии должно отличаться от штатного названия. Иначе, мы будем просто путаться. Для этого нам нужно, что бы AssemblyName в разных конфигурациях был разным. Однако, Visual Studio не предоставляет нам такой возможности. Тем не менее, компилятор считывает название из файла проекта (.csproj), который мы можем править вручную в любом текстовом редакторе. Каждая конфигурация будет соответствовать секции
<PropertyGroup ...>
    ...
</PropertyGroup>
Поэтому, самое простое решение будет найти секцию, соответствующую нашей демо-версии и вписать внутрь элемент AssemblyName
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Demo|AnyCPU'">
    ...
    <AssemblyName>ConsoleTest.Demo</AssemblyName>
    ...
</PropertyGroup>
После этого достаточно запустить VS, открыть наше решение и скомпилировать все пакетным построением, отметив галочками основную и демо конфигурации. Однако, читая форумы, я обнаружил, что некоторые версии VS данный подход воспринимают плохо. Если у вас тоже возникли сложности, то можно использовать более длинный вариант:
1. В секции PropertyGroup объявляем какую-нибудь переменную
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Demo|AnyCPU'">
    ...
    <isDemo>true</isDemo>
    ...
</PropertyGroup>
2. После всех PropertyGroup добавляем секцию Choose, в которой и задаем наш AssemblyName
<Choose>
    <When Condition=" '$(isDemo)' == 'true' ">
      <PropertyGroup>
        <AssemblyName>$(AssemblyName).Demo</AssemblyName>
      </PropertyGroup>
    </When>
</Choose>
В некоторых случаях, такой подход работает стабильнее.
Ну и, конечно же, надо помнить, что сама студия по-прежнему не поддерживает наши добавки. Поэтому, запустить демо-версию из-под нее не удастся. Зато, компилятор отработает правильно и пакетная компиляция даст нам сразу обе версии - основную и демо.

суббота, 27 апреля 2013 г.

WiX. Минимальный набор диалоговых окон

На днях у меня возникла задача написать простейший инсталятор к простенькой програмульке. Решил использовать WiX - благо, со студией интегрируется и, вроде как, все должно быть легко. Почесав затылок, подумал, что не плохо было бы прикрутить стандартные окошки - "приветствие" и "выбор папки". Больше, в принципе, ничего не надо. Добавил в References WixUIExtension.dll, начал разбираться со стандартными наборами диалогов и с ходу напоролся на неприятный момент - в классический набор запихнули диалог с лицензий. Так уж сложилось, что авторы WiX-а убеждены, что лицензия вещь офигенно важная и без нее ну никак. Для моей же задачки лицензия была, как бельмо на причинном месте. Пришлось "лечить". Как ни странно, простого решения не нашлось - никаких настроек и/или флажков. Пришлось лезть в исходники и править ручками файлик WixUI_InstallDir.wxs. Дабы не было конфликтов, предварительно переименовал его в WixUI_Simple.wxs. Затем вычистил все, что касается лицензии и аккуратненько перекинул ссылочки. Получилось вполне прилично. Для использования достаточно добавить в проект WixUI_Simple.wxs и добавить пару строчек в Product.wxs:
...
<property id="WIXUI_INSTALLDIR" value="INSTALLFOLDER"></property>
<uiref id="WixUI_Simple"/>
INSTALLFOLDER - это идентификатор конечной директории. Т. е. он прописывается в теге Directory
...
<Directory Id="INSTALLFOLDER" Name="ProductName">

Скачать WixUI_Simple.wxs

пятница, 26 апреля 2013 г.

Первое знакомство с VS2012

Время летит и прогресс не стоит на месте. Технологии развиваются с бешеной скоростью. Пытаюсь угнаться. Пару месяцев назад дошли руки до Visual Studio 2012. Установил. Запускаю. С замиранием сердца жду чуда. Супер-новый интерфейс, новые возможности, новый дизайн.. Но, что это? Все такое серенькое и невзрачное. Но почему? Ок. Наверное, яркий дизайн просто вышел из моды. Будем знать - надо будет учесть в своих проектах это "ноу-хау".

CAPS в меню

Тут мой взгляд остановился на главном меню. Тут определенно было что-то не так. Меню резало глаз. После десяти секунд одупления до меня дошло - большие буквы. Ужас! Ок. Я спокоен и гугл мне поможет. Пара минут поиска выдала пару статей и проблема решилась простенькой настройкой реестра.

Для возврата старого меню внесите следующие изменения в реестр Windows:
HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\11.0\General\SuppressUppercaseConversion REG_DWORD value: 1
(если лень копаться в реестре вручную, то можно скачать файлик  SuppressUppercaseConversion.reg и просто запустить его)
Хорошо. Менюшку привели в "божеский" вид. Начинаем работать.

Макросы

Но что это? Не работают горячие клавиши? Да нет - некоторые работают. Не перенеслись настройки из предыдущей студии? Тоже как-то странно. Еще пара минут плясок с бубном и до меня доходит - ПЕРЕСТАЛИ РАБОТАТЬ МАКРОСЫ!!! Т. е. совсем перестали - даже редактор макросов из меню исчез. Активно гуглю и выясняю, что макросы реально убрали. Убрали совсем, безвозвратно и надежды на сервиспаки нет никакой. Печально.

Скорость

А вот скорость порадовала - тупить стало меньше. Я, по-началу, по своей наивности даже подумал, что "мелкомягкие" научились оптимизировать код. Однако, после пары дней более пристального изучения, понял, что нет - оптимизации как таковой нет. Просто тяжелые задачи распараллелили по потокам. Т. е. теперь, пока грузится дизайнер, основной поток не висит и можно что-то делать. Ну хоть так - все же, лучше, чем было.

Инсталяшки

Итак, я написал свой первый проект. К этому времени я уже успел привыкнуть к новому дизайну и он мне даже стал нравиться. Пора собирать дистрибутив. Проект простенький, без заморочек, поэтому на дистрибутив я выделил 1.5 часа - как мне тогда казалось "с приличным запасом". Но тут меня ждал новый сюрприз - старого доброго конструктора дистрибутивов больше нет. Снова лезу в гугл, листаю статьи, курю форумы. Выясняю, что теперь для сбора инсталяшки народ использует волшебный бубен сторонний продукт под названием "WiX". Полноценную документацию на родном языке найти не удалось.. как, впрочем и удобного UI. Пришлось снова курить форумы и лазить по буржуйским сайтам. Через 2 дня дистрибутив таки был собран. Не могу сказать, что WiX мне не понравился. Но все его возможности можно вкуривать ни один месяц.

Итог

А зори здесь тихие...