Архив рубрики: .net

Многопоточность и GUI

Маленький пример использования многопоточности в приложениях, содержащих графический пользовательский интерфейс (GUI). Продемонстрировано два способа обращения к элементам пользовательского интерфейса из рабочего потока в UI-поток. Графический интерфейс при этом не "подвисает".

.Net Framework 4.5

 /* Sandbox.cs
 * © Andrey Bushman, 2017
 *
 * Небольшой пример создания дополнительного потока,
 * работающего параллельно с потоком пользовательского
 * интерфейса (UI) и обновляющего этот интерфейс по мере
 * необходимости. Дополнительных потоков можно создавать
 * сколько угодно. В данном примере для простоты создаётся
 * только один.
 *
 * В данном примере вместо прямого использоватия потока
 * (Thread) я использую задачу (Task), которая в свою очередь
 * использует пул потоков.
 *
 * Рабочий поток (т.е. задача) вычисляет текущую дату и время,
 * после чего записывает их в ListBox, находящийся в потоке UI.
 *
 * Снятие/установка галочки "Do work" управляет
 * стартом/завершением рабочего потока. Кнопка "Clear" очищает
 * ListBox и заголовок окна.
 *
 * В консоль выводятся идентификаторы текущих потоков и маркер
 * их принадлежности (или не принадлежности) к пулу потоков.
 *
 * В данном примере для обращения к потоку UI из рабочего
 * потока я использую два способа: диспетчер и контекст
 * синхронизации.
 *
 * В примере используется WPF, но всё то же самое применимо к
 * WinForms и ASP.NET.
 */
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Bushman.Sandbox {

    class MyWindow : Window {

        SynchronizationContext context = null;

        public MyWindow() : base() {

            Console.Title = "Sandbox";
            string prefix = "Main Window";
            Title = prefix;
            Topmost = true;

            Width = 300;
            Height = 600;

            ResizeMode = ResizeMode.NoResize;

            WindowStartupLocation = WindowStartupLocation
                .CenterScreen;

            Grid grid = new Grid();
            grid.RowDefinitions.Add(new RowDefinition());
            grid.RowDefinitions.Add(new RowDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());
            grid.ColumnDefinitions.Add(new ColumnDefinition());

            grid.ColumnDefinitions[1].Width = new GridLength(0,
                GridUnitType.Auto);
            grid.RowDefinitions[1].Height = new GridLength(0,
                GridUnitType.Auto);

            Content = grid;

            Thickness margin = new Thickness(5, 5, 5, 5);

            ListBox listbox = new ListBox();
            listbox.Margin = margin;
            grid.Children.Add(listbox);
            listbox.SetValue(Grid.RowProperty, 0);
            listbox.SetValue(Grid.ColumnProperty, 0);
            listbox.SetValue(Grid.ColumnSpanProperty, 2);

            CheckBox chBox = new CheckBox();
            chBox.Margin = margin;
            chBox.Content = "Do work";
            grid.Children.Add(chBox);
            chBox.SetValue(Grid.ColumnProperty, 0);
            chBox.SetValue(Grid.RowProperty, 1);

            Task task = null;
            long i = 0;

            chBox.Checked += (s, e) => {

                task = new Task(() => {
                    using (task) {
                        i = 0;
                        while (Dispatcher.Invoke(
                            () => chBox.IsChecked == true) &&
                            i < long.MaxValue) {

                            // В рабочем потоке выполняем
                            // некоторую работу. Например -
                            // формируем строку текущих даты и
                            // времени.
                            string value = DateTime.Now
                                .ToString("yyyy-MM-dd hh:mm:ss"
                                );

                            // В данном примере мы мы можем
                            // имитировать длительную работу,
                            // либо заблокировать эту строку
                            // кода, если такая имитация нам не
                            // нужна:
                            //Thread.Sleep(TimeSpan.FromSeconds
                            //    (1));

                            Console.WriteLine(
                                "Current thread Id: {0}. " +
                                "Is pull thread: {1}",
                                Thread.CurrentThread
                                .ManagedThreadId.ToString(),
                                Thread.CurrentThread
                                .IsThreadPoolThread);

                            // Результат наших "вычислений"
                            //записываем в поток UI
                            context.Post(_ => {
                                listbox.Items.Add(value);
                                Title = string.Format(
                                    "{0}. Items Count: {1}",
                                prefix, i++.ToString());

                                Console.WriteLine(
                                "Current thread Id: {0}. " +
                                "Is pull thread: {1}",
                                Thread.CurrentThread
                                .ManagedThreadId.ToString(),
                                Thread.CurrentThread
                                .IsThreadPoolThread);
                            }, null);
                        }
                    }
                });

                task.Start();
            };

            chBox.IsChecked = false;

            Button button = new Button();
            button.Content = "Clear";
            button.Margin = margin;
            button.Padding = margin;
            grid.Children.Add(button);
            button.SetValue(Grid.ColumnProperty, 1);
            button.SetValue(Grid.RowProperty, 1);
            button.Click += (s, e) => {
                listbox.Items.Clear();
                Title = prefix;
                i = 0;
            };

            EventHandler action = null;

            action = (s, e) => {
                context = SynchronizationContext.Current;
                Activated -= action;
            };

            Activated += action;

            NameScope.SetNameScope(this, new NameScope());
            RegisterName(nameof(listbox), listbox);
            RegisterName(nameof(chBox), chBox);
            RegisterName(nameof(button), button);
        }
    }

    class Sandbox {
        [STAThread]
        static void Main(string[] args) {

            MyWindow win = new MyWindow();
            Application app = new Application();
            app.Run(win);
        }
    }
}


Результат работы кода выглядит следующим образом:

 

Динамический вызов функций неуправляемых DLL

