使用 WCF 实现健壮优雅的单实例 WPF 应用程序方法





5.00/5 (5投票s)
如何利用 WCF 实现任何 WPF 应用程序的单个实例运行,而无需使用互斥体、额外的程序集或特殊的“技巧”。
引言
如今的设计范式正从过去可能使用多文档界面 (MDI) 方法的单个运行实例,转向允许同一应用程序的多个实例与单文档界面 (SDI) 并存。这在微软 (参见 Office 系列应用程序)、Adobe 等公司的产品中都能看到。然而,仍然有一些应用程序(例如大多数网页浏览器)希望或需要只有一个实例在运行(无论是否实际使用了 MDI)。
在围绕 WPF 构建的应用程序中,如果想要创建应用程序的单个实例,并没有内置的魔术般的功能。您必须自己编写检查和验证代码。有多种方法可以解决这个问题,每种方法都有其优缺点,但我所了解的所有解决方案都需要应用程序根据解决方案进行编码,而不是提供一个易于适应通用 WPF 应用程序的解决方案。
一个好的解决方案应该能够处理启动参数,可以跨多个项目重用,如果可能的话不应该需要任何额外的程序集(或者至少将额外的引用程序集限制在一个以内),可以轻松地转换为框架类进行分发,并且总的来说允许程序员遵循通用的 WPF 项目模板。您不应该需要编写新的入口点或对 App.xaml 进行更改,以免破坏其使用的传统范例。不需要覆盖任何标准方法(即不需要添加 OnStartup)。也就是说,理想情况下,您只需要添加一个类并进行一个方法调用,而无需额外的管理。
我曾为此类目的寻找解决方案,虽然有很多方法,但没有一种满足我简便地传递命令行参数(如果需要)、在第二个实例启动时有灵活性地执行更多操作,并且不需要任何特殊处理或编码(超出基本要求)的需求。经过一段时间的实验,我偶然发现了这种方法,它利用了 WCF 的一些特殊属性来实现这一目标。
背景
第一个实例将始终正常运行。问题的核心是如何处理任何后续启动的实例。显而易见的方法是让操作系统阻止任何后续的启动。当然,如果我想启动多个实例,那就不可能了。这一点是无关紧要的,因为 Windows 和 *nix 并不以这种方式限制应用程序。
这意味着我们必须以编程方式来实现。一种方法是使用 `Mutex`。在这种情况下,Mutex 将在应用程序启动时实例化,并在关闭时释放。任何进一步的实例都会尝试获取 Mutex 的锁,如果被拒绝,它就知道要关闭。
这种方法非常简单有效。问题是,如果我们需要的启动参数从第二个实例传递给第一个实例,这种方法并未提供机制。同样,如果第一个实例需要因为第二个实例被启动而执行某些操作,也没有可用的通知机制。
另一个选项(微软等推荐了一段时间)允许传递命令行参数,但它要求使用 Windows Forms,添加一个 Visual Basic 引用程序集,并创建一个新的应用程序入口点。这过于复杂,并且偏离了纯 WPF 的范例。此外,它使用了已弃用的 .NET Remoting,您永远不应该依赖已弃用的代码。
为了使解决方案成功,需要一个进程间通信通道,并在第二个实例启动时触发一个事件来通知第一个实例。下面的方法使用了 Windows Communication Foundation (WCF),特别是命名管道,以及可用的事件编码范例。这种方法的一个副作用是无需 Mutex。该方法还非常简洁,并为构建更复杂的特定应用程序交互提供了良好的基础。
使用代码
快速回顾目标
- 它应该能够使用 Visual Studio 提供的通用 WPF 模板,无需任何修改。
- 它应该能够普遍使用任何其他类型的 WPF 模板(只要它不是太“奇怪”)
- 除了 WPF 的基本要求外,不应需要任何额外的程序集,或者尽可能少。
- 应用程序一次应只有一个实例在运行
- 它应该能够从第二个实例将启动参数传递给第一个实例
- 如果需要或希望,当启动第二个实例时,第一个实例应能够执行特殊代码。
- 如果需要,它应该能够将第一个实例的窗口带到前台
- 它应该遵循一般的编码指南
- 应该能在广泛的 .NET 版本上运行
该解决方案旨在重用,因此我们将创建一个单独的类,该类可以编译成 .dll 库或直接添加到项目中。我们从定义类开始。为此,我们将创建一个名为 SingleInstanceManager 的 .cs 文件,命名空间也相同。
using System;
using System.ServiceModel;
using System.Windows;
namespace SingleInstanceManager
{
}
我们将通过思考其逻辑处理组件来开始构建此类。实现单一实例的流程如下:
- 应用程序启动
- 应用程序检查是否还有其他实例正在运行
- 如果是第一个实例
- 确立自己为第一个实例
- 设置服务器通信通道
- run
- 等待第二个实例通信
- 运行任何特殊指令
- 如果是第二个实例
- 设置客户端通信通道
- 发出第二个实例存在的信号
- 发送任何启动参数
- 关闭
那么,如何进行设计呢?设计的核心是实例检查。大多数开发人员首先想到的可能是实现一个进程间的 `Mutex`。如果您能获取到 Mutex 的锁,那么您就是唯一运行的实例。如果您获取不到 Mutex,那么您就是第二个实例。
如果我们不需要启动参数或特殊执行,那么可以使用 Mutex 就可以了。我们想要更健壮的方法,允许通过进程间通信通道传递启动参数。在深入实例检查之前,让我们先研究一下这种通信通道的性质,以便我们了解在检查之后如何继续设置通道。
过去,进程间通信有 .NET Remoting,但它已经被 Windows Communication Foundation (WCF) 取代了很长时间。WCF 提供了多种创建进程间通信的途径和方法。我们将假设应用程序将在同一台本地桌面计算机上运行。此外,我们不需要复杂的机制,因为我们只是发送一个字符串参数。WCF 传输的命名管道就是为此类场景设计的,最终我们将得到非常类似于远程过程调用的东西。(注意:此示例可以扩展到使用其他传输方式,以便您可以设计一种限制单个实例仅限于桌面,而不是跨网络的方式。)
代码本质上将公开一个面向外部世界的方法,或者更字面地说,面向 Windows 桌面上的所有进程。然后,WCF 可以创建一个从另一个进程(在本场景中是我们的应用程序的第二个实例,称为客户端)到第一个实例(称为服务器)的隧道,特别是到第一个实例的那个类方法。我们将通过以下步骤建立这种通信。
首先,定义一个 `Interface`。我们使用 `[ServiceContract]` 属性来指示此接口要暴露给客户端。`[OperationContract]` 属性指示此方法可供客户端使用。第二个实例将调用服务方法 `PassStartupArgs`,变量 `args` 将包含第二个实例的命令行或启动参数。
///<summary> The WCF interface for passing the startup parameters </summary>
[ServiceContract]
public interface ISingleInstance
{
/// <summary> Notifies the first instance that another instance of the application attempted to start. </summary>
/// <param name="args">The other instance's command-line arguments.</param>
[OperationContract<]
void PassStartupArgs(string[] args);
}
现在来创建实现我们的服务并管理单实例方法的类。
首先,我们定义类 `SingleInstance`,并指示它将使用 `ISingleInstance` 接口。接下来,我们定义接口方法 `PassStartupArgs`。一旦 WCF 通道打开,就可以与此方法进行交互。现在,我们继续专注于 WCF 的底层机制,暂时将 `PassStartupArgs` 方法留空。
/// <summary>
/// A class to use for single-instance applications.
/// </summary>
public class SingleInstance : ISingleInstance
{
public void PassStartupArgs(string[] args)
{
}
}
此下一个方法的目的是实例化或创建 WCF 服务器。我们将此方法称为 `OpenServiceHost`。它不需要在类外部可访问,因此可以是 `private`。我们需要一个返回指示器来告知 WCF 服务器是否已成功启动并正在监听,因此我们将使用 `bool` 返回类型。如果一切成功,则返回 `true`,如果出现问题,则返回 `false`。目前我们的方法如下所示:
/// <summary>
/// Attempts to create the named pipe service.
/// </summary>
/// <returns>true, if the service was published successfully.</returns>
private bool OpenServiceHost()
{
}
我们的通道通过 `ServiceHost` 类提供。`ServiceHost` 对象将监听来自第二个实例的传入通信。为了初始化我们的 `ServiceHost` 对象,我们需要为其构造函数提供要使用的服务类型(在这种情况下是我们的 `SingleInstance` 类类型)以及我们的服务地址的 URI。本地命名管道的 URI 简单地是 net.pipe:://。
请注意:WCF 提供三种主要传输方式:HTTP、TCP 和命名管道。HTTP 面向基于 Web 和非 WCF 的旧通信。TCP 面向不同计算机之间的通信。根据微软的说法:“命名管道是 Windows 操作系统内核中的一个对象,例如进程可以用来通信的共享内存段。命名管道有一个名称,可用于在单台计算机上的进程之间进行单向或双向通信。”命名管道面向单台计算机上的应用程序之间的通信。如上所述,我们将在服务中使用命名管道。
因此,我们的初始化如下:
ServiceHost NamedPipeHost = new ServiceHost(typeof(SingleInstance), new Uri("net.pipe://"));
在初始化 `NamedPipeHost` 后,我们需要创建服务终结点。终结点代表通信的“隧道”。终结点启用服务的特定传输绑定,并提供客户端发送通信的地址。这通过 `ServiceHost` 类的 `AddServiceEndpoint` 方法完成。我们需要提供服务合同(这里是我们的接口)、绑定类型(在此情况下是 `NetNamedPipeBinding`)以及一个将附加到我们基 URI “net.pipe://” 的地址,现在我们只使用字符串“startupargs”。(注意:NetNamedPipe 绑定提供传输安全级别。根据您对此特定启动参数消息的开销和安全需求的看法,您可以在 `NetNamedPipeBinding` 构造函数中将 `NetNamedPipeSecurityMode.Node` 作为字段传递,以禁用默认启用的传输安全。)
NamedPipeHost.AddServiceEndpoint(typeof(ISingleInstance), new NetNamedPipeBinding(), "startupargs");
添加终结点后,我们只需打开通道并通过调用 `NetNamedPipeBinding` 上的 `Open` 方法来启动服务监听。
NamedPipeHost.Open();
这就是我们的命名管道服务器的全部内容,此时代码执行已准备好允许从另一个进程调用 `PassStartupArgs` 方法。在继续之前,让我们快速回顾一下服务器的一些错误检查,这些检查恰好对我们的设计有很大影响。
您只能有一个服务监听特定的地址,否则在调用 `Open` 方法时会抛出 `System.ServiceModel.AddressAlreadyInUseException` 异常。目前我们的地址是“net.pipe:///startupargs”。如果此代码在不同的应用程序中被重用,则在启动该应用程序时会抛出异常。我们需要为使用此代码的每个应用程序提供一个唯一的地址。
通过创建应用程序每次启动时使用的唯一地址,我们可以利用 `AddressAlreadyInUseException` 作为 Mutex 的替代方案来进行实例检查。我们不再需要管理 Mutex,并且可以简化设计和代码的复杂性。
为了实现这一切,我们修改方法以接受一个 `string`(我们将很快设计接口),该字符串代表我们的唯一地址(可以认为是应用程序的唯一 ID),并将命名管道创建包装在一个 try/catch 块中。如果我们可以打开服务(因此是第一个实例),则返回 `true` 表示成功,或者如果捕获到异常(因此是第二个实例),则返回 `false`。
我们的代码现在如下所示:
/// <summary>
/// Attempts to create the named pipe service.
/// </summary>
/// <param name="uid">the application's unique identifier used to create a unique endpoint address.</param>
/// <returns>true, if the service was published successfully.</returns>
private bool OpenServiceHost(string uid)
{
try
{
//setup the WCF service using NamedPipe
ServiceHost NamedPipeHost = new ServiceHost(typeof(SingleInstance), new Uri("net.pipe://"));
NamedPipeHost.AddServiceEndpoint(typeof(ISingleInstance), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), uid);
//if the service is already open (i.e. another instance is already running) the following will cause an exception
NamedPipeHost.Open();
return true; //success
}
catch (AddressAlreadyInUseException)
{
//failed to open the service so must be a second instance
return false;
}
}
继续我们的错误检查回顾,您可能会想到,没有提供关闭和清理 `ServiceHost` 对象的方法。该对象的一个特殊方面可以排除这种需要。该对象将在应用程序线程上构造,并且由于我们使用的是 WPF,UI 线程将使应用程序保持运行,从而也使 `ServiceHost` 对象保持运行,直到应用程序结束,除非它被明确关闭。当应用程序关闭时,它将被处置并可供垃圾回收。也就是说,我们不需要对 `NamedPipeHost` 进行任何“清理”;它会自行管理,我们只需要打开通道。无需跟踪或担心我们的 `ServiceHost` 对象,这使其非常适合用作这种“附加”类型的类。
话虽如此,仍然存在非常小的竞态条件的可能性。当应用程序关闭时,消息可能在通道被处置之前继续流入,当然,这些消息将不会被处理,导致客户端连接暂时挂起。存在一个小的风险,即第二个实例可能会进入某种未知状态。如果我们希望完全规避风险,可以显式地在应用程序关闭时关闭主机。
为了执行此操作,我们挂钩 `Exit` 事件并添加一个新的事件处理程序。我们还需要将 `NamedPipeHost` 设为类变量,以便可以在事件方法的范围内访问它。我们将以下内容添加到 `OpenServiceHost` 方法中:
Application.Current.Exit += new ExitEventHandler(OnAppExit);
我们创建 `OnAppExit` 方法:
private static void OnAppExit(object sender, EventArgs e)
{
if (NamedPipeHost != null)
{
NamedPipeHost.Close();
NamedPipeHost = null;
}
}
还有两个错误需要注意。一是如果我们的监听器进入“故障”状态或出现未处理的异常,并且我们没有优雅地关闭应用程序。这两种情况都可能导致服务处于未知状态,并且任何将来的启动都无法进行,因为它无法打开服务通道。
如果服务进入 `Faulted` 状态,它将无法处理任何新的客户端连接。但是,当第二个实例尝试打开服务时,它将收到 `CommunicationObjectFaultedException`。我认为捕获我们计划处理的异常比使用通配符 `Exception` 更好,它允许开发人员决定如何处理其他类型的异常。在这种情况下,我们再添加一个 catch 块并处置服务。
由于 `NamedPipeHost` 也是一个类变量,我们应该在 catch 语句中妥善处置它。对于这些特定的异常,`NamedPipeHost` 将处于“故障”状态,如果我们调用 `Close` 方法,将抛出异常。相反,我们调用 `Abort` 然后将变量设置为 null。
对于未处理的异常,我们要确保 `NamedPipeHost` 被妥善处置。因此,我们挂钩 `AppDomain.CurrentDomain.UnhandledException` 来捕获未处理的异常并处置主机(首先检查它是否处于故障状态)。
我们的代码现在如下所示:
public class SingleInstance: ISingleInstance
{
private ServiceHost NamedPipeHost = null;
public void PassStartupArgs(string[] args) { }
/// <summary>
/// Attempts to create the named pipe service.
/// </summary>
/// <param name="uid">the application's unique identifier used to create a unique endpoint address.</param>
/// <returns>true, if the service was published successfully.</returns>
private bool OpenServiceHost(string uid)
{
try
{
//hook the application exit event to avoid a race condition when messages flow while the application is disposing of the channel during shutdown
Application.Current.Exit += new ExitEventHandler(OnAppExit);
//setup the WCF service using NamedPipe
NamedPipeHost = new ServiceHost(typeof(SingleInstance), new Uri("net.pipe://"));
NamedPipeHost.AddServiceEndpoint(typeof(ISingleInstance), new NetNamedPipeBinding(), uid);
//for any unhandled exception we need to ensure NamedPipeHost is disposed
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException);
//if the service is already open (i.e. another instance is already running) the following will cause an exception
NamedPipeHost.Open();
//success and we are first instance
return true;
}
catch (AddressAlreadyInUseException)
{
//failed to open the service so must be a second instance
NamedPipeHost.Abort();
NamedPipeHost = null;
return false;
}
catch (CommunicationObjectFaultedException)
{
//failed to open the service so must be a second instance
NamedPipeHost.Abort();
NamedPipeHost = null;
return false;
}
}
/// <summary>
/// Ensures that the named pipe service host is closed on the application exit
/// </summary>
private void OnAppExit(object sender, EventArgs e)
{
if (NamedPipeHost != null)
{
NamedPipeHost.Close();
NamedPipeHost = null;
}
}
/// <summary>
/// ensure host is disposed of if there is an unhandled exception
/// </summary>
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (NamedPipeHost != null)
{
if (NamedPipeHost.State == CommunicationState.Faulted)
NamedPipeHost.Abort();
else
NamedPipeHost.Close();
NamedPipeHost = null;
}
}
}
此代码完成了我们的 WCF 服务实例化。它还提供了一种为哪个实例提供答案的方式,我们可以利用这一点并实现我们的检查方法。
遵循我们的流程,在应用程序启动后,它需要执行实例检查。我们的方法将尝试打开 WCF 服务。如果成功,它可以告诉应用程序它是第一个实例并继续。如果失败,我们就是第二个实例,需要继续通知第一个实例。这是一个简单的实现,不需要让开发人员实例化我们的 SingleInstanceManager 类。遵循此设计原则,我们将此方法设为静态,这需要我们将其他方法(目前除了 PassStartupArgs 之外的所有方法)更新为静态。
我们将该方法指定为 `IsFirstInstance`。它需要一个 `bool` 返回类型,以便向应用程序指示它是第一个实例还是第二个实例(如果是第一个实例则为 `true`,否则为 `false`)。请记住,我们的服务主机需要一个唯一 ID,因此我们将有一个 `string uid` 参数供应用程序传递命名管道地址的唯一 ID。
public static bool IsFirstInstance(string uid) { }
为了检查我们是否是第一个实例,我们的方法需要调用 `OpenServiceHost` 并测试返回代码。如果 `OpenServiceHost` 返回 true,那么这是第一个实例,此方法可以返回 `true`。如果返回 false,我们需要通知第一个实例,传递启动参数,然后关闭此第二个实例。
为了实现这一点,我们需要另一个方法来建立 WCF 客户端并使用我们的接口 `PassStartupArgs` 传递启动参数。此方法可以是私有的,并且不需要返回类型。它将需要唯一 ID,以便客户端可以在正确的地址上连接。
private static void NotifyFirstInstance(string uid) { }
为了在客户端和服务器之间建立连接,需要设置一个通道或隧道。WCF 提供 `ChannelFactory
using (ChannelFactory<ISingleInstance> factory = new ChannelFactory<ISingleInstance>(new NetNamedPipeBinding(), new EndpointAddress("net.pipe:///" + uid))) { }
我们现在可以调用工厂上的 `CreateChannel`,它将返回连接到第一个实例的接口。我们只需调用返回接口上的 `PassStartupArgs`,我们就与第一个实例进行了通信。完整的方法如下所示:
private static void NotifyFirstInstance(string uid)
{
//create channel with first instance interface
using (ChannelFactory<ISingleInstance> factory = new ChannelFactory<ISingleInstance>(new NetNamedPipeBinding(), new EndpointAddress("net.pipe:///" + uid)))
{
ISingleInstance singleInstanceInterface = factory.CreateChannel();
//pass the startup args to the first instance
singleInstanceInteface.PassStartupArgs(Environment.GetCommandLineArgs());
}
}
既然我们正在与我们的接口通信,让我们完成那个方法。我们的接口需要处理启动参数或当第二个实例启动时。您可以直接在这里插入所需代码。如果您重用代码,您只需知道填写此方法即可满足任何需求。另一种方法是将接口附加到 WPF App 类,并将代码移至引用 App.PassStartupArgs。这更困难,并且破坏了由单独的类处理所有这些问题的范例。这里我们将研究的方法是注册一个将在 `PassStartupArgs` 中引发的事件。这提供了额外的灵活性,因为对第二个实例的反应可以随时间改变。
为了实现事件,我们需要在我们的 SingleInstanceManager 命名空间内创建一个单独的类。事件只需要处理启动参数,即 `string[] args`。这是事件:
/// <summary>
/// Event declaration for the startup of a second instance
/// </summary>
public class SecondInstanceStartedEventArgs : EventArgs
{
/// <summary>
/// The event method declartion for the startup of a second instance
/// </summary>
/// <param name="args">The other instance's command-line arguments.</param>
public SecondInstanceStartedEventArgs(string[] args)
{ Args = args; }
/// <summary>
/// Property containing the second instance's command-line arguments
/// </summary>
public string[] Args { get; set; }
}
现在我们在 `SingleInstance` 类中添加一个 EventHandler:
/// <summary>
/// Is raised when another instance attempts to start up.
/// </summary>
public static event EventHandler<SecondInstanceStartedEventArgs> OnSecondInstanceStarted;
最后回到 `PassStartupArgs`,我们调用事件:
/// <summary>
/// Interface: Notifies the first instance tht another instance of the application attempted to start.
/// </summary>
/// <param name="args">The other instance's command-line arguments.</param>
public void PassStartupArgs(string[] args)
{
//check if an event is registered for when a second instance is started and if so raise the event
OnSecondInstanceStarted?.Invoke(this, new SecondInstanceStartedEventArgs(args));
}
现在 `IsFirstInstance` 方法可以调用 `NotifyFirstInstance`,一切都应该正常工作。
在查看如何在应用程序中实现此解决方案之前的最后一个考虑因素。可以在类中提供一种将第一个实例窗口带到前台的方法。这可能并不总是想要的,所以我们可以向 `IsFirstInstance` 参数列表添加一个标志,以提示需要将第一个实例窗口带到前台。如果标志为 true,我们可以通过挂钩我们刚刚实现的事件来完成此操作。代码如下所示:
/// <summary>
/// Checks to see if this instance is the first instance of this application on the local machine. If it is not, this method will
/// send the first instance this instance's command-line arguments.
/// </summary>
/// <param name="uid">The application's unique identifier.</param>
/// <param name="activatewindow">Should the main window become active on a second instance launch</param>
/// <returns>True if this instance is the first instance.</returns>
public static bool IsFirstInstance(string uid, bool activatewindow)
{
//attempt to open the service, should succeed if this is the first instance
if (OpenServiceHost(uid))
{
if (activatewindow)
OnSecondInstanceStarted += ActivateMainWindow;
return true;
}
//notify the first instance and pass the command-line args
NotifyFirstInstance(uid);
//ok to shutdown second instance
Application.Current.Shutdown();
return false;
}
/// <summary>
/// Activate the first instance's main window
/// </summary>
private static void ActivateMainWindow(object sender, SecondInstanceStartedEventArgs e)
{
//activate first instance window
Application.Current.Dispatcher.Invoke(new Action(() =>
{
//check if window state is minimized then restore
if (Application.Current.MainWindow.WindowState == WindowState.Minimized)
Application.Current.MainWindow.WindowState = WindowState.Normal; //despite saying Normal this actually will restore
Application.Current.MainWindow.Activate(); //now activate window
}));
}
快速概述 `ActivateMainWindow`。由于我们正在与 UI 交互,我们必须使用 `Dispatcher` 并将命令调度到 UI 线程。我们希望将应用程序窗口带到前台,但窗口可能被最小化。如果是这样,我们希望将其恢复到最小化时的状态。正确的方法是检查窗口状态,如果它是 `Minimized`,则将其状态设置为 `Normal`,这将恢复到之前的状态(而不是默认或恢复状态)。然后我们调用 `Activate` 方法。
这完成了 `SingleInstanceManager`。现在让我们在应用程序中实现所有这些。第二个实例需要在启动后、WPF UI 开始运行之前执行其实例检查。这意味着它不能插入到 `MainWindow.xaml.cs` 中。然而,WPF 与 .NET 的其他方面不同,它隐藏了 `Main()` 方法。这是自动生成的,并出现在 `App.g.cs` 中。这段生成的代码看起来与此类似:
/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
public static void Main() {
AnApplication.App app = new AnApplication.App();
app.InitializeComponent();
app.Run();
您可以覆盖此 `Main()` 方法,但目标是使用典型的 WPF 模板。因此,考虑到这一点,查看这段生成的代码,我们可以看到我们可以最早获得控制权来执行检查的地方是在 `App` 构造函数中。此构造函数位于 `App.xaml.cs` 文件中。我们添加构造函数 `public App()` 并调用我们的方法 `IsFirstInstance`。我们传递一个唯一的字符串(下面的示例使用生成的 GUID)以及 `true` 或 `false`,具体取决于我们是否希望在启动第二个实例时将第一个实例窗口带到前台(示例使用 true)。
由于我们要处理启动参数,如果 `IsFirstInstance` 返回 true,我们会注册一个事件处理程序。我们可以在此处添加任何其他所需的构造函数代码。这是插入显示启动画面代码的好地方。启动画面将在与主应用程序不同的线程上执行,而第二个实例关闭可能会导致问题。此外,第二个实例启动时可能不希望显示启动画面。
我们与目标唯一的偏差是,我们必须将 `System.ServiceModel`(它处理 WCF)程序集添加到项目的引用中。在大多数情况下,这应该是可以接受的。
我们的应用程序代码将类似于以下内容:
public partial class App : Application
{
public App()
{
//check if we are the first instance
if (SingleInstanceManager.SingleInstance.IsFirstInstance("C69674D6-4A98-417B-ADC0-5919F56AE8FE", true))
{
//we are, register our event handler for receiving the new arguments
SingleInstanceManager.SingleInstance.OnSecondInstanceStarted += NewStartupArgs;
//place additional startup code here
SplashScreen splashScreen = new SplashScreen("SplashScreen.jpg");
splashScreen.Show(true);
}
//we are secondary instance and shutdown will happen automatically
}
private void NewStartupArgs(object sender, SingleInstanceManager.SecondInstanceStartedEventArgs e)
{
//handle new startup arguments and/or do anything else for second instance launch
}
}
现在您拥有一个可以“仅启动”一个实例、处理新的启动参数,并(如果需要)将第一个实例窗口带到前台的应用程序。我们仅用一个方法调用就完成了所有这些,代码量不到 200 行。可以进行更多自定义或增强此解决方案,但它是一个通用的、适用于多个项目的优秀解决方案。
SingleInstanceManager 命名空间的完整代码如下:
using System;
using System.ServiceModel;
using System.Windows;
namespace SingleInstanceManager
{
/// <summary> The WCF interface for passing the startup parameters </summary>
[ServiceContract]
public interface ISingleInstance
{
/// <summary>
/// Notifies the first instance that another instance of the application attempted to start.
/// </summary>
/// <param name="args">The other instance's command-line arguments.</param>
[OperationContract]
void PassStartupArgs(string[] args);
}
/// <summary>
/// Event declaration for the startup of a second instance
/// </summary>
public class SecondInstanceStartedEventArgs : EventArgs
{
/// <summary>
/// The event method declaration for the startup of a second instance
/// </summary>
/// <param name="args">The other instance's command-line arguments.</param>
public SecondInstanceStartedEventArgs(string[] args)
{ Args = args; }
/// <summary>
/// Property containing the second instance's command-line arguments
/// </summary>
public string[] Args { get; set; }
}
/// <summary>
/// A class to allow for a single-instance of an application.
/// </summary>
public class SingleInstance : ISingleInstance
{
/// <summary>
/// Is raised when another instance attempts to start up.
/// </summary>
public static event EventHandler<SecondInstanceStartedEventArgs> OnSecondInstanceStarted;
private static ServiceHost NamedPipeHost = null;
/// <summary>
/// Interface: Notifies the first instance that another instance of the application attempted to start.
/// </summary>
/// <param name="args">The other instance's command-line arguments.</param>
public void PassStartupArgs(string[] args)
{
//check if an event is registered for when a second instance is started
OnSecondInstanceStarted?.Invoke(this, new SecondInstanceStartedEventArgs(args));
}
/// <summary>
/// Checks to see if this instance is the first instance of this application on the local machine. If it is not, this method will
/// send the first instance this instance's command-line arguments.
/// </summary>
/// <param name="uid">The application's unique identifier.</param>
/// <param name="activatewindow">Should the main window become active on a second instance launch</param>
/// <returns>True if this instance is the first instance.</returns>
public static bool IsFirstInstance(string uid, bool activatewindow)
{
//attempt to open the service, should succeed if this is the first instance
if (OpenServiceHost(uid))
{
if (activatewindow)
OnSecondInstanceStarted += ActivateMainWindow;
return true;
}
//notify the first instance and pass the command-line args
NotifyFirstInstance(uid);
//ok to shutdown second instance
Application.Current.Shutdown();
return false;
}
/// <summary>
/// Attempts to create the named pipe service.
/// </summary>
/// <param name="uid">The application's unique identifier.</param>
/// <returns>True if the service was published successfully.</returns>
private static bool OpenServiceHost(string uid)
{
try
{
//hook the application exit event to avoid race condition when messages flow while the application is disposing of the channel during shutdown
Application.Current.Exit += new ExitEventHandler(OnAppExit);
//setup the WCF service using a NamedPipe
NamedPipeHost = new ServiceHost(typeof(SingleInstance), new Uri("net.pipe://"));
NamedPipeHost.AddServiceEndpoint(typeof(ISingleInstance), new NetNamedPipeBinding(), uid);
//for any unhandled exception we need to ensure NamedPipeHost is disposed
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException);
//if the service is already open (i.e. another instance is already running) this will cause an exception
NamedPipeHost.Open();
//success and we are first instance
return true;
}
catch (AddressAlreadyInUseException)
{
//failed to open the service so must be a second instance
NamedPipeHost.Abort();
NamedPipeHost = null;
return false;
}
catch (CommunicationObjectFaultedException)
{
//failed to open the service so must be a second instance
NamedPipeHost.Abort();
NamedPipeHost = null;
return false;
}
}
/// <summary>
/// Ensures that the named pipe service host is closed on the application exit
/// </summary>
private static void OnAppExit(object sender, EventArgs e)
{
if (NamedPipeHost != null)
{
NamedPipeHost.Close();
NamedPipeHost = null;
}
}
/// <summary>
/// ensure host is disposed of if there is an unhandled exception
/// </summary>
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (NamedPipeHost != null)
{
if (NamedPipeHost.State == CommunicationState.Faulted)
NamedPipeHost.Abort();
else
NamedPipeHost.Close();
NamedPipeHost = null;
}
}
/// <summary>
/// Notifies the main instance that this instance is attempting to start up.
/// </summary>
/// <param name="uid">The application's unique identifier.</param>
private static void NotifyFirstInstance(string uid)
{
//create channel with first instance interface
using (ChannelFactory<ISingleInstance> factory = new ChannelFactory<ISingleInstance>(
new NetNamedPipeBinding(), new EndpointAddress("net.pipe:///" + uid)))
{
ISingleInstance singleInstanceInterface = factory.CreateChannel();
//pass the command-line args to the first interface
singleInstanceInterface.PassStartupArgs(Environment.GetCommandLineArgs());
}
}
/// <summary>
/// Activate the first instance's main window
/// </summary>
private static void ActivateMainWindow(object sender, SecondInstanceStartedEventArgs e)
{
//activate first instance window
Application.Current.Dispatcher.Invoke(new Action(() =>
{
//check if window state is minimized then restore
if (Application.Current.MainWindow.WindowState == WindowState.Minimized)
Application.Current.MainWindow.WindowState = WindowState.Normal; //despite saying Normal this actually will restore
Application.Current.MainWindow.Activate(); //now activate window
}));
}
}
}
历史
首次发布