Как правильно создать приложение WPF с одним экземпляром? - PullRequest
607 голосов
/ 21 августа 2008

Используя C # и WPF под .NET (а не Windows Forms или консоль), как правильно создать приложение, которое можно запустить только как один экземпляр?

Я знаю, что это как-то связано с какой-то мифической штукой, называемой мьютексом, редко я могу найти кого-то, кто мешает остановиться и объяснить, что из этого есть.

Код должен также информировать уже запущенный экземпляр о том, что пользователь попытался запустить второй, и, возможно, также передать любые аргументы командной строки, если таковые были.

Ответы [ 35 ]

6 голосов
/ 19 апреля 2013

Следующий код - мое решение WCF с именованными каналами для регистрации приложения с одним экземпляром. Это хорошо, потому что это также вызывает событие, когда другой экземпляр пытается запустить, и получает командную строку другого экземпляра.

Он ориентирован на WPF, поскольку использует класс System.Windows.StartupEventHandler, но его можно легко изменить.

Этот код требует ссылки на PresentationFramework и System.ServiceModel.

Usage:

class Program
{
    static void Main()
    {
        var applicationId = new Guid("b54f7b0d-87f9-4df9-9686-4d8fd76066dc");

        if (SingleInstanceManager.VerifySingleInstance(applicationId))
        {
            SingleInstanceManager.OtherInstanceStarted += OnOtherInstanceStarted;

            // Start the application
        }
    }

    static void OnOtherInstanceStarted(object sender, StartupEventArgs e)
    {
        // Do something in response to another instance starting up.
    }
}

Исходный код:

/// <summary>
/// A class to use for single-instance applications.
/// </summary>
public static class SingleInstanceManager
{
  /// <summary>
  /// Raised when another instance attempts to start up.
  /// </summary>
  public static event StartupEventHandler OtherInstanceStarted;

  /// <summary>
  /// Checks to see if this instance is the first instance running on this machine.  If it is not, this method will
  /// send the main instance this instance's startup information.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  /// <returns>True if this instance is the main instance.</returns>
  public static bool VerifySingleInstace(Guid guid)
  {
    if (!AttemptPublishService(guid))
    {
      NotifyMainInstance(guid);

      return false;
    }

    return true;
  }

  /// <summary>
  /// Attempts to publish the service.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  /// <returns>True if the service was published successfully.</returns>
  private static bool AttemptPublishService(Guid guid)
  {
    try
    {
      ServiceHost serviceHost = new ServiceHost(typeof(SingleInstance));
      NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
      serviceHost.AddServiceEndpoint(typeof(ISingleInstance), binding, CreateAddress(guid));
      serviceHost.Open();

      return true;
    }
    catch
    {
      return false;
    }
  }

  /// <summary>
  /// Notifies the main instance that this instance is attempting to start up.
  /// </summary>
  /// <param name="guid">The application's unique identifier.</param>
  private static void NotifyMainInstance(Guid guid)
  {
    NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
    EndpointAddress remoteAddress = new EndpointAddress(CreateAddress(guid));
    using (ChannelFactory<ISingleInstance> factory = new ChannelFactory<ISingleInstance>(binding, remoteAddress))
    {
      ISingleInstance singleInstance = factory.CreateChannel();
      singleInstance.NotifyMainInstance(Environment.GetCommandLineArgs());
    }
  }

  /// <summary>
  /// Creates an address to publish/contact the service at based on a globally unique identifier.
  /// </summary>
  /// <param name="guid">The identifier for the application.</param>
  /// <returns>The address to publish/contact the service.</returns>
  private static string CreateAddress(Guid guid)
  {
    return string.Format(CultureInfo.CurrentCulture, "net.pipe://localhost/{0}", guid);
  }