В .NET-атрибутах можно указывать только константные выражения. Т.о. атрибуту DllImport нужно указывать имя библиотеки статически, дабы это имя было известно на этапе компиляции. Порой это порождает уродливые конструкции, разрастающиеся как снежный ком, по мере появления новых версий AutoCAD. Наглядный тому пример можно увидеть здесь в коде Александра Ривилиса: по мере появления AutoCAD 2018, 2019 и т.д. - этот код придётся каждый раз дописывать.


Как правило, имя нужной DLL достаточно просто выяснить во время выполнения кода. Например, в обозначенном выше коде Александра Ривилиса нужная функция хранится в файлах acdb17.dll, acdb18.dll, ac1st19.dll, ac1st20.dll, ac1st21.dll. Т.е. мы видим, что каждая библиотека в качестве суффикса использует значение Major версии приложения. Кроме того, мы видим, что начиная с версии 19 (AutoCAD 2013) функция была перенесена в др. библиотеку (т.е. изменился префикс в её имени). Зная обозначенные выше правила, можно динамически вычислить имя DLL файла, в котором находится интересующая нас функция.

Далее показываю небольшой пример того, как можно динамически вызывать функцию acedSetEnv в разных версиях AutoCAD, не создавая для неё статических обёрток. На всякий случай напоминаю, что до версии AutoCAD 2013 эта функция находилась в файле acad.exe, а начиная с AutoCAD 2013 была перенесена в accore.dll.

Код проверялся в AutoCAD 2009 и 2016.

-------------------------------------------------------------
/* Utils.cs */
using System;
using System.Text;
using System.Runtime.InteropServices;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;

namespace Bushman.AutoCAD.Sandbox {

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet
        = CharSet.Auto)]
    public delegate int acedSetEnvDelegate(string envName,
        StringBuilder NewValue);

    public static class Utils {

        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string
            dllToLoad);

        [DllImport("kernel32.dll")]
        public static extern IntPtr GetProcAddress(IntPtr
            hModule, string procedureName);


        [DllImport("kernel32.dll")]
        public static extern bool FreeLibrary(IntPtr hModule);

        const int AutoCAD_2013_Major = 19;

        public static string GetDllName() {

            if (cad.Version.Major < AutoCAD_2013_Major)
                return "acad.exe";
            else
                return "accore.dll";
        }

        public static acedSetEnvDelegate acedSetEnv;

        static Utils() {
            acedSetEnv = GetAcedSetEnv();
        }

        static acedSetEnvDelegate GetAcedSetEnv() {

            string dllName = Utils.GetDllName();
            IntPtr pDll = Utils.LoadLibrary(dllName);

            if (pDll == IntPtr.Zero) {
                return null;
            }

            string funcName = "acedSetEnv";

            IntPtr pAddressOfFunctionToCall = Utils
                .GetProcAddress(pDll, funcName);

            if (pAddressOfFunctionToCall == IntPtr.Zero) {

                return null;
            }

            acedSetEnvDelegate acedSetEnv = (
                acedSetEnvDelegate) Marshal
                .GetDelegateForFunctionPointer(
                pAddressOfFunctionToCall,
                typeof(acedSetEnvDelegate));

            bool result = Utils.FreeLibrary(pDll);

            return acedSetEnv;
        }
    }
}

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

Далее создадим тестовую команду, которая изменит значение переменной "ACAD":

------------------------------------------------------------
/* Commands.cs */
using System;
using System.Runtime.InteropServices;
using System.Text;
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;

namespace Bushman.AutoCAD.Sandbox {

    public sealed class Commands {

        [CommandMethod("Test", CommandFlags.Modal)]
        public void Test() {

            Document doc = cad.DocumentManager
                .MdiActiveDocument;

            if (doc == null)
                return;

            Editor ed = doc.Editor;

            acedSetEnvDelegate acedSetEnv = Utils.acedSetEnv;

            if (acedSetEnv == null) {
                ed.WriteMessage("acedSetEnv function was " +
                    "not found.");
                return;
            }

            /* For example, we'll edit the "Support File Search
             * Path" value: */
            string varName = "ACAD";
            StringBuilder sb = new StringBuilder(
                @"C:\abc\def");

            int theResult = acedSetEnv(varName, sb); // 5100
        }
    }
}

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

Динамическое связывание и изменение системой переменной ACAD успешно происходит в обоих тестируемых версиях AutoCAD: 2009 и 2016.



Работа с реестром в операционных системах Windows (часть 2)

Когда-то здесь я выкладывал инструменты по работе с реестром для .NET 3.5 SP1. Обнаружилось, что начиная с .NET 4.0 сигнатура нужных для обозначенного кода конструкторов класса RegistryKey была изменена. Как следствие - если наша сборка, скомпилированная под .NET 3.5 SP1 в дальнейшем окажется загруженной в .NET 4.0 (или любую более новую), то мы будем получать исключение времени выполнения при вызове некоторых методов опубликованного кода.


Я внёс некоторые правки в исходный код класса RegistryExtensions и написал несколько интеграционных тестов, проверяющих корректность работы кода в различных ситуациях (в т.ч. избавился от всех директив препроцессора - теперь они не нужны). Проверял в .NET 3.5 SP1, 4.0 и 4.6.1 для платформ x86|x64|AnyCPU:


Обозначенный код я поддерживаю в виду того, что у меня имеется в наработке некоторый объем кода, компилируемого под .NET 3.5 SP1 (т.к. AutoCAD 2009 не поддерживает более новые версии .Net Framework). Этот код должен без проблем компилироваться и успешно работать  не только в .NET 3.5 SP1, но и во всех более новых версиях .NET. Кроме того, необходимо, чтобы ранее скомпилированные мною под .NET 3.5 SP1 сборки успешно загружались и работали в .NET 4.0 - 4.6.1.

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

