WPF 项目架构解析

154 阅读14分钟

本文是对一个 WPF 项目的入口文件 App.xaml 内容的学习笔记。App.xaml 作为入口文件,主要学习其 OnStartup 中执行了什么内容,以及如何扩展 Application 的实例方法,同样还有异常捕捉及一些常用组件的使用。

1. packages.config 文件的作用

在WPF(Windows Presentation Foundation)项目中,packages.config 文件(XML 格式)主要扮演着管理NuGet包依赖的角色。以下是其作用的具体说明:

一、记录NuGet包依赖

packages.config 文件详细记录了项目中使用的所有NuGet包的名称、版本号以及包的其他相关信息。这使得项目在迁移、构建或部署时,能够准确地获取并还原所需的NuGet包依赖。

二、确保项目一致性

通过packages.config 文件,团队成员可以确保项目在不同开发环境中的一致性。当团队成员从源代码管理系统中检出项目时,他们可以根据packages.config 文件中的信息,使用NuGet包管理器恢复所需的NuGet包,从而保证项目在不同开发环境中的构建和运行一致性。

三、支持包恢复和更新

NuGet包管理器使用packages.config 文件来恢复项目中缺失的NuGet包。此外,当项目中的NuGet包需要更新时,NuGet包管理器也会参考packages.config 文件中的信息来执行更新操作。这使得NuGet包的管理变得更加便捷和高效。

四、与解决方案级别和全局级别的NuGet包管理区分

值得注意的是,packages.config 文件是针对项目级别的NuGet包管理。与解决方案级别(.sln 文件中的solutionNuGetConfig节点指定的NuGet配置文件)和全局级别(如NuGet.config文件)的NuGet包管理相比,packages.config 文件更加专注于单个项目的NuGet包依赖管理。

五、XML格式的优势

packages.config 文件采用XML格式,这使得其具有良好的可读性和可扩展性。开发者可以轻松地查看和编辑文件中的NuGet包依赖信息,同时也可以根据需要添加自定义的节点或属性来扩展文件的功能。

综上所述,packages.config 文件在WPF项目中扮演着至关重要的角色,它确保了NuGet包依赖的准确记录、项目的一致性、包恢复和更新的便捷性,以及良好的可读性和可扩展性。

六、与 yarn.lock 的异同

WPF项目中的packages.config文件与前端项目中的yarn.lock文件在作用上有一定的相似性,但两者也存在一些差异。以下是对两者作用的比较和分析:

相似性

  1. 记录依赖

    • packages.config文件记录了WPF项目中使用的NuGet包的名称、版本号等依赖信息。
    • yarn.lock文件则记录了前端项目中使用的所有npm模块(或yarn包)的确切版本号和依赖关系。
  2. 确保一致性

    • 两者都用于确保项目在不同开发环境或不同时间点上能够获取到相同的依赖版本,从而保持项目的一致性。
  3. 支持恢复

    • 当项目中的依赖缺失时,NuGet包管理器可以根据packages.config文件恢复所需的NuGet包。
    • 同样地,yarn可以根据yarn.lock文件恢复前端项目中所需的所有npm模块。

差异

  1. 使用场景

    • packages.config文件主要用于.NET Framework下的WPF项目或其他类型的.NET项目。
    • yarn.lock文件则主要用于JavaScript或TypeScript等前端项目,特别是那些使用yarn作为包管理器的项目。
  2. 依赖管理方式

    • 在.NET Core及更高版本的.NET项目中,NuGet包的依赖管理通常通过项目文件(如.csproj)中的PackageReference节点进行,而不是使用packages.config文件。然而,对于仍在使用.NET Framework的项目,packages.config文件仍然是管理NuGet包依赖的主要方式。
    • yarn.lock文件则是yarn包管理器自动生成的,用于锁定前端项目中所有npm模块的版本,以确保在不同环境中都能获得相同的依赖版本。
  3. 文件格式和内容

    • packages.config文件采用XML格式,记录了NuGet包的名称、版本号、安装路径等信息。
    • yarn.lock文件则采用了yarn特定的格式,记录了所有模块的确切版本号和依赖关系,包括模块的下载地址、完整性校验信息等。

