65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 应用程序包装器( 支持单一实例)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (3投票s)

2009 年 12 月 18 日

CPOL

5分钟阅读

viewsIcon

32143

downloadIcon

387

一个应用程序类包装器,为 WPF 程序提供常见功能和单一实例支持。

引言

本文描述了一个类,该类可用于减少编写标准 WPF 桌面应用程序所需的冗余代码量。该类增加了单一实例支持、更轻松的未处理异常处理,并可以处理丢失的程序集。该类是 App.xaml 文件的扩展,因此仍然可以使用 Visual Studio 的 XAML 编辑器进行编辑。

这篇文章也在我的 网站 上有描述。

背景

对于我开发的应用类型,我通常更倾向于将我的程序开发成单一实例应用程序。本质上,这意味着确保在任何给定时间只有一个应用程序实例正在运行,并且所有后续的应用程序打开请求都由内存中已有的单一实例处理。

我还使用许多需要外部 DLL 的第三方库。虽然像 ILMerge 这样的工具通过将所有文件合并到一个文件中来消除对这些必需外部文件的依赖,这非常方便,但由于某些许可限制和其他因素,并非总是可行。然而,依赖其他外部库文件会在文件丢失或无效时引发其他问题。这是我必须在我编写的大多数应用程序中解决的另一个问题。

尽管单一实例应用程序支持和处理丢失的第三方库并非难解的问题,但我发现自己一遍又一遍地编写它。因此,我着手创建一个通用的 WPF 应用程序包装器类,以帮助减少我编写的冗余代码量。

WPF 的当前版本没有内置的单一实例应用程序支持,但有许多不同的方法可以解决这个问题。例如,可以使用 GetProcessesByName,使用 Microsoft.VisualBasic 命名空间,或使用 mutexes

对于我的应用程序类,我选择使用 Microsoft.VisualBasic 方法,如《Pro WPF in C# 2008》一书中所述。这种解决方案似乎是最可靠且最容易以通用方式实现的方法。

.NET 应用程序通过使用 AppDomain.CurrentDomain.AssemblyResolve 事件已经支持丢失的程序集问题,因此只需注册该事件并为新的 Application 类中的函数提供一个包装器即可。

在 WPF 应用程序中,App.xaml 文件提供了一种控制程序设置的便捷方式。

<Application x:Class="WpfApplicationSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
        <Application.Resources>

        </Application.Resources>
</Application>

由于我希望将这个新的 Application 类轻松地添加到任何 WPF 项目中,因此要求新类与 XAML 编辑器兼容。我希望最终结果看起来像这样:

<local:Application x:Class=" MyNamespace.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyNamespace"
    StartupUri="Window1.xaml">
    <local:WpfApplication.Resources>

    </local:WpfApplication.Resources>
</local:WpfApplication>

为了实现这一功能,我决定创建一个 System.Windows.Application 类的包装器,它具有完全相同的功能,以及额外的功能。我将这个类命名为 WpfApplication。我创建了一个派生自 System.Windows.DependencyObject 的空类,并在内存中保存一个 System.Windows.Application 实例,以便执行其所有必需的功能。这个 System.Windows.Application 实例在应用程序的整个生命周期内都存在。我为 System.Windows.Application 实例的每个事件附加了一个事件处理程序,以便引发 WpfApplication 的事件,并在某些事件(如异常处理)时执行新的自定义功能。

WpfApplicationRun 方法是实际逻辑开始的地方。我创建了另一个应用程序包装器,它派生自支持单一实例的 Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase 对象。该对象在每次调用 Run 方法时创建,然后调用 System.Windows.ApplicationRun 方法。

/// <summary>
/// Starts the single instance application wrapper
/// </summary>
/// <param name="window">The Window which is to be started. This can be null</param>
/// <returns>The return code returned from the WPF Application instance</returns>
private int Start(Window window)
{
    var wrapper = new 
        SingleInstanceApplicationWrapper(this, IsSingleInstance) {Window = window};

    // Although an empty array of string can be given
    // to the application wrapper, if done so then subsequent
    // instances will not be provided command line arguments in the
    // Microsoft.VisualBasic.ApplicationServices.StartupEventArgs argument
    wrapper.Run(Environment.GetCommandLineArgs());
    return wrapper.ReturnCode;
}

一旦 System.Windows.Application 终止,Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase 包装器也会终止。Microsoft.VisualBasic.ApplicationServices. WindowsFormsApplicationBase.OnStartupNextInstance 方法在 SingleInstanceApplicationWrapper 类中被重写,以便它可以调用 WpfApplication 实例的新事件。

/// <summary>
/// Calls the appropriate method of the WPF application
/// when the next instance of the application is
/// started
/// </summary>
/// <param name="eventArgs">The arguments that describe this event</param>
protected override void OnStartupNextInstance(
     Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs eventArgs)
{
    // Convert the ReadOnlyCollection<string>
    // arguments to a string[] collection of arguments and remove
    // the first argument which will always be the application path
    ReadOnlyCollection<string> argsCollection = eventArgs.CommandLine;
    int count = argsCollection.Count - 1;
    string[] args;

    if (count > 0)
    {
        args = new string[eventArgs.CommandLine.Count - 1];
        for (int i = 0; i < count; ++i)
        {
            args[i] = argsCollection[i + 1];
        }
    }
    else
    {
        args = new string[0];
    }

    _app.OnStartupNextInstance(new StartupNextInstanceEventArgs(args));
}

最终的类看起来像这样:

Class diagram

使用代码

为了使用新类,我们只需将 WpfApplication 类复制到我们的项目中,并将 App.xaml 修改为使用新类而不是默认类。然后可以使用 Visual Studio 的 XAML 编辑器照常设置选项。

<local:WpfApplication x:Class="WpfApplicationSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplicationSample"
      StartupUri="Window1.xaml" IsSingleInstance="True" 
      HandleUnhandledException="True" 
      HandleUnresolvedAssembly="True">
    <local:WpfApplication.Resources>

    </local:WpfApplication.Resources>
</local:WpfApplication>

还需要将 Microsoft.VisualBasic 引用添加到项目中。

Add reference

为了完成这个过程,需要将 App.xaml 代码修改为派生自 WpfApplication。所以,从以下代码更改:

// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}

改为这样。

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
}

由于 App.xaml 是一个部分类,其基类型在 XAML 文件中声明,因此不需要在 C# 文件中再次声明(尽管这样做也是完全可以的)。

就是这样。只需将新文件复制到您的项目中,更改 App.xaml 文件,您就可以轻松管理一个单一实例应用程序,并且对运行时错误有更好的支持。有关更多信息,请参阅演示项目。

已知错误

Visual Studio 2008 中存在一个 bug,编辑器会显示类似“无法创建 'WpfApplication' 类型的实例”的错误。我一直无法确定这个 bug 的原因,并得出结论,这是一个 Visual Studio IDE 的 bug,而不是代码本身的 bug。这个错误可以被安全地忽略,而无需担心(它不是编译时或运行时错误,只是设计时错误)。如果有人知道解决此问题的方法或原因,请告知我。

历史

  • 版本 1.0 (2009/12/18) - 添加到 CodeProject。
© . All rights reserved.