======================================================================
/* AcProducts
 * RegistryExtensions.cs
 * © Андрей Бушман, 2014
 *
 * В файле RegistryExtensions.cs определён дополнительный
 * функционал, расширяющий возможности работы с реестром из
 * .NET приложений, написанных на .NET 3.5 SP1.
 *
 * ИЗМЕНЕНИЯ:
 * 10-авг-2016
 *      В код внесены правки для того, чтобы он мог
 *      использоваться не только в .NET 3.5 SP1, но и во всех
 *      более новых версиях .NET Framework: например, когда
 *      сборка, скомпилированная под .NET 3.5 SP1 загружается в
 *      код .Net 4.6.1. Код не зависит от разрядности (x86|x64|
 *      AnyCPU).
 */
using System;
using Microsoft.Win32;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Bushman.AcProducts {

    /// <summary>
    /// Данный класс предназначен для предоставления 32-битным
    /// приложениям доступа к 64-битным разделам реестра. Класс
    /// так же может использоваться для предоставления
    /// 64-битным приложениям ветке реестра, предназначенной
    /// для 32-битных. За основу взят код, опубликованный в
    /// блоге http://clck.ru/96A9U
    /// </summary>

    public static class RegistryExtensions {

        /// <summary>
        /// Открытие ключа реестра, с указанием того, какую
        /// именно часть следует открывать: записи для
        /// 32-битных приложений, или же записи для 64-битных.
        ///
        /// ВНИМАНИЕ
        /// У данного метода имеется побочный эффект: свойство
        /// `Name` у возвращаемого объекта `RegistryKey` даёт
        /// пустую строку. Однако это не страшно, т.к. имя нам
        /// известно - первую часть можно получить из родителя,
        /// а вторую часть этого имени мы и так знаем,
        /// поскольку передаём её в виде параметра.
        /// </summary>
        /// <param name="parentKey">Родительский элемент
        /// RegistryKey, в котором следует выполнить открытие
        /// подраздера.</param>
        /// <param name="subKeyName">Name of the key to be
        /// opened</param>
        /// <param name="writable">true - открывать для чтения
        /// и записи; false - открывать только для чтения.
        /// </param>
        /// <param name="options">Какую именно часть реестра
        /// следует открывать: относящуюся к 32-битным
        /// приложениям или же относящуюся к 64-битным.
        /// </param>
        /// <returns>Возвращается RegistryKey или null, если по
        /// каким-либо причинам получить RegistryKey не
        /// удалось.</returns>
        public static RegistryKey OpenSubKey(this RegistryKey
            parentKey, String subKeyName, Boolean writable,
            RegWow64Options options) {

            // Проверка работоспособности
            if (parentKey == null || GetPtr(
                parentKey) == IntPtr.Zero) {

                return null;
            }

            // Назначение прав
            Int32 rights = (Int32) (writable ? RegistryRights
                .WriteKey : RegistryRights.ReadKey);

            // Вызов функций неуправляемого кода
            Int32 subKeyHandle, result = RegOpenKeyEx(
                GetPtr(parentKey), subKeyName, 0,
                rights | (Int32) options, out subKeyHandle);

            // Если мы ошиблись - возвращаем null
            if (result != 0) {

                return null;
            }

            /* Получаем ключ, представленный указателем,
             * возвращённым из RegOpenKeyEx */
            RegistryKey subKey = PtrToRegistryKey((IntPtr)
                subKeyHandle, writable, false, options);

            return subKey;
        }

        /// <summary>
        /// Получить указатель на ключ реестра.
        /// </summary>
        /// <param name="registryKey">Ключ реестра, указатель
        /// на который нужно получить.
        /// </param>
        /// <returns>Возвращается объект IntPtr. Если не
        /// удалось получить указатель на обозначенный объект
        /// RegistryKey, то возвращается IntPtr.Zero.</returns>
        public static IntPtr GetPtr(this RegistryKey
            registryKey) {

            if (registryKey == null)
                return IntPtr.Zero;

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 I get it
             * through reflection. */
            Type registryKeyType = typeof(RegistryKey);

            FieldInfo fieldInfo = registryKeyType.GetField(
                "hkey", BindingFlags.NonPublic | BindingFlags
                .Instance);

            SafeHandle handle = (SafeHandle) fieldInfo.GetValue
                (registryKey);

            IntPtr unsafeHandle = handle.DangerousGetHandle(
                );

            return unsafeHandle;
        }

        /// <summary>
        /// Получить ключ реестра на основе его указателя.
        /// </summary>
        /// <param name="hKey">Указатель на ключ реестра
        /// </param>
        /// <param name="writable">true - открыть для записи;
        /// false - для чтения.</param>
        /// <param name="ownsHandle">Владеем ли мы
        /// дескриптором: true - да, false - нет.</param>
        /// <returns>Возвращается объект RegistryKey,
        /// соответствующий полученному указателю.</returns>
        public static RegistryKey PtrToRegistryKey(IntPtr
            hKey, Boolean writable, Boolean ownsHandle,
            RegWow64Options opt) {

            if (IntPtr.Zero == hKey) {
                return null;
            }

            Type safeRegistryHandleType =
                typeof(SafeHandleZeroOrMinusOneIsInvalid)
                .Assembly.GetType("Microsoft.Win32." +
                "SafeHandles.SafeRegistryHandle");

            /* Получаем массив типов, соответствующих
             * аргументом конструктора, который нам нужен. */
            Type[] argTypes = new Type[] { typeof(IntPtr),
                typeof(Boolean) };

            BindingFlags flags = default(BindingFlags);

            if (Environment.Version.Major < 4) {
                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }
            else {
                flags = BindingFlags.Instance | BindingFlags
                    .Public;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo safeRegistryHandleCtorInfo =
                safeRegistryHandleType.GetConstructor(flags,
                    null, argTypes, null);

            /* Вызываем конструктор для SafeRegistryHandle.
             * Класс SafeRegistryHandle появился начиная с .NET
             * 4.0. */
            Object safeHandle = safeRegistryHandleCtorInfo
                .Invoke(new Object[] { hKey, ownsHandle });

            Type registryKeyType = typeof(RegistryKey);
            Type registryViewType = null;

            /*Получаем массив типов, соответствующих аргументом
             * конструктора, который нам нужен */
            Type[] registryKeyConstructorTypes = null;

            if (Environment.Version.Major < 4) {
                registryKeyConstructorTypes = new Type[] {
                safeRegistryHandleType, typeof(bool) };
            }
            else {
                registryViewType = typeof(
                    SafeHandleZeroOrMinusOneIsInvalid).Assembly
                    .GetType("Microsoft.Win32.RegistryView");

                registryKeyConstructorTypes = new Type[] {
                    safeRegistryHandleType, typeof(bool),
                    registryViewType };

                flags = BindingFlags.Instance | BindingFlags
                    .NonPublic;
            }

            // Получаем ConstructorInfo для нашего объекта
            ConstructorInfo registryKeyCtorInfo =
                registryKeyType.GetConstructor(flags, null,
                registryKeyConstructorTypes, null);

            RegistryKey resultKey = null;

            if (Environment.Version.Major < 4) {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable });
            }
            else {
                // Вызываем конструктор для RegistryKey
                resultKey = (RegistryKey) registryKeyCtorInfo
                    .Invoke(new Object[] {
                    safeHandle, writable, (int) opt});
            }

            // возвращаем полученный ключ реестра
            return resultKey;
        }

        /// <summary>
        /// Получение числового значения указателя на искомый
        /// подраздел реестра.
        /// </summary>
        /// <param name="hKey">Указатель на родительский раздел
        /// реестра.</param>
        /// <param name="subKey">Имя искомого подраздела.
        /// </param>
        /// <param name="ulOptions">Этот параметр
        /// зарезервирован и всегда должен быть равным 0.
        /// </param>
        /// <param name="samDesired">Права доступа (чтение\
        /// запись) и указание того, как именно следует
        /// открывать реестр. Значение этого параметра
        /// формируется путём применения операции логического
        /// "И" для объектов перечислений RegistryRights и
        /// RegWow64Options.</param>
        /// <param name="phkResult">Ссылка на переменную, в
        /// которую следует сохранить полученное числовое
        /// значение указателя на искомый подраздел.</param>
        /// <returns>В случае успеха возвращается 0.</returns>
        [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
        static extern Int32 RegOpenKeyEx(IntPtr hKey,
            String subKey, Int32 ulOptions, Int32 samDesired,
            out Int32 phkResult);
    }

    /// <summary>
    /// Перечисление указывает, какую именно часть реестра
    /// следует открывать: относящуюся к 32-битным приложениям
    /// или же относящуюся к 64-битным.
    /// </summary>
    public enum RegWow64Options {

        /// <summary>
        /// Открывать ту часть реестра, которая хранит
        /// информацию приложений, разрядность которых
        /// соответствует разрядности текущего приложения
        /// (x86\x64).</summary>
        None = 0,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 64-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_64KEY = 0x0100,
        /// <summary>
        /// Открывать часть реестра, относящуюся к 32-битным
        /// приложениям.
        /// </summary>
        KEY_WOW64_32KEY = 0x0200
    }

    /// <summary>
    /// Перечисление, указывающее на то, с каким уровнем
    /// доступа следует открывать ветку реестра: для чтения,
    /// или же для чтения\записи.
    /// </summary>
    public enum RegistryRights {

        /// <summary>
        /// Открыть только для чтения.
        /// </summary>
        ReadKey = 131097,
        /// <summary>
        /// Открыть для чтения и записи.
        /// </summary>
        WriteKey = 131078
    }
}
==================================================================