综上所述,虽然packages.config文件和yarn.lock文件在记录依赖、确保一致性和支持恢复方面有着相似的作用,但它们在使用场景、依赖管理方式和文件格式等方面存在明显的差异。这些差异反映了不同技术栈和项目类型在依赖管理方面的不同需求和最佳实践。

七、在 csproj 文件中被使用

在WPF(或其他.NET项目类型)的项目文件中,当你看到<None Include="packages.config" />这样的代码行时,它通常出现在项目文件(如.csproj)的ItemGroup元素内。这行代码的作用是将packages.config文件标记为项目中的一个“无操作”项(None item)。

这里有几个关键点需要解释:

  1. ItemGroupItemGroup是项目文件中用于组织一组相关项(如文件、引用、编译项等)的元素。每个ItemGroup可以包含多个不同类型的项。
  2. NoneNone是一种特殊的项类型,用于表示那些不被编译、不参与构建过程的文件。这些文件可能是文档、配置文件、资源文件等,它们对项目的构建没有直接影响,但可能对项目运行或开发过程很重要。
  3. IncludeInclude属性指定了要包含在ItemGroup中的文件或通配符模式。在这个例子中,它指定了packages.config文件。

packages.config文件标记为None项通常意味着:

  • 不编译packages.config文件不会被编译成程序集的一部分。
  • 不直接参与构建:虽然packages.config文件用于管理NuGet包依赖,但将其标记为None项并不直接参与构建过程。构建过程通常通过NuGet恢复命令(如msbuild /t:restore)来读取packages.config文件并恢复所需的NuGet包。
  • 保留在项目中:尽管packages.config文件对构建没有直接影响,但将其保留在项目中并标记为None项可以确保它随项目一起存储和版本控制,这对于团队协作和项目一致性很重要。

然而,需要注意的是,在.NET Core及更高版本的.NET项目中,NuGet包的依赖管理通常通过项目文件中的PackageReference节点进行,而不是使用packages.config文件。因此,在较新的.NET项目中,你可能会看到PackageReference节点而不是packages.config文件被包含在项目文件中。但在仍在使用.NET Framework的项目中,packages.config文件仍然是管理NuGet包依赖的主要方式。

总结:packages.config 是为了兼容旧版本框架的,在最新版本中我们使用的是 csproj 中的 ProjectReference

2. App.xaml 文件的作用

App.xaml主要被用来配置应用程序级别的资源(如样式、主题、图标等),但它实际上还承载着一些其他重要的功能和配置。

以下是App.xaml文件在WPF应用程序中的几个关键作用:

  1. 定义应用程序级别的资源

    • 如您所见,App.xaml中的<Application.Resources>部分允许您定义在整个应用程序范围内可用的资源。这些资源可以包括样式、控件模板、颜色、字体等,它们可以被应用程序中的任何部分引用。
  2. 启动应用程序

    • App.xaml文件通常与App.xaml.cs(或App.vb,取决于您使用的编程语言)代码隐藏文件一起工作。在App.xaml.cs中,您可以定义应用程序的启动逻辑,如初始化设置、配置服务、处理异常等。
  3. 配置应用程序属性

    • 除了资源外,App.xaml还可以包含其他应用程序级别的配置,尽管这些配置通常通过代码隐藏文件而不是XAML来设置。例如,您可以设置应用程序的标题、图标、启动窗口等。
  4. 支持多语言和文化

    • 在您的例子中,App.xaml通过合并资源字典支持了多种语言(如en-us.xamlzh-cn.xaml)。这允许应用程序根据用户的语言偏好动态地更改其界面语言。
  5. 作为应用程序的入口点

    • 当WPF应用程序启动时,它会首先加载App.xaml文件。因此,它是应用程序的入口点,负责初始化应用程序环境和设置。

综上所述,虽然App.xaml在您的例子中主要用于定义视图层的资源,但它在WPF应用程序中扮演着更为广泛的角色。它是应用程序配置、资源管理和启动逻辑的中心点。通过App.xaml和相关的代码隐藏文件,您可以完全控制应用程序的行为和外观。

3. 受保护的虚方法 OnStartup

虚方法是什么

虚方法(Virtual Method)是面向对象编程中的一个概念,它允许在派生类中重写(Override)基类中的方法。当一个类中的方法被声明为虚方法时,这意味着该方法可以在派生类中被改变或扩展,而无需修改基类中的代码。虚方法提供了一种灵活的方式来定义可以在派生类中定制的行为。

