web-dev-qa-db-fra.com

Meilleures pratiques pour une instance unique WPF

Voici le code que j'ai implémenté jusqu'à présent pour créer une application WPF à une seule instance:

#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
    }
}

Tout fonctionne parfaitement ... mais j'ai quelques doutes à ce sujet et j'aimerais recevoir vos suggestions sur la façon dont mon approche pourrait être améliorée.

1) Code Analysis m'a demandé d'implémenter l'interface IDisposable car j'utilisais les membres IDisposable (les Mutex). Mon implémentation de Dispose() est-elle assez bonne? Dois-je l'éviter car il ne sera jamais appelé?

2) Il vaut mieux utiliser m_Mutex = new Mutex(true, mutexName, out mutexCreated); et vérifier le résultat ou utiliser m_Mutex = new Mutex(false, mutexName); puis vérifier m_Mutex.WaitOne(TimeSpan.Zero, false);? En cas de multithreading, je veux dire ...

3) RegisterWindowMessage L'appel d'API doit renvoyer UInt32 ... mais HwndSourceHook n'accepte que Int32 Comme valeur de message ... si je m'inquiète des comportements inattendus (comme un résultat supérieur à Int32.MaxValue)?

4) Dans OnStartup override ... dois-je exécuter base.OnStartup(e); même si une autre instance est déjà en cours d'exécution et que je vais fermer l'application?

5) Existe-t-il un meilleur moyen d'amener l'instance existante au sommet qui n'a pas besoin de définir la valeur de Topmost? Peut-être Activate()?

6) Pouvez-vous voir un défaut dans mon approche? Quelque chose concernant le multithreading, la mauvaise gestion des exceptions et quelque chose comme ça? Par exemple ... que se passe-t-il si mon application plante entre OnStartup et OnExit?

35
Tommaso Belluzzo

1) Cela ressemble à une implémentation Dispose standard pour moi. Ce n'est pas vraiment nécessaire (voir point 6) mais cela ne fait pas de mal. (Nettoyage à la fermeture, c'est un peu comme nettoyer la maison avant de la brûler, à mon humble avis, mais les opinions sur la question diffèrent ..)

Quoi qu'il en soit, pourquoi ne pas utiliser "Dispose" comme nom de la méthode de nettoyage, même si elle n'est pas appelée directement? Vous auriez pu l'appeler "Cleanup", mais n'oubliez pas que vous écrivez également du code pour les humains, et Dispose semble familier et que quiconque sur .NET comprend à quoi cela sert. Alors, optez pour "Dispose".

2) J'ai toujours vu m_Mutex = new Mutex(false, mutexName); Je pense que c'est plus une convention qu'un avantage technique.

3) À partir de MSDN:

Si le message est enregistré avec succès, la valeur de retour est un identifiant de message compris entre 0xC000 et 0xFFFF.

Je ne m'inquiéterais donc pas. Habituellement, pour cette classe de fonctions, UInt n'est pas utilisé pour "il ne rentre pas dans Int, utilisons UInt pour avoir quelque chose de plus" mais pour clarifier un contrat "la fonction ne retourne jamais de valeur négative".

4) J'éviterais de l'appeler si vous arrêtez, même raison que # 1

5) Il y a plusieurs façons de procéder. La manière la plus simple dans Win32 est simplement de demander à la deuxième instance d'appeler SetForegroundWindow (Regardez ici: http://blogs.msdn.com/b/oldnewthing/archive/2009/02/20/9435239.aspx ); cependant, je ne sais pas s'il existe une fonctionnalité WPF équivalente ou si vous devez l'inclure.

6)

Par exemple ... que se passe-t-il si mon application se bloque entre OnStartup et OnExit?

C'est OK: quand un processus se termine, tous les descripteurs appartenant au processus sont libérés; le mutex est également libéré.

Bref, mes recommandations:

  • J'utiliserais une approche basée sur des objets de synchronisation nommés: c'est la plus établie sur la ou les plateformes Windows. (Soyez prudent lorsque vous envisagez un système multi-utilisateur, comme un serveur de terminaux! Nommez l'objet de synchronisation en combinant, peut-être, le nom d'utilisateur/SID et le nom de l'application)
  • Utilisez l'API Windows pour augmenter l'instance précédente (voir mon lien au point # 5), ou l'équivalent WPF.
  • Vous n'avez probablement pas à vous soucier des plantages (le noyau diminuera le compteur de références pour l'objet noyau pour vous; faites quand même un petit test), MAIS si je peux suggérer une amélioration: que faire si votre première instance d'application ne plante pas mais se bloque? (Cela se produit avec Firefox .. Je suis sûr que cela vous est arrivé aussi! Pas de fenêtre, processus ff, vous ne pouvez pas en ouvrir une nouvelle). Dans ce cas, il peut être bon de combiner une autre technique ou deux, pour a) tester si l'application/la fenêtre répond; b) trouver l'instance bloquée et y mettre fin

Par exemple, vous pouvez utiliser votre technique (essayer d'envoyer/publier un message dans la fenêtre - si elle ne répond pas, elle est bloquée), ainsi que la technique MSK, pour rechercher et terminer l'ancien processus. Commencez ensuite normalement.

7
Lorenzo Dematté

Il y a plusieurs choix,

  • Mutex
  • Gestionnaire de processus
  • Nommé sémaphore
  • Utilisez une prise d'écoute

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();  
    }
}

Gestionnaire de processus

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(); 
    }
}

Utilisez une prise d'écoute