Интеграционные тесты:

==================================================================
/* AcProducts.IntegrationTests
 * RegistryExtensionsTests.cs
 * © Andrey Bushman, 2016
 *
 * Integration tests of Bushman.AcProducts.RegistryExtensions
 * class.
 */
using System;
using Microsoft.Win32;
using NUnit.Framework;

namespace Bushman.AcProducts.IntegrationTests {

    [TestFixture]
    public class RegistryExtensionsTests {

        [Test]
        public void GetPtr_Returns_ValidPtr() {

            // `Registry.LocalMachine` key always exists.
            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr rkPtr2 = RegistryExtensions.GetPtr(rk);

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void GetPtr_ReturnsZero_ForNull() {

            /* The `RegistryKey.Handle` property appears since
             * .Net 4.0, therefore for .Net 3.5 in the code
             * under test I get it through reflection. */
            IntPtr ptr = RegistryExtensions.GetPtr(null);

            Assert.AreEqual(IntPtr.Zero, ptr);
        }

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_64KEY, false)]

        [TestCase(@"SOFTWARE\7-Zip", RegWow64Options
            .KEY_WOW64_32KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_64KEY, true)]

        [TestCase(@"SOFTWARE\Notepad++",
            RegWow64Options.KEY_WOW64_32KEY, false)]

        [Description("7-Zip and Notepad++ are to be installed"
            )]
        public void OpenSubKey_ReturnsValidValue(string subkey,
            RegWow64Options opt, bool isNull) {

            RegistryKey rk = Registry.LocalMachine;

            RegistryKey rk2 = RegistryExtensions.OpenSubKey(rk,
                subkey, false, opt);

            if (isNull) {
                Assert.IsNull(rk2);
            }
            else {
                Assert.IsNotNull(rk2);
            }
        }