  /// <summary>
  /// The interface that describes the single instance service.
  /// </summary>
  [ServiceContract]
  private interface ISingleInstance
  {
    /// <summary>
    /// Notifies the main instance that another instance of the application attempted to start.
    /// </summary>
    /// <param name="args">The other instance's command-line arguments.</param>
    [OperationContract]
    void NotifyMainInstance(string[] args);
  }

  /// <summary>
  /// The implementation of the single instance service interface.
  /// </summary>
  private class SingleInstance : ISingleInstance
  {
    /// <summary>
    /// Notifies the main instance that another instance of the application attempted to start.
    /// </summary>
    /// <param name="args">The other instance's command-line arguments.</param>
    public void NotifyMainInstance(string[] args)
    {
      if (OtherInstanceStarted != null)
      {
        Type type = typeof(StartupEventArgs);
        ConstructorInfo constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
        StartupEventArgs e = (StartupEventArgs)constructor.Invoke(null);
        FieldInfo argsField = type.GetField("_args", BindingFlags.Instance | BindingFlags.NonPublic);
        Debug.Assert(argsField != null);
        argsField.SetValue(e, args);

        OtherInstanceStarted(null, e);
      }
    }
  }
}
6 голосов
/ 11 августа 2011

Похоже, есть действительно хороший способ справиться с этим:

Приложение для одного экземпляра WPF

Это предоставляет класс, который вы можете добавить, который управляет всеми мьютексами и сообщениями, чтобы упростить вашу реализацию до такой степени, что это просто тривиально.

6 голосов
/ 05 мая 2010

Так много ответов на такой, казалось бы, простой вопрос. Просто немного встряхнуть, вот мое решение этой проблемы.

Создание Mutex может быть проблематичным, потому что JIT-er видит, что вы используете его только для небольшой части вашего кода, и хочет пометить его как готовый для сборки мусора. Он очень хочет превзойти вас, думая, что вы не собираетесь использовать этот Mutex так долго. На самом деле вы хотите использовать этот Mutex до тех пор, пока ваше приложение работает. Лучший способ убедить сборщика мусора оставить вас Mutex в одиночестве - сказать, чтобы он оставался в живых, несмотря на разные поколения гаражной коллекции. Пример:

var m = new Mutex(...);
...
GC.KeepAlive(m);

Я поднял идею с этой страницы: http://www.ai.uga.edu/~mc/SingleInstance.html

5 голосов
/ 26 января 2009

Никогда не следует использовать именованный мьютекс для реализации приложения с одним экземпляром (или, по крайней мере, не для производственного кода). Вредоносный код может легко сделать DoS ( отказ в обслуживании ) вашей задницей ...

4 голосов
/ 30 ноября 2012

Посмотрите на следующий код. Это отличное и простое решение для предотвращения нескольких экземпляров приложения WPF.

private void Application_Startup(object sender, StartupEventArgs e)
{
    Process thisProc = Process.GetCurrentProcess();
    if (Process.GetProcessesByName(thisProc.ProcessName).Length > 1)
    {
        MessageBox.Show("Application running");
        Application.Current.Shutdown();
        return;
    }

    var wLogin = new LoginWindow();

    if (wLogin.ShowDialog() == true)
    {
        var wMain = new Main();
        wMain.WindowState = WindowState.Maximized;
        wMain.Show();
    }
    else
    {
        Application.Current.Shutdown();
    }
}
4 голосов
/ 04 апреля 2011

Я нашел более простое решение, похожее на Дейла Рагана, но немного измененное. Он делает практически все, что вам нужно, и на основе стандартного класса Microsoft WindowsFormsApplicationBase.

Во-первых, вы создаете класс SingleInstanceController, который вы можете использовать во всех других приложениях с одним экземпляром, которые используют Windows Forms:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.VisualBasic.ApplicationServices;


