Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
342 views
in Technique[技术] by (71.8m points)

c# - How to use ActiveX component in ClassLibrary without Winforms

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 anywhere on the form, so I don't want to use WindowsFormsHost; mainly because I would like to use this my library in Console App and Windows Service.

In this case, the ActiveX control I want to use is a video analysis component. Additionally I want my component to register itself in deployed environment.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 = @"xxxx86xxx.dll";
        Process.Start("regsvr32", "/s " + dllPath);
    }

    private static void RegSvr64()
    {
        string dllPath = @"xxxx64xxx.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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...