How to use ActiveX component in ClassLibrary without Winforms

前端 未结 1 1503
轮回少年
轮回少年 2020-12-20 06:58

How is it possible to use an ActiveX control in a ClassLibrary type project?

I intend to call it later from WPF application but I don\'t want to place a control anyw

相关标签:
1条回答
  • 2020-12-20 07:49

    I think that the common knowledge is that you need Winforms to be able to use ActiveX control. Well, not entirely true. You need winforms-like message loop and STAThread.

    Let's start by presenting the design of my solution. I prefer to seperate code to as many layers as needed when dealing with something unknown so you may find some layers redundant. I encourage you to help me improve the solution to find the equilibrium.

    Please remember about the need to implement the IDisposable interface in all outer layers if needed.

    ActiveXCore - class containing an ActiveX control declared as a private field. In this class you use just code like you would in Winforms.

    CoreAPI - an internal API class that exposes the methods of ActiveXCore. I found out that it is good to mark the methods with [STAThreadAttribute] as I had some problems without it, though it may be specific to this case only.

    PublicAPI - my main library class that will be called in the referencing projects.

    Now, in the ActiveXCore there are really no guidelines. In CoreAPI the sample method would be

    [STAThreadAttribute]
    internal bool Init()
    {
        try
        {
            _core = new ActiveXCore();
            //...
    
            return true;
        }
        catch (System.Runtime.InteropServices.COMException)
        {
            //handle the exception
        }
        return false;
    }
    

    To be able to properly run these you would need Winforms like message loop like this (the desing is not mine at all, I just slightly modified the code). You don't need the Winforms project type, but you have to reference System.Windows.Forms assembly

    public class MessageLoopApartment : IDisposable
    {
        public static MessageLoopApartment Apartament
        {
            get
            {
                if (_apartament == null)
                    _apartament = new MessageLoopApartment();
                return _apartament;
            }
        }
    
        private static MessageLoopApartment _apartament;
        private Thread _thread; // the STA thread
    
        private TaskScheduler _taskScheduler; // the STA thread's task scheduler
    
        public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
    
        /// <summary>MessageLoopApartment constructor</summary>
        public MessageLoopApartment()
        {
            var tcs = new TaskCompletionSource<TaskScheduler>();
    
            // start an STA thread and gets a task scheduler
            _thread = new Thread(startArg =>
            {
                EventHandler idleHandler = null;
    
                idleHandler = (s, e) =>
                {
                    // handle Application.Idle just once
                    Application.Idle -= idleHandler;
                    // return the task scheduler
                    tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
                };
    
                // handle Application.Idle just once
                // to make sure we're inside the message loop
                // and SynchronizationContext has been correctly installed
                Application.Idle += idleHandler;
                Application.Run();
            });
    
            _thread.SetApartmentState(ApartmentState.STA);
            _thread.IsBackground = true;
            _thread.Start();
            _taskScheduler = tcs.Task.Result;
        }
    
        /// <summary>shutdown the STA thread</summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool disposing)
        {
            if (_taskScheduler != null)
            {
                var taskScheduler = _taskScheduler;
                _taskScheduler = null;
    
                // execute Application.ExitThread() on the STA thread
                Task.Factory.StartNew(
                    () => Application.ExitThread(),
                    CancellationToken.None,
                    TaskCreationOptions.None,
                    taskScheduler).Wait();
    
                _thread.Join();
                _thread = null;
            }
        }
    
        /// <summary>Task.Factory.StartNew wrappers</summary>
        public void Invoke(Action action)
        {
            Task.Factory.StartNew(action,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
        }
    
        public TResult Invoke<TResult>(Func<TResult> action)
        {
            return Task.Factory.StartNew(action,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
        }
    
        public Task Run(Action action, CancellationToken token)
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        }
    
        public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        }
    
        public Task Run(Func<Task> action, CancellationToken token)
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }
    
        public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }
    }
    

    And then you can provide methods like that

    public bool InitLib()
    {   
        return MessageLoopApartment.Apartament.Run(() =>
        {
             ca = new CoreAPI();
             bool initialized = ca.Init();
        }, CancellationToken.None).Result;
    }
    

    of if the method would be void

    public void InitLib()
    {   
        MessageLoopApartment.Apartament.Run(() =>
        {
             ca = new CoreAPI();
             ca.Init();
        }, CancellationToken.None).Wait();
    }
    

    As for the auto registering I designed something like this (I call it from CoreAPI)

    internal static class ComponentEnvironment
    {
        internal static void Prepare()
        {   
            CopyFilesAndDeps();
    
            if (Environment.Is64BitOperatingSystem) 
                RegSvr64();
            RegSvr32(); //you may notice no "else" here
            //in my case for 64 bit I had to copy and register files for both arch 
        }
    
        #region unpack and clean files
    
        private static void CopyFilesAndDeps()
        {
            //inspect what file you need
        }
    
        #endregion unpack and clean files
    
        #region register components
    
        private static void RegSvr32()
        {
            string dllPath = @"xxx\x86\xxx.dll";
            Process.Start("regsvr32", "/s " + dllPath);
        }
    
        private static void RegSvr64()
        {
            string dllPath = @"xxx\x64\xxx.dll";
            Process.Start("regsvr32", "/s " + dllPath);
        }
    
        #endregion register components
    }
    

    I spent many days and nights to design this reusable pattern so I hope it will help someone.

    0 讨论(0)
提交回复
热议问题