namespace SingleInstanceController_NET
{
    public class SingleInstanceController
    : WindowsFormsApplicationBase
    {
        public delegate Form CreateMainForm();
        public delegate void StartNextInstanceDelegate(Form mainWindow);
        CreateMainForm formCreation;
        StartNextInstanceDelegate onStartNextInstance;
        public SingleInstanceController(CreateMainForm formCreation, StartNextInstanceDelegate onStartNextInstance)
        {
            // Set whether the application is single instance
            this.formCreation = formCreation;
            this.onStartNextInstance = onStartNextInstance;
            this.IsSingleInstance = true;

            this.StartupNextInstance += new StartupNextInstanceEventHandler(this_StartupNextInstance);                      
        }

        void this_StartupNextInstance(object sender, StartupNextInstanceEventArgs e)
        {
            if (onStartNextInstance != null)
            {
                onStartNextInstance(this.MainForm); // This code will be executed when the user tries to start the running program again,
                                                    // for example, by clicking on the exe file.
            }                                       // This code can determine how to re-activate the existing main window of the running application.
        }

        protected override void OnCreateMainForm()
        {
            // Instantiate your main application form
            this.MainForm = formCreation();
        }

        public void Run()
        {
            string[] commandLine = new string[0];
            base.Run(commandLine);
        }
    }
}

Затем вы можете использовать его в своей программе следующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using SingleInstanceController_NET;

namespace SingleInstance
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static Form CreateForm()
        {
            return new Form1(); // Form1 is used for the main window.
        }

        static void OnStartNextInstance(Form mainWindow) // When the user tries to restart the application again,
                                                         // the main window is activated again.
        {
            mainWindow.WindowState = FormWindowState.Maximized;
        }
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);            
            SingleInstanceController controller = new SingleInstanceController(CreateForm, OnStartNextInstance);
            controller.Run();         
        }
    }
}

И программа, и решение SingleInstanceController_NET ​​должны ссылаться на Microsoft.VisualBasic. Если вы просто хотите повторно активировать запущенное приложение как обычное окно, когда пользователь пытается перезапустить запущенную программу, второй параметр в SingleInstanceController может быть нулевым. В данном примере окно развернуто.

4 голосов
/ 22 июня 2010

Вот что я использую. Он объединил перечисление процессов для выполнения переключения и мьютекс для защиты от «активных кликеров»:

public partial class App
{
    [DllImport("user32")]
    private static extern int OpenIcon(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        var p = Process
           .GetProcessesByName(Process.GetCurrentProcess().ProcessName);
            foreach (var t in p.Where(t => t.MainWindowHandle != IntPtr.Zero))
            {
                OpenIcon(t.MainWindowHandle);
                SetForegroundWindow(t.MainWindowHandle);
                Current.Shutdown();
                return;
            }

            // there is a chance the user tries to click on the icon repeatedly
            // and the process cannot be discovered yet
            bool createdNew;
            var mutex = new Mutex(true, "MyAwesomeApp", 
               out createdNew);  // must be a variable, though it is unused - 
            // we just need a bit of time until the process shows up
            if (!createdNew)
            {
                Current.Shutdown();
                return;
            }

            new Bootstrapper().Run();
        }
    }
2 голосов
/ 06 декабря 2013

Обновление 2017-01-25. Попробовав несколько вещей, я решил перейти на VisualBasic.dll, он проще и работает лучше (по крайней мере, для меня). Я оставил свой предыдущий ответ в качестве ссылки ...

Так же, как ссылка, это то, что я делал без передачи аргументов (что я не могу найти для этого никакой причины ... Я имею в виду одно приложение с аргументами, которые должны передаваться из одного экземпляра в другой) , Если требуется сопоставление файлов, то приложение (согласно стандартному ожиданию пользователя) должно быть создано для каждого документа. Если вам нужно передать аргументы в существующее приложение, я думаю, что я бы использовал vb dll.