Une façon de signaler une autre application consiste à lui ouvrir une connexion Tcp. Créez un socket, liez-le à un port et écoutez sur un thread d'arrière-plan les connexions. Si cela réussit, exécutez normalement. Sinon, établissez une connexion à ce port, ce qui signale à l'autre instance qu'une deuxième tentative de lancement d'application a été effectuée. L'instance d'origine peut ensuite mettre sa fenêtre principale au premier plan, le cas échéant.

Les logiciels/pare-feu de "sécurité" peuvent poser problème.

Application à instance unique C # .Net avec Win32

41
C-va

Je voulais avoir une expérience utilisateur un peu meilleure - si une autre instance est déjà en cours d'exécution, activons-la plutôt que d'afficher une erreur sur la deuxième instance. Voici ma mise en œuvre.

J'utilise nommé Mutex pour m'assurer qu'une seule instance est en cours d'exécution et nommé EventWaitHandle pour passer la notification d'une instance à une autre.

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
}

Et BringToForeground dans 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();
    }

Et ajoutez Startup = "AppOnStartup" (merci 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>

Travaille pour moi :)

32
ZakiMa

Pour WPF, utilisez simplement:

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);
    }          
}
29
smedasn

La façon la plus simple de gérer cela serait d'utiliser un sémaphore nommé. Essayez quelque chose comme ça ...

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();
        }
    }

}
5
blaise

J'ai utilisé un simple TCP socket pour cela (en Java, il y a 10 ans).

  1. Au démarrage, connectez-vous à un port prédéfini, si la connexion est acceptée, une autre instance est en cours d'exécution, sinon, démarrez un TCP écouteur
  2. Une fois que quelqu'un se connecte à vous, ouvrez la fenêtre et déconnectez-vous
4
Luuk

pour empêcher une deuxième instance,

  • en utilisant EventWaitHandle (puisque nous parlons d'un événement),
  • en utilisant Task,
  • aucun code Mutex requis,
  • pas de TCP,
  • pas de Pinvokes,
  • pas de trucs GarbageCollection,
  • fil de sauvegarde

cela pourrait être fait comme ceci (ceci pour une application WPF (voir ref à App ()), mais fonctionne aussi sur WinForms):

public partial class App : Application
{
    public App()
    {
        // initiate it. Call it first.
        preventSecond();
    }

    private const string UniqueEventName = "{GENERATE-YOUR-OWN-GUID}";

    private void preventSecond()
    {
        try
        {
            EventWaitHandle.OpenExisting(UniqueEventName); // check if it exists
            this.Shutdown();
        }
        catch (WaitHandleCannotBeOpenedException)
        {
            new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName); // register
        }
    }
}

Deuxième version: ci-dessus plus signalisation de l'autre instance pour afficher la fenêtre (changez la partie MainWindow pour WinForms):

public partial class App : Application
{
    public App()
    {
        // initiate it. Call it first.
        //preventSecond();
        SingleInstanceWatcher();
    }

    private const string UniqueEventName = "{GENERATE-YOUR-OWN-GUID}";
    private EventWaitHandle eventWaitHandle;

    /// <summary>prevent a second instance and signal it to bring its mainwindow to foregorund</summary>
    /// <seealso cref="https://stackoverflow.com/a/23730146/1644202"/>
    private void SingleInstanceWatcher()
    {
        // check if it is allready open.
        try
        {
            // try to open it - if another instance is running, it will exist
            this.eventWaitHandle = EventWaitHandle.OpenExisting(UniqueEventName);

            // Notify other instance so it could bring itself to foreground.
            this.eventWaitHandle.Set();

            // Terminate this instance.
            this.Shutdown();
        }
        catch (WaitHandleCannotBeOpenedException)
        {
            // listen to a new event
            this.eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName);
        }

        // if this instance gets the signal to show the main window
        new Task(() =>
        {
            while (this.eventWaitHandle.WaitOne())
            {
                Current.Dispatcher.BeginInvoke((Action)(() =>
                {
                    // could be set or removed anytime
                    if (!Current.MainWindow.Equals(null))
                    {
                        var mw = Current.MainWindow;

                        if (mw.WindowState == WindowState.Minimized || mw.Visibility != Visibility.Visible)
                        {
                            mw.Show();
                            mw.WindowState = WindowState.Normal;
                        }

                        // According to some sources these steps gurantee that an app will be brought to foreground.
                        mw.Activate();
                        mw.Topmost = true;
                        mw.Topmost = false;
                        mw.Focus();
                    }
                }));
            }
        })
        .Start();
    }
}

Ce code en tant que drop in class, sera @ Selfcontained-C-Sharp-WPF-compatible-utility-classes-utilitaires/tils.SingleInstance.cs

4
BananaAcid

Il s'agit d'une solution simple, ouvrez votre fichier de démarrage (vue à partir de l'endroit où votre application démarre) dans ce cas son MainWindow.xaml. Ouvrez votre fichier MainWindow.xaml.cs. Allez dans le constructeur et après intializecomponent () ajoutez ce code:

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();
 }

N'oubliez pas d'ajouter System.Diagnostics

2
Hamed

Voici un exemple qui amène l'ancienne instance au premier plan également:

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);
        }
    }
}

Mais cela ne fonctionnera que si le titre de votre fenêtre principale ne change pas pendant l'exécution.

Modifier:

Vous pouvez également utiliser l'événement Startup dans App.xaml Au lieu de remplacer 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

N'oubliez pas de ne pas appeler base.OnStartup(e) dans ce cas!

0
Dave_cz