        [Test]
        public void OpenSubKey_ReturnsNull_ForInvalidSubkey() {

            string subkey =
                "{F28A3464-0A5D-48FB-AFF6-B07F058D3EFC}";

            RegistryKey rk = RegistryExtensions.OpenSubKey(
                Registry.LocalMachine, subkey, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }

        [Test]
        public void PtrToRegistryKey_Returns_ValidValie() {

            RegistryKey rk = Registry.LocalMachine;
            IntPtr rkPtr = rk.Handle.DangerousGetHandle();

            RegistryKey rk2 = RegistryExtensions
                .PtrToRegistryKey(rkPtr, false, false,
                RegWow64Options.None);
            IntPtr rkPtr2 = rk2.Handle.DangerousGetHandle();

            Assert.AreEqual(rkPtr, rkPtr2);
        }

        [Test]
        public void PtrToRegistryKey_ReturnsNull_ForZero() {

            RegistryKey rk = RegistryExtensions
                .PtrToRegistryKey(IntPtr.Zero, false, false,
                RegWow64Options.None);

            Assert.IsNull(rk);
        }
    }
}
==================================================================

Импорт конфигурационных настроек сервисами и клиентами WCF

В некоторых случаях службы и клиенты удобно реализовывать в виде отдельных DLL, которые затем могут быть использованы различными приложениями. Например: MyService.dll и MyClient.dll. Предполагается, что они будут находиться в подкаталогах ./Extensions/MyExtensionName/ хостовых приложений, дабы при необходимости оный контент всегда можно было бы просто удалить даже вручную, не зацепив при этом случайно ресурсы основного приложения.

Однако, будучи подгруженными в хостовое приложение они, как и любые другие DLL, по умолчанию будут искать свои настройки в составе конфигурационного файла этого приложения. Всё же мне бы не хотелось в чужой config-файл вносить правки, необходимые для работы моих сервисов или клиентов, ну или хотелось бы, по крайней мере, минимизировать объём таких корректировок настолько, насколько это возможно... На мой взгляд, предпочтительным способом была бы возможность сохранения нужных мне настроек в отдельных конфигурационных файлах, находящихся рядом с соответствующей DLL моего сервиса или клиента, т.е. в файлах MyService.dll.config и MyClient.dll.config.

Экспорт конфигурационных настроек на стороне сервиса
Начиная с .NET 4.5 в классе реализации сервиса можно определить метод Configure со следующей сигнатурой:

public static void Configure(ServiceConfiguration config)

Этот метод будет вызван WCF перед началом использования нашей службы. В нём можно выполнить загрузку конфигурационных настроек из внешнего файла, например так:

public static void Configure(ServiceConfiguration config) {
    ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
    configMap.ExeConfigFilename = typeof(MyService).Assembly.Location + ".config";
    Configuration dll_config = ConfigurationManager.OpenMappedExeConfiguration(configMap,
        ConfigurationUserLevel.None);
    config.LoadFromConfiguration(dll_config);
}

Однако в бочке мёда имеется и ложка дёгтя: в процессе импорта настроек WCF будет игнорировать все базовые адреса, обозначенные в импортируемом файле. Соответственно, либо эти базовые адреса нужно будет предварительно указать в конфигурационном файле самого хостового приложения (чего делать не очень хотелось бы, хотя это и не смертельно), либо в составе импортируемого файла настроек для конечных точек подключения и для метаданных использовать только полные адреса. Какой из вариантов выбрать - это уже нужно будет смотреть по ситуации.

Например, если расширение хранится в каталоге %AppData%/CompanyName/ApplicationName/Extensions/MyExtensionName/ с тем, чтобы предоставлять пользователю возможность править конфигурационные настройки или вовсе удалять расширение, то нужным вариантом будет второй.

А если пользователь не имеет административных прав и расширение хранится в каталоге %ProgramFiles%/CompanyName/ApplicationName/Extensions/MyExtensionName/, то в данном случае вариант использования - это дело вкуса администратора.

Экспорт конфигурационных настроек на стороне клиента
На стороне клиента использовать метод Configure в коде наших прокси не получится, но начиная с WCF 4.0 нам в помощь появился класс ConfigurationChannelFactory. Его можно создать в конструкторе создаваемого нами прокси  и использовать в процессе работы например так:

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class Drawing : IDisposable, IDrawing {

    IDrawing Channel;
    ConfigurationChannelFactory<IDrawing> channelFactory;

    public void Open() {
        if (channelFactory.State != CommunicationState.Opened &&
            channelFactory.State != CommunicationState.Opening)
            channelFactory.Open();
    }

    public void Close() {
        if (channelFactory.State == CommunicationState.Opened)
            channelFactory.Close();
    }

    public CommunicationState State
    {
        get { return channelFactory.State; }
    }

    public Drawing(string endpointName) {
        ExeConfigurationFileMap configMap = new ExeConfigurationFileMap();
        configMap.ExeConfigFilename = typeof(Drawing).Assembly.Location + ".config";
        Configuration dll_config = ConfigurationManager.OpenMappedExeConfiguration(configMap,
            ConfigurationUserLevel.None);
        channelFactory =
            new ConfigurationChannelFactory<IDrawing>(endpointName, dll_config, null);
        Channel = channelFactory.CreateChannel();
    }

    public void Dispose() {
        if (channelFactory != null&& channelFactory.State == CommunicationState.Opened)
            channelFactory.Close();
    }

    // here is other members...   
}
Обратите внимание на то, что в процессе реализации нужного нам прокси мы обошлись без наследования от класса ClientBase.

Ссылки:

И снова о хостинге WCF-сервисов в 2016-м accoreconsole.exe…