在C#等编程语言中,虚方法使用virtual关键字来声明。例如:

	public class BaseClass
	{
	    public virtual void MyMethod()
	    {
	        // 基类中的实现
	    }
	}

	 

	public class DerivedClass : BaseClass
	{
	    public override void MyMethod()
	    {
	        // 派生类中的重写实现
	    }
	}

在这个例子中,BaseClass中的MyMethod是一个虚方法,而DerivedClass重写了这个方法。

OnStartup 执行的时机

在WPF(Windows Presentation Foundation)应用程序中,OnStartup方法是Application类的一个受保护的虚方法。它通常在应用程序启动时由框架自动调用,作为应用程序启动过程中的一部分。

具体来说,OnStartup方法的执行时机如下:

  1. 应用程序启动:当WPF应用程序开始运行时,框架会创建一个Application类的实例,并准备启动应用程序。
  2. 调用OnStartup:在应用程序启动的过程中,框架会调用Application实例的OnStartup方法。这是执行启动逻辑(如初始化设置、配置服务、显示主窗口等)的合适时机。
  3. 执行启动逻辑:在OnStartup方法中,您可以编写自定义的启动逻辑。这包括连接数据库、加载配置文件、注册全局事件处理器等。
  4. 显示主窗口:通常,在OnStartup方法的末尾,您会调用主窗口的Show方法来显示应用程序的主界面。但是,请注意,显示主窗口并不是OnStartup方法执行完毕的必要条件;您可以根据需要在OnStartup方法中执行其他操作。

综上所述,OnStartup方法在WPF应用程序启动时由框架自动调用,是执行启动逻辑和准备应用程序状态的重要时机。

4. Application.Current.Dispatcher.Invoke 和 mainWindow.show

实际上,mainWindow.Show(); 通常不需要被放在 Application.Current.Dispatcher.Invoke 中执行。mainWindow.Show(); 是一个用于显示窗口的标准方法调用,它应该直接在 OnStartup 方法或其他适当的启动逻辑中调用。

Application.Current.Dispatcher.Invoke 通常用于在 WPF 应用程序的 UI 线程上执行代码。WPF 应用程序有一个消息循环(也称为调度器或 Dispatcher),它负责处理所有的 UI 操作。如果你从非 UI 线程(例如,后台线程或任务)中需要更新 UI 元素或执行与 UI 相关的操作,那么你需要使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke 来确保这些操作在 UI 线程上执行。

然而,在 OnStartup 方法中,你通常已经处于 UI 线程中,因为 OnStartup 是由 WPF 框架在应用程序启动时以同步方式调用的。因此,在这个上下文中,没有必要使用 Dispatcher.Invoke 来显示窗口。

下面是一个典型的 WPF 应用程序启动过程的例子:

	public partial class App : Application
	{
	    protected override void OnStartup(StartupEventArgs e)
	    {
	        base.OnStartup(e);
	        // 创建主窗口实例
	        MainWindow mainWindow = new MainWindow();
	        // 显示主窗口
	        mainWindow.Show();
	    }
	}

在这个例子中,mainWindow.Show(); 直接在 OnStartup 方法中调用,这是完全正确的做法。

如果你在某些情况下确实需要在非 UI 线程上执行 UI 操作,那么你应该使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke。但是,在 OnStartup 方法中显示窗口时,这通常不是必要的。

必要性说明:

确保线程安全

-   如果项目的其他部分或第三方库在非 UI 线程上运行,并且需要显示窗口或更新 UI,那么使用 `Dispatcher.Invoke` 可以确保这些操作在正确的线程上执行,从而避免线程冲突和潜在的 UI 冻结或崩溃。
-   在这种情况下,即使 `OnStartup` 方法本身在 UI 线程上,代码作者可能出于一致性和安全性的考虑,仍然选择使用 `Dispatcher.Invoke`

5. Application 实例方法拓展

public static TApp AddSingleProcess<TApp>(this TApp app) where TApp : Application
{
    var process = Process.GetProcesses().Where(p => p.ProcessName == Process.GetCurrentProcess().ProcessName && p.Id != Process.GetCurrentProcess().Id).FirstOrDefault();
    if (process != null)
    {
        if (MessageBox.Show("应用程序已启动,是否重开", "警告", MessageBoxButton.YesNo, MessageBoxImage.Exclamation) == MessageBoxResult.Yes)
        {
            process.Kill();
        }
        else
        {
            app.Shutdown();
        }
    }

    return app;
}

