c# - Cómo usar el componente ActiveX en ClassLibrary sin Winforms
wpf (1)
¿Cómo es posible usar un control ActiveX en un proyecto tipo ClassLibrary?
Tengo la intención de llamarlo más tarde desde la aplicación WPF, pero no quiero colocar un control en ningún lugar en el formulario, por lo que no quiero usar WindowsFormsHost
; principalmente porque me gustaría utilizar esta mi biblioteca en la aplicación de consola y el servicio de Windows.
En este caso, el control ActiveX que quiero usar es un componente de análisis de video. Además, quiero que mi componente se registre en un entorno desplegado.
Creo que el conocimiento común es que necesita Winforms para poder usar el control ActiveX. Bueno, no del todo cierto. Necesitas winforms-like loop de mensaje y STAThread.
Comencemos presentando el diseño de mi solución. Prefiero separar el código en tantas capas como sea necesario cuando se trata de algo desconocido, por lo que puede encontrar algunas capas redundantes. Te animo a que me ayudes a mejorar la solución para encontrar el equilibrio.
Recuerde la necesidad de implementar la interfaz IDisposable
en todas las capas externas si es necesario.
ActiveXCore
- clase que contiene un control ActiveX declarado como un campo privado. En esta clase usas solo código como lo harías en Winforms.
CoreAPI
- una clase API interna que expone los métodos de ActiveXCore
. Descubrí que es bueno marcar los métodos con [STAThreadAttribute]
ya que tuve algunos problemas sin él, aunque puede ser específico solo para este caso.
PublicAPI
- mi clase de biblioteca principal que se llamará en los proyectos de referencia.
Ahora, en ActiveXCore
realmente no hay pautas. En CoreAPI
el método de muestra sería
[STAThreadAttribute]
internal bool Init()
{
try
{
_core = new ActiveXCore();
//...
return true;
}
catch (System.Runtime.InteropServices.COMException)
{
//handle the exception
}
return false;
}
Para poder ejecutarlos correctamente necesitarías Winforms como un ciclo de mensajes como este (el diseño no es mío para nada, simplemente modifiqué ligeramente el código). No necesita el tipo de proyecto Winforms, pero debe hacer referencia al ensamblado System.Windows.Forms
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();
}
}
Y luego puedes proporcionar métodos como ese
public bool InitLib()
{
return MessageLoopApartment.Apartament.Run(() =>
{
ca = new CoreAPI();
bool initialized = ca.Init();
}, CancellationToken.None).Result;
}
de si el método sería nulo
public void InitLib()
{
MessageLoopApartment.Apartament.Run(() =>
{
ca = new CoreAPI();
ca.Init();
}, CancellationToken.None).Wait();
}
En cuanto al registro automático, diseñé algo así (lo llamo de 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
}
Pasé muchos días y noches para diseñar este patrón reutilizable, así que espero que ayude a alguien.