Подумал тут... Вполне возможно, что Autodesk умышленно прикрыла возможность хостинга сервисов в 2016-м accoreconsole.exe, дабы если не полностью пресечь (т.к. это не возможно), то хотя бы максимально усложнить возможность использования инструмента в системе распределённых приложений взаимодействующих через сервисы...

Наличие возможности хостинга WCF-сервисов в accoreconsole.exe для ряда случаев может существенно сократить потребность в количестве приобретаемых лицензий AutoCAD на предприятиях, где выполнение некоторого (если даже не всего) объёма работ можно автоматизировать (т.е. сделать программно). Одним сервисом или набором сервисов (читать как "одной лицензией AutoCAD") в таком случае сможет пользоваться неограниченное количество сотрудников компании одновременно. Для компании клиента это, безусловно, экономия... Но вот для компании Autodesk - это потеря денег... Т.е. налицо явный конфликт интересов и вполне очевидно, кто в данной ситуации имеет на руках "все козыри".

Кроме того, не стоит забывать, что теперь в Autodesk хотят одних и тех же овец стричь каждый год (новая ценовая политика) в виду чего даже сама по себе потенциальная возможность  сокращения лицензий вряд ли будет воспринята ими положительно. Т.о., как я уже писал выше: не исключено, что в 2016-м AutoCAD "кислород" сервисам был перекрыт преднамеренно...

Резюме
Я не удивлюсь, если в более новых версиях AutoCAD приложение accoreconsole.exe будет вовсе тихо изъято, мол "за ненадобностью"...

О хостинге WCF-сервисов в accoreconsole.exe (продолжение)

В продолжение предыдущей записи по обозначенной теме...

Экспериментальным путём выяснил, что проблема хостинга WCF сервисов в accoreconsole.exe присутствует в AutoCAD 2016, но отсутствует во всех более ранних версиях (2013-2015). Проверялось на AutoCAD 2016 x64 SP1 English с установленным Hotfix 3. Проверить наличие обозначенной проблемы в AutoCAD 2017 нет возможности за неимением оного, но очень даже не исключено, что обозначенный баг будет теперь и в нём и во всех последующих версиях...

Примечание:
Во всех без исключениях версиях accoreconsole.exe наблюдал ещё и такую проблему: то, что программно отправляется в консоль AutoCAD через Editor.WriteLine(...) по факту в консоли не появляется... Можно вместо этого воспользоваться Application.ShowAlertDialog(...) - в этом случае текст попадает в консоль, но это очень похоже на выдёргивание зубов плоскогубцами через зад...

О хостинге WCF-сервисов в accoreconsole.exe (AutoCAD 2016)

Как известно, WCF-сервисы могут в качестве хостинга использовать не только IIS и WAS, но так же и произвольные приложения (консольные или GUI). Как показывает практика, в качестве хоста можно использовать acad.exe. В идеале хотелось бы иметь возможность хостить службы в accoreconsole.exe, но не забываем, что это Autodesk, а это означает, что скучать не придётся...


Когда это может оказаться интересным?

Как известно, в параметрах запуска accoreconsole.exe можно указывать набор ключей, в т.ч. и ключ /s, при помощи которого разрешено передавать имя файла скрипта (SCR-файла). По завершению работы скрипта приложение так же автоматически завершит свою работу. Однако порой может возникнуть потребность интерактивного использования функционала, предоставляемого accoreconsole.exe (т.е. само по себе консольное окно при этом не требуется) не закрывая приложение столько времени, сколько потребуется (можно просто скрыть консольное окошко).

Хостинг службы в AutoCAD позволяет другим приложениям (т.н. клиентам) взаимодействовать с ним не прибегая к использованию AutoCAD COM API и при этом получая возможность задействовать возможности AutoCAD .NET API. Кроме того, в любой момент служба может быть перемещена на любой др. компьютер абсолютно прозрачно для клиентов.

В случае необходимости, клиент так же сможет отправлять службе произвольный набор команд, которые будут выполняться в AutoCAD на локальной или удалённо расположенной машине. Например с планшета, работающего под управлением OS Android пользователь сможет отправлять команды пакетной обработки чертежей (очистка, аудит, пересохранение и т.п.).

Это не означает, что на удалённой или локальной машинке обязательно должен быть постоянно запущен AutoCAD. Нет. Клиент может обратиться к службе, которая хостится в IIS или WAS с требованием что-то сделать в AutoCAD. Эта служба запускает AutoCAD (устанавливая видимость его окна в False) и в свою очередь является клиентом для другой службы, хостящейся в AutoCAD, передавая ей ваши запросы, а вам - её ответы. После того, как необходимый вам набор операций в AutoCAD будет выполнен служба, хостящаяся в IIS или WAS завершает работу AutoCAD и ждёт следующих обращений. В случае необходимости, параллельно может быть запущено несколько экземпляров AutoCAD, выполняющих каждый свою задачу, полученную от клиента. Для упрощения в данной теме я не использую промежуточную службу.

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

В идеале, конечно же, лучше всего на роль хоста службы подошёл бы accoreconsole.exe...


Служба

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

namespace Bushman.CAD.Services {

    [ServiceContract(Namespace = "www.gpsm.ru")]
    interface IMyContract {
        [OperationContract]
        void Write(string msg); // Write message into AutoCAD command console.

        [OperationContract]
        string GetVersion(); // Get AutoCAD version
    }
}

Реализуем обозначенный интерфейс как-то так:

using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
using Autodesk.AutoCAD.ApplicationServices;

namespace Bushman.CAD.Services {
    class MyService : IMyContract {
        public string GetVersion() {
            return cad.Version.ToString();
        }

        public void Write(string msg) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if (null != doc) {
                doc.Editor.WriteMessage(msg);
            }
        }
    }