Не передавая аргументы (только приложение с одним экземпляром), я предпочитаю не регистрировать новое сообщение Window и не перезаписывать цикл сообщений, как это определено в Matt Davis Solution. Хотя добавить VisualBasic dll не составляет особого труда, но я предпочитаю не добавлять новую ссылку только для создания приложения с одним экземпляром. Кроме того, я предпочитаю создавать новый класс с помощью Main вместо вызова Shutdown из переопределения App.Startup, чтобы обеспечить выход как можно скорее.

В надежде, что это кому-нибудь понравится ... или немного вдохновит :-)

Класс запуска проекта должен быть установлен как «SingleInstanceApp».

public class SingleInstanceApp
{
    [STAThread]
    public static void Main(string[] args)
    {
        Mutex _mutexSingleInstance = new Mutex(true, "MonitorMeSingleInstance");

        if (_mutexSingleInstance.WaitOne(TimeSpan.Zero, true))
        {
            try
            {
                var app = new App();
                app.InitializeComponent();
                app.Run();

            }
            finally
            {
                _mutexSingleInstance.ReleaseMutex();
                _mutexSingleInstance.Close();
            }
        }
        else
        {
            MessageBox.Show("One instance is already running.");

            var processes = Process.GetProcessesByName(Assembly.GetEntryAssembly().GetName().Name);
            {
                if (processes.Length > 1)
                {
                    foreach (var process in processes)
                    {
                        if (process.Id != Process.GetCurrentProcess().Id)
                        {
                            WindowHelper.SetForegroundWindow(process.MainWindowHandle);
                        }
                    }
                }
            }
        }
    }
}

WindowHelper:

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;

namespace HQ.Util.Unmanaged
{
    public class WindowHelper
    {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);
2 голосов
/ 30 декабря 2016

Не используя Mutex, простой ответ:

System.Diagnostics;    
...
string thisprocessname = Process.GetCurrentProcess().ProcessName;

if (Process.GetProcesses().Count(p => p.ProcessName == thisprocessname) > 1)
                return;

Поместите его внутрь Program.Main().
Пример :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;

namespace Sample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            //simple add Diagnostics namespace, and these 3 lines below 
            string thisprocessname = Process.GetCurrentProcess().ProcessName;
            if (Process.GetProcesses().Count(p => p.ProcessName == thisprocessname) > 1)
                return;

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Sample());
        }
    }
}

Вы можете добавить MessageBox.Show к if заявлению и поставить «Приложение уже запущено».
Это может быть полезно для кого-то.

1 голос
/ 29 ноября 2013

Вот легковесное решение, которое я использую, которое позволяет приложению вывести уже существующее окно на передний план, не прибегая к пользовательским сообщениям Windows или не просматривая имена процессов вслепую.

[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);

static readonly string guid = "<Application Guid>";

static void Main()
{
    Mutex mutex = null;
    if (!CreateMutex(out mutex))
        return;

    // Application startup code.

    Environment.SetEnvironmentVariable(guid, null, EnvironmentVariableTarget.User);
}

static bool CreateMutex(out Mutex mutex)
{
    bool createdNew = false;
    mutex = new Mutex(false, guid, out createdNew);

    if (createdNew)
    {
        Process process = Process.GetCurrentProcess();
        string value = process.Id.ToString();

        Environment.SetEnvironmentVariable(guid, value, EnvironmentVariableTarget.User);
    }
    else
    {
        string value = Environment.GetEnvironmentVariable(guid, EnvironmentVariableTarget.User);
        Process process = null;
        int processId = -1;

        if (int.TryParse(value, out processId))
            process = Process.GetProcessById(processId);

        if (process == null || !SetForegroundWindow(process.MainWindowHandle))
            MessageBox.Show("Unable to start application. An instance of this application is already running.");
    }

    return createdNew;
}

Редактировать: Вы также можете хранить и инициализировать мьютекс и статически созданные новые, но вам нужно будет явно удалить / освободить мьютекс, как только вы закончите с ним. Лично я предпочитаю сохранять мьютекс локальным, так как он будет автоматически удален, даже если приложение закроется, не достигнув конца Main.

...