这段代码定义了一个泛型扩展方法 AddSingleProcess<TApp>,它旨在确保一个应用程序(TApp 类型)的单个实例在运行。此方法通过检查当前系统上是否存在与当前进程同名的其他进程来实现这一目的。以下是该代码的详细解释及类型约束的说明:

方法签名解释

  • public static TApp AddSingleProcess<TApp>(this TApp app): 这是一个泛型静态扩展方法。它接受一个类型为 TApp 的参数 app,并返回相同类型的对象。this 关键字表示这是一个扩展方法,它扩展了 TApp 类型。
  • where TApp : Application: 这是一个类型约束,它指定 TApp 必须是从 Application 类派生的类型。这意味着此方法只能用于 Application 类或其子类的实例。

方法体解释

  1. 查找同名进程:

    • var process = Process.GetProcesses().Where(p => p.ProcessName == Process.GetCurrentProcess().ProcessName && p.Id != Process.GetCurrentProcess().Id).FirstOrDefault();
    • 这行代码获取当前系统上所有进程的列表,并使用 LINQ 查询来查找与当前进程同名但 ID 不同的进程。Process.GetCurrentProcess() 返回当前进程的实例。FirstOrDefault() 确保如果找不到匹配的进程,则 process 变量将为 null
  2. 处理找到的进程:

    • 如果找到了一个与当前进程同名的其他进程(process != null),则显示一个消息框,询问用户是否希望关闭已存在的进程实例。
    • 如果用户点击“是”(MessageBoxResult.Yes),则使用 process.Kill(); 终止该进程。
    • 如果用户点击“否”,则调用 app.Shutdown(); 来关闭当前的应用程序实例。
  3. 返回应用程序实例:

    • 无论是否找到了同名进程,方法最终都会返回传入的 app 实例。

注意事项

  • 安全性: 强制终止其他进程可能会导致数据丢失或不一致,因此在实际应用中应谨慎使用。
  • 用户体验: 弹出消息框询问用户是否关闭已存在的进程可能会影响用户体验。更好的做法可能是使用更隐蔽的方法来处理这种情况,例如通过互斥锁(Mutex)来防止多个实例的启动。
  • 类型约束: 通过 where TApp : Application 约束,此方法仅限于 Application 类及其子类的实例。这确保了 app.Shutdown(); 调用是有效的,因为 Application 类提供了 Shutdown 方法。
  • 异常处理: 代码中没有显示异常处理逻辑。在实际应用中,可能需要添加适当的异常处理来确保程序的健壮性。

总的来说,这段代码的目的是确保一个应用程序只能有一个实例在运行。它通过查找并可能终止同名进程来实现这一点,但这种方法有其潜在的风险和局限性。

6. WPF 工程异常捕获及 Application 实例方法扩展

如下代码展示的是如何将异常捕获之后的弹窗逻辑代码拓展到 Application 实例上去:

public static TApp AddExceptionDialog<TApp>(this TApp app) where TApp : Application
{
    app.DispatcherUnhandledException += (s, e) =>
    {
        if (MessageBox.Show(e.Exception.Message, "发生错误是否查看详情", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
        {
            Window window = new Window();
            window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            window.Content = new ScrollViewer()
            {
                HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
                VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
                Content = new TextBlock { Text = e.Exception.ToString() }
            };
            window.Title = "详细错误原因";
            window.ShowDialog();
        }
        e.Handled = true;
    };
    AppDomain.CurrentDomain.UnhandledException += (s, e) => {
        Exception exception = e.ExceptionObject as Exception;
        if (MessageBox.Show(exception.Message, "发生错误是否查看详情", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
        {
            Window window = new Window();
            window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            window.Content = new ScrollViewer()
            {
                HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
                VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
                Content = new TextBlock { Text = exception.ToString() }
            };
            window.Title = "详细错误原因";
            window.ShowDialog();
        }
    };
    return app;
}

上述代码中有三个可以学习的点:

  • 异常处理函数如何设置,以及设置的两个时机
  • MessageBox 组件的使用及常用的属性和方法
  • Window 组件实例化及样式、内容设置