Никаких команд определять не будем, а инициализацию расширения выполним следующим образом:

using System;
using System.ServiceModel;
using cad = Autodesk.AutoCAD.ApplicationServices.Core.Application;
usingAutodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: ExtensionApplication(typeof(Bushman.CAD.Services.ExtensionApplication))]

namespace Bushman.CAD.Services {
    public class ExtensionApplication : IExtensionApplication {

        static ServiceHost host = null;

        static ExtensionApplication() {
            try{
                host = new ServiceHost(typeof(MyService));
                host.Open();
                host.UnknownMessageReceived += Host_UnknownMessageReceived;
                AppDomain.CurrentDomain.ProcessExit += ProcessExit;
            }
            catch(System.Exception ex) {
                Document doc = cad.DocumentManager.MdiActiveDocument;
                if (null != doc) doc.Editor.WriteMessage(ex.Message);
            }
        }

        private static void Host_UnknownMessageReceived(object sender,
            UnknownMessageReceivedEventArgs e) {
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage(e.Message.ToString());
        }

        private static void ProcessExit(object sender, EventArgs e) {
            if(null != host) host.Close();
        }

        public void Initialize() {
            string status = null == host ? "null" : host.State.ToString();
            Document doc = cad.DocumentManager.MdiActiveDocument;
            if(null != doc) doc.Editor.WriteMessage("\nHost status: {0}.\n", status);
        }

        public void Terminate() {
            // Nothing is here.
        }
    }
}


Клиент

Клиента реализуем следующим образом:

using System;
using System.ServiceModel;
usingBushman.MyClient.ServiceReference1;

