c# - Mejores prácticas de instancia única de WPF
singleton mutex (8)
1) Me parece una implementación estándar de Dispose. No es realmente necesario (ver punto 6) pero no hace ningún daño. (La limpieza al cerrar es un poco como limpiar la casa antes de incendiarla, IMHO, pero las opiniones sobre el tema son diferentes ...)
De todos modos, ¿por qué no usar "Dispose" como nombre del método de limpieza, incluso si no se llama directamente? Podrías haberlo llamado "Limpieza", pero recuerda que también escribes código para humanos, y Dispose parece familiar y cualquier persona en .NET entiende para qué sirve. Entonces, ve por "Disose".
2) Siempre he visto m_Mutex = new Mutex(false, mutexName);
Sin embargo, creo que es más una convención que una ventaja técnica.
3) Desde MSDN:
Si el mensaje se registra correctamente, el valor de retorno es un identificador de mensaje en el rango de 0xC000 a 0xFFFF.
Así que no me preocuparía. Por lo general, para esta clase de funciones, UInt no se usa para "no encaja en Int, usemos UInt, así que tenemos algo más", pero para aclarar un contrato ", la función nunca devuelve un valor negativo".
4) Evitaría llamarlo si cierras, por la misma razón que # 1
5) Hay un par de maneras de hacerlo. La forma más fácil en Win32 es simplemente hacer que la segunda instancia realice la llamada a SetForegroundWindow (consulte aquí: http://blogs.msdn.com/b/oldnewthing/archive/2009/02/20/9435239.aspx ); sin embargo, no sé si hay una funcionalidad WPF equivalente o si necesita hablarla.
6)
Por ejemplo ... ¿qué sucede si mi aplicación falla entre OnStartup y OnExit?
Está bien: cuando finaliza un proceso, se liberan todos los controladores que pertenecen al proceso; el mutex se libera también.
En resumen, mis recomendaciones:
- Usaría un enfoque basado en objetos de sincronización con nombre: es el más establecido en las plataformas de Windows. (¡Tenga cuidado al considerar un sistema multiusuario, como Terminal Server! Nombre el objeto de sincronización como una combinación de, tal vez, nombre de usuario / SID y nombre de aplicación)
- Use la API de Windows para generar la instancia anterior (vea mi enlace en el punto # 5), o el equivalente de WPF.
- Probablemente no tenga que preocuparse por los bloqueos (el kernel disminuirá el contador de ref para el objeto del kernel por usted; haga una pequeña prueba de todos modos), PERO, si puedo sugerir una mejora: ¿qué sucede si su primera instancia de aplicación no falla pero se bloquea? (Sucede con Firefox ... ¡Estoy seguro de que también te sucedió a ti! Sin ventana, no se puede abrir una nueva). En ese caso, puede ser bueno combinar otra técnica o dos, para a) probar si la aplicación / ventana responde; b) encontrar la instancia colgada y terminarla
Por ejemplo, puede usar su técnica (intentar enviar / publicar un mensaje en la ventana; si no responde, está bloqueada), además de la técnica MSK, para encontrar y terminar el proceso anterior. Entonces comience normalmente.
Este es el código que implementé hasta ahora para crear una aplicación WPF de instancia única:
#region Using Directives
using System;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
#endregion
namespace MyWPF
{
public partial class MainApplication : Application, IDisposable
{
#region Members
private Int32 m_Message;
private Mutex m_Mutex;
#endregion
#region Methods: Functions
private IntPtr HandleMessages(IntPtr handle, Int32 message, IntPtr wParameter, IntPtr lParameter, ref Boolean handled)
{
if (message == m_Message)
{
if (MainWindow.WindowState == WindowState.Minimized)
MainWindow.WindowState = WindowState.Normal;
Boolean topmost = MainWindow.Topmost;
MainWindow.Topmost = true;
MainWindow.Topmost = topmost;
}
return IntPtr.Zero;
}
private void Dispose(Boolean disposing)
{
if (disposing && (m_Mutex != null))
{
m_Mutex.ReleaseMutex();
m_Mutex.Close();
m_Mutex = null;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region Methods: Overrides
protected override void OnStartup(StartupEventArgs e)
{
Assembly assembly = Assembly.GetExecutingAssembly();
Boolean mutexCreated;
String mutexName = String.Format(CultureInfo.InvariantCulture, "Local//{{{0}}}{{{1}}}", assembly.GetType().GUID, assembly.GetName().Name);
m_Mutex = new Mutex(true, mutexName, out mutexCreated);
m_Message = NativeMethods.RegisterWindowMessage(mutexName);
if (!mutexCreated)
{
m_Mutex = null;
NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, m_Message, IntPtr.Zero, IntPtr.Zero);
Current.Shutdown();
return;
}
base.OnStartup(e);
MainWindow window = new MainWindow();
MainWindow = window;
window.Show();
HwndSource.FromHwnd((new WindowInteropHelper(window)).Handle).AddHook(new HwndSourceHook(HandleMessages));
}
protected override void OnExit(ExitEventArgs e)
{
Dispose();
base.OnExit(e);
}
#endregion
}
}
Todo funciona perfectamente ... pero tengo algunas dudas al respecto y me gustaría recibir sus sugerencias acerca de cómo podría mejorarse mi enfoque.
1) Code Analysis me pidió que implementara la interfaz IDisposable
porque estaba usando miembros IDisposable
(el Mutex
). ¿Es mi implementación de Dispose()
suficientemente buena? ¿Debo evitarlo porque nunca va a ser llamado?
2) Es mejor usar m_Mutex = new Mutex(true, mutexName, out mutexCreated);
y verifique el resultado o use m_Mutex = new Mutex(false, mutexName);
y luego verifique m_Mutex.WaitOne(TimeSpan.Zero, false);
? En caso de multihilo quiero decir ...
3) La llamada a la API RegisterWindowMessage
debería devolver UInt32
... pero HwndSourceHook
solo acepta Int32
como valor de mensaje ... ¿debería preocuparme por comportamientos inesperados (como un resultado mayor que Int32.MaxValue
)?
4) En OnStartup
override ... debería ejecutar base.OnStartup(e);
¿Incluso si ya se está ejecutando otra instancia y voy a cerrar la aplicación?
5) ¿Hay una mejor manera de llevar la instancia existente a la cima que no necesite establecer el valor Topmost
? Quizás Activate()
?
6) ¿Puedes ver algún defecto en mi enfoque? ¿Algo relacionado con multihilo, manejo de malas excepciones y algo así? Por ejemplo ... ¿qué sucede si mi aplicación falla entre OnStartup
y OnExit
?
Aquí hay un ejemplo que trae la antigua instancia a primer plano también:
public partial class App : Application
{
[DllImport("user32", CharSet = CharSet.Unicode)]
static extern IntPtr FindWindow(string cls, string win);
[DllImport("user32")]
static extern IntPtr SetForegroundWindow(IntPtr hWnd);
[DllImport("user32")]
static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32")]
static extern bool OpenIcon(IntPtr hWnd);
private static Mutex _mutex = null;
protected override void OnStartup(StartupEventArgs e)
{
const string appName = "LinkManager";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
ActivateOtherWindow();
//app is already running! Exiting the application
Application.Current.Shutdown();
}
base.OnStartup(e);
}
private static void ActivateOtherWindow()
{
var other = FindWindow(null, "!YOUR MAIN WINDOW TITLE HERE!");
if (other != IntPtr.Zero)
{
SetForegroundWindow(other);
if (IsIconic(other))
OpenIcon(other);
}
}
}
Pero solo funcionará si el título de la ventana principal no cambia durante el tiempo de ejecución.
Editar:
También puede usar el evento de Startup
en App.xaml
en lugar de anular OnStartup
.
// App.xaml.cs
private void Application_Startup(object sender, StartupEventArgs e)
{
const string appName = "LinkManager";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
ActivateOtherWindow();
//app is already running! Exiting the application
Application.Current.Shutdown();
}
}
// App.xaml
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
StartupUri="MainWindow.xaml" Startup="Application_Startup"> //<- startup event
Recuerde no llamar a base.OnStartup(e)
en este caso!
Esta es una solución simple, abra su archivo de inicio (Vea desde donde se inicia su aplicación) en este caso su MainWindow.xaml. Abra su archivo MainWindow.xaml.cs. Vaya al constructor y después de intializecomponent () agregue este código:
Process Currentproc = Process.GetCurrentProcess();
Process[] procByName=Process.GetProcessesByName("notepad"); //Write the name of your exe file in inverted commas
if(procByName.Length>1)
{
MessageBox.Show("Application is already running");
App.Current.Shutdown();
}
No te olvides de añadir System.Diagnostics
Hay varias opciones,
- Mutex
- Gestor de procesos
- Llamado semáforo
Usa un zócalo de oyente
Mutex
Mutex myMutex ; private void Application_Startup(object sender, StartupEventArgs e) { bool aIsNewInstance = false; myMutex = new Mutex(true, "MyWPFApplication", out aIsNewInstance); if (!aIsNewInstance) { MessageBox.Show("Already an instance is running..."); App.Current.Shutdown(); } }
Gestor de procesos
private void Application_Startup(object sender, StartupEventArgs e) { Process proc = Process.GetCurrentProcess(); int count = Process.GetProcesses().Where(p=> p.ProcessName == proc.ProcessName).Count(); if (count > 1) { MessageBox.Show("Already an instance is running..."); App.Current.Shutdown(); } }
Usa un zócalo de oyente
Una forma de señalar otra aplicación es abrir una conexión TCP a ella. Cree un socket, enlace a un puerto y escuche en un hilo de fondo las conexiones. Si esto tiene éxito, ejecute normalmente. Si no, haga una conexión a ese puerto, lo que indica a la otra instancia que se ha realizado un segundo intento de inicio de la aplicación. La instancia original puede traer su ventana principal al frente, si es apropiado.
El software / cortafuegos de "seguridad" puede ser un problema.
He utilizado un simple socket TCP para esto (en Java, hace 10 años).
- En el inicio, conéctese a un puerto predefinido, si se acepta la conexión, se está ejecutando otra instancia, si no, inicie una escucha TCP
- Una vez que alguien se conecta a ti, abre la ventana y desconecta
La forma más directa de manejar eso sería usar un semáforo nombrado. Intenta algo como esto ...
public partial class App : Application
{
Semaphore sema;
bool shouldRelease = false;
protected override void OnStartup(StartupEventArgs e)
{
bool result = Semaphore.TryOpenExisting("SingleInstanceWPFApp", out sema);
if (result) // we have another instance running
{
App.Current.Shutdown();
}
else
{
try
{
sema = new Semaphore(1, 1, "SingleInstanceWPFApp");
}
catch
{
App.Current.Shutdown(); //
}
}
if (!sema.WaitOne(0))
{
App.Current.Shutdown();
}
else
{
shouldRelease = true;
}
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{
if (sema != null && shouldRelease)
{
sema.Release();
}
}
}
Para WPF solo usa:
public partial class App : Application
{
private static Mutex _mutex = null;
protected override void OnStartup(StartupEventArgs e)
{
const string appName = "MyAppName";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
//app is already running! Exiting the application
Application.Current.Shutdown();
}
base.OnStartup(e);
}
}
Quería tener una experiencia de usuario un poco mejor: si ya se está ejecutando otra instancia, activémosla en lugar de mostrar un error sobre la segunda instancia. Aquí está mi implementación.
Utilizo Mutex con nombre para asegurarme de que solo se ejecute una instancia y se llame EventWaitHandle para pasar la notificación de una instancia a otra.
App.xaml.cs:
/// <summary>Interaction logic for App.xaml</summary>
public partial class App
{
#region Constants and Fields
/// <summary>The event mutex name.</summary>
private const string UniqueEventName = "{GUID}";
/// <summary>The unique mutex name.</summary>
private const string UniqueMutexName = "{GUID}";
/// <summary>The event wait handle.</summary>
private EventWaitHandle eventWaitHandle;
/// <summary>The mutex.</summary>
private Mutex mutex;
#endregion
#region Methods
/// <summary>The app on startup.</summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
private void AppOnStartup(object sender, StartupEventArgs e)
{
bool isOwned;
this.mutex = new Mutex(true, UniqueMutexName, out isOwned);
this.eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
// So, R# would not give a warning that this variable is not used.
GC.KeepAlive(this.mutex);
if (isOwned)
{
// Spawn a thread which will be waiting for our event
var thread = new Thread(
() =>
{
while (this.eventWaitHandle.WaitOne())
{
Current.Dispatcher.BeginInvoke(
(Action)(() => ((MainWindow)Current.MainWindow).BringToForeground()));
}
});
// It is important mark it as background otherwise it will prevent app from exiting.
thread.IsBackground = true;
thread.Start();
return;
}
// Notify other instance so it could bring itself to foreground.
this.eventWaitHandle.Set();
// Terminate this instance.
this.Shutdown();
}
#endregion
}
Y BringToForeground en MainWindow.cs:
/// <summary>Brings main window to foreground.</summary>
public void BringToForeground()
{
if (this.WindowState == WindowState.Minimized || this.Visibility == Visibility.Hidden)
{
this.Show();
this.WindowState = WindowState.Normal;
}
// According to some sources these steps gurantee that an app will be brought to foreground.
this.Activate();
this.Topmost = true;
this.Topmost = false;
this.Focus();
}
Y agregue Startup = "AppOnStartup" (gracias vhanla!):
<Application x:Class="MyClass.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="AppOnStartup">
<Application.Resources>
</Application.Resources>
</Application>
Funciona para mi :)