namespace Bushman.MyClient {
    classProgram {
        static void Main(string[] args) {
            Console.Title = "CAD client";
            try{
                using (MyContractClient client = new MyContractClient("http")) {
                    if (client.InnerChannel.State != CommunicationState.Faulted) {
                        client.Open();
                        string version = client.GetVersion();
                        Console.WriteLine("CAD version: {0}", version);
                        client.Write("Client said: Hello, AutoCAD.\n");
                    }
                }
            }
            catch(Exception ex) {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}


Конфигурационные файлы acad.exe.config и accoreconsole.exe.config настраиваю на работу с обозначенной выше службой:


<configuration>

  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

  <!--All assemblies in AutoCAD are fully trusted so there's no point generating publisher evidence-->
  <runtime>
    <generatePublisherEvidence enabled="false"/>
  </runtime>

  <system.serviceModel>
    <services>
      <service name="Bushman.CAD.Services.MyService" behaviorConfiguration="MEXGET">
        <host>
          <baseAddresses>
            <add baseAddress="http://win7x64ac2:8001"/>
          </baseAddresses>
        </host>
        <endpoint name="http"
                  binding="wsHttpBinding"
                  address="MyService"
                  bindingConfiguration="MyContract"
                  contract ="Bushman.CAD.Services.IMyContract">
        </endpoint>
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="MyContract"/>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MEXGET">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Service-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>
</configuration>


Конфигурационный файл клиента выглядит так:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="http" />
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://win7x64ac2:8001/MyService" binding="wsHttpBinding"
          bindingConfiguration="http" contract="ServiceReference1.IMyContract"
          name="http">
        <identity>
          <!-- WARNING: change the value according your user principal name. -->
          <userPrincipalName value="admin@hyprostr" />
        </identity>
      </endpoint>
    </client>

    <diagnostics wmiProviderEnabled="true">
      <messageLogging
           logEntireMessage="true"
           logMalformedMessages="true"
           logMessagesAtServiceLevel="true"
           logMessagesAtTransportLevel="true"
           maxMessagesToLog="3000"
       />
    </diagnostics>

  </system.serviceModel>

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true" >
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="System.ServiceModel.MessageLogging">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
      <source name="myUserTraceSource"
              switchValue="Information, ActivityTracing">
        <listeners>
          <add name="xml"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="xml"
           type="System.Diagnostics.XmlWriterTraceListener"
                 initializeData="\\hyprostroy\dfs\Обмен\Бушман\logs\Client-Traces.svclog" />
    </sharedListeners>
  </system.diagnostics>

</configuration>


Запускаем службу и клиента
Служба и клиент могут находится как на одном компьютере, так и на разных компьютерах в сети (или в Интернет). Если в качестве хоста использовать acad.exe, то всё нормально работает:



Но вот если в качестве хоста использовать accoreconsole.exe, то достучаться до сервиса не удастся даже с локального компьютера, на котором запущен хост сервиса. При этом сервис успешно запускается, но не доступен:




В логах клиента можно посмотреть описание проблемы:



По обозначенной теме мне ответил Августо Гонсалес:
Augusto Goncalves (API Evangelist at Autodesk):
As far as I remember trying, accoreconsole.exe doesn't accept new calls (from automation) after is open (but I haven't tried with WCF). Acad.exe is a little different... I don't believe accoreconsole will remain receiving calls after it was launched, it was designed to launch with a list of commands on the .scr file.
Если Августо прав, то это будет очередной, весьма досадный недостаток accoreconsole.exe...

Продолжение темы - здесь...

Блокировка кнопки и контекстного меню закрытия консольного окна

Как известно, accoreconsole.exe всегда был и до сих пор остаётся достаточно кривым... Один из неприятных аспектов его поведения, присутствующий по сей день, заключается в том, что если завершать работу приложения кликом мышки по кнопке закрытия консольного окна в верхнем правом углу, либо выбирая соответствующий пункт из контекстного меню консольного окна, то приложение завершает свою работу через задницу - не выполняя код методов Terminate(), а так же код зарегистрированных событий, таких например, как AppDomain.CurrentDomain.ProcessExit.

Однако обозначенная проблема гораздо глубже и не ограничивается рамками кода ваших расширений: при таком способе закрытия AutoCAD так же не выполняет и свой собственный код, который он обычно выполняет при завершении работы приложения (код корректного освобождения ресурсов, сохранения настроек и т.п.). Например, не происходит восстановление настроек в реестре, которые временно были изменены accoreconsole.exe под свои нужды. Это сразу бросается в глаза на напримере переменной FILEDIA, на время работы консольного приложения устанавливается в 0:  при очередном запуске acad.exe для неё приходится вручную восстанавливать значение 1 (в противном случае вместо диалоговых окон AutoCAD будет использовать свою консоль).

Если завершать работу accoreconsole.exe путём вызова команд quit и exit, то завершение работы приложения происходит так, как это должно было происходить (т.е. выполняется весь необходимый код). Однако никто не застрахован от клика мышкой по обозначенной выше кнопке, а пользователи с вероятностью 100% будут клацать как раз именно по ней, когда потребуется завершить работу приложения, потому как такой способ завершения работы - самый простой.

В качестве "лекарства" против обозначенной выше проблемы я блокирую кнопку закрытия консольного окна и соответствующий её пункт контекстного меню:


  1. const uint MF_BYCOMMAND = 0x00000000;
  2. const uint MF_GRAYED = 0x00000001;
  3. const uint SC_CLOSE = 0xF060;
  4. const uint MF_DISABLED = 0x00000002;
  5. [DllImport("kernel32.dll")]
  6. static extern IntPtr GetConsoleWindow();
  7. [DllImport("user32.dll")]
  8. static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
  9. [DllImport("User32.dll", SetLastError = true)]
  10. static extern uint EnableMenuItem(IntPtr hMenu, uint itemId, uint uEnable);
  11. [DllImport("user32.dll")]
  12. static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
  13. ...
  14. // Disable the Close ("X") button and "Close" context menu item of the Console window
  15. IntPtr hwnd = GetConsoleWindow();
  16. IntPtr hmenu = GetSystemMenu(hwnd, false);
  17. uint hWindow = EnableMenuItem(hmenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
  18. // Also it is possible to delete "Close" context menu item
  19. // instead of disabling it.
  20. // DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

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

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

Оказалось, что обозначенная проблема свойственна не только accoreconsole.exe, но и любому консольному приложению в Windows 7 x64, а так же в Windows Server 2003. А вот в Windows 10 x64 всё работает корректно...

Т.о. то, что контекстное меню не блокируется в некоторых версиях Windows - очень похоже на баг WinAPI.

 В этой же теме сразу размещаю код примера того, как можно скрывать или отображать консольное окно (например всё тот же accoreconsole.exe):
  1. [DllImport("kernel32.dll")]
  2. static extern IntPtr GetConsoleWindow();
  3. [DllImport("user32.dll")]
  4. static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  5. const int SW_HIDE = 0;
  6. const int SW_SHOW = 5;
  7. IntPtr hwnd = GetConsoleWindow();
  8. // Hide window
  9. ShowWindow(hwnd, SW_HIDE);
  10. // Show window
  11. ShowWindow(hwnd, SW_SHOW);

Длительная, нередко печальная практика показывает, что лозунг Autodesk касательно данного продукта, к сожалению, выглядит как-то так:
`accoreconsole.exe` - мы заставим Вас работать через задницу!

C#: скачиваем файлы из Интернет


Пример кода для простого скачивания файлов из Интернет (например файла http://stroustrup.com/Programming/PPP2code/std_lib_facilities.h). Сетевые настройки при этом считываются из настроек Internet Explorer.




static voidMain(string[] args) {
    String file_name = Path.GetRandomFileName();
    String full_path = Environment.ExpandEnvironmentVariables(
        Path.Combine(@"%LocalAppData%Temp", file_name));

    using (WebClientclient = new WebClient()) {
        String uri = "http://stroustrup.com/Programming/PPP2code/std_lib_facilities.h";
        var proxyUri = WebRequest.GetSystemWebProxy()
            .GetProxy(new Uri(uri));
        client.Proxy = new WebProxy(proxyUri);
        client.Proxy.Credentials = CredentialCache.DefaultCredentials;
        try{
            client.DownloadFile(uri, full_path);
        }
        catch(Exception ex) {
            Console.WriteLine(ex.Message);
        }
    }
    Console.WriteLine("The result file: {0}", full_path);
    Console.WriteLine("Press any key for exit.");
    Console.ReadKey();
}



Хостинг PowerShell в AutoCAD

На Bitbucket опубликовал пример хостинга PowerShell в AutoCAD. Такой хостинг позволяет программировать в AutoCAD на PowerShell путём использования AutoCAD .NET API. Демонстрационное видео здесь. Откомпилированная под AutoCAD 2016 версия проекта тут.



В проекте продемострировано использования двух подходов в программировании под AutoCAD:

1. Программирование на PowerShell.
2. Динамическая компиляция исходников C# с автоматической загрузкой и возможностью последующего выполнения скомпилированного кода. Аналогичный пример на VB.NET показывать не буду, т.к. там всё происходит аналогичным образом.

Дополнительная информация для размышления: хостинг PowerShell в AutoCAD может оказаться полезным в т.ч. и для программистов, пишущих на AutoLISP\Visual LISP, т.к. помимо доступа к различным технологиям и платформам от Майкрософт, дополнительно предосталяет им возможность в Lisp-коде пользоваться .NET-библиотеками, в т.ч. выполнять динамическую компиляцию произвольного .NET-кода с последующим его исполнением.

Рассказывать о том, что такое PowerShell и зачем он нужен не буду - желающие смогут без труда сами найти информацию на эту тему в Интернете.