dotnet 职业技术学院

博客

dotnet 职业技术学院

在 Windows 系统上降低 UAC 权限运行程序(从管理员权限降权到普通用户权限)

dotnet 职业技术学院 发布于 2019-03-19

在 Windows 系统中,管理员权限和非管理员权限运行的程序之间不能使用 Windows 提供的通信机制进行通信。对于部分文件夹(ProgramData),管理员权限创建的文件是不能以非管理员权限修改和删除的。

然而,一个进程运行之后启动的子进程,会继承当前进程的 UAC 权限;于是有时我们会有降权运行的需要。本文将介绍 Windows 系统上降权运行的几种方法。


本文的降权运行指的是:

  1. 有一个 A 程序是以管理员权限运行的(典型的,如安装包);
  2. 有一个 B 程序会被 A 启动(我们期望降权运行的 B 程序)。

如何判断当前进程的 UAC 权限

通过下面的代码,可以获得当前进程的 UAC 权限。

var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);

而如果要判断是否是管理员权限,则使用:

if (principal.IsInRole(WindowsBuiltInRole.Administrator))
{
    // 当前正在以管理员权限运行。
}

此代码如果在 .NET Core 中编写,需要额外安装 Windows 兼容包:Microsoft.Windows.Compatibility

方法一:使用 runas 命令来运行程序(推荐)

使用 runas 命令来运行,可以指定一个权限级别:

> runas /trustlevel:0x20000 "C:\Users\walterlv\Desktop\walterlv.exe"
var subProcessFileName = "C:\Users\walterlv\Desktop\walterlv.exe";
Process.Start("runas.exe", $"/trustlevel:0x20000 {subProcessFileName}");

关于 runas 的更多细节,可以参考我的另一篇博客:

方法二:使用 explorer.exe 代理运行程序

请特别注意,使用 explorer.exe 代理运行程序的时候,是不能带参数的,否则 explorer.exe 将不会启动你的程序。

因为绝大多数用户启动系统的时候,explorer.exe 进程都是处于运行状态,而如果启动一个新的 explorer.exe,都会自动激活当前正在运行的进程而不会启动新的。

于是我们可以委托默认以普通权限运行的 explorer.exe 来代理启动我们需要启动的子进程,这时启动的子进程便是与 explorer.exe 相同权限的。

var subProcessFileName = "C:\Users\walterlv\Desktop\walterlv.exe";
Process.Start("explorer.exe", subProcessFileName);

如果用户计算机上的 UAC 是打开的,那么 explorer.exe 默认就会以标准用户权限运行。通过以上代码,walterlv.exe 就会以与 explorer.exe 相同权限运行,也就是降权运行了。

不过值得注意的是,Windows 7 上控制面板的 UAC 设置拉倒最低就是关掉 UAC 了;Windows 8 开始拉倒最底 UAC 还是打开的,只是不会提示 UAC 弹窗而已。也就是说,拉倒最底的话,Windows 7 的 UAC 就会关闭,explorer.exe 就会以管理员权限启动。

下面的代码,如果发现自己是以管理员权限运行的,那么就降权重新运行自己,然后自己退出。(当然在关闭 UAC 的电脑上是无效的。)

var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
if (principal.IsInRole(WindowsBuiltInRole.Administrator))
{
    // 检测到当前进程是以管理员权限运行的,于是降权启动自己之后,把自己关掉。
    Process.Start("explorer.exe", Assembly.GetEntryAssembly().Location);
    Shutdown();
    return;
}

请再次特别注意,使用 explorer.exe 代理运行程序的时候,是不能带参数的,否则 explorer.exe 将不会启动你的程序。

方法三:在启动进程时传入用户名和密码

ProcessStartInfo 中有 UserNamePassword 属性,设置此属性可以以此计算机上的另一个用户身份启动此进程。如果这个用户是普通用户,那么就会以普通权限运行此进程。

var processInfo = new ProcessStartInfo
{
    Verb = "runas",
    FileName = "walterlv.exe",
    UserName = "walterlv",
    Password = ReadPassword(),
    UseShellExecute = false,
    LoadUserProfile = true
};
Process.Start(processInfo);

上面的 ReadPassword 函数来自我的另一篇博客:如何让 .NET Core 命令行程序接受密码的输入而不显示密码明文 - walterlv

然而,此方法最大的问题在于——产品级的程序,不可能也不应该知道用户的密码!所以实际上这样的方法并不实用。

方法四:使用 Shell 进程的 Access Token 来启动进程

此方法需要较多的 Windows API 调用,我没有尝试过这种方法,但是你可以自行尝试下面的链接:


参考资料

启用 Windows 审核模式(Audit Mode),以 Administrator 账户来设置电脑的开箱体验

dotnet 职业技术学院 发布于 2019-03-17

在你刚刚安装完 Windows,在 Windows 开箱体验输入以创建你的用户账户之前,你可以按下 Ctrl + Shift + F3 来进入审核模式。

本文将介绍审核模式。


OOBE

OOBE,Out-of-Box Experience,开箱体验。对于 Windows 系统来说,就是当你买下电脑回来,兴奋地打开电脑开机后第一个看到的界面。

具体来说,就是设置你的账号以及各种个性化设置的地方。

本文即将要说的审核模式就是在这里开启的。当然你设置完账号也一样能开启,但开箱就是要来个干净整洁嘛,所以就是应该在还没有账号的时候进入审核模式。

进入审核模式

在 OOBE 界面中,按下 Ctrl + Shift + F3 两次即会进入审核模式。

实际上此时进入的账号是 Administrator 账号。我在 Windows 中的 UAC 用户账户控制 一文中说到,Administrator 账号下启动进程获取到的访问令牌都是完全访问令牌。所以在这里 UWP 程序是无法运行的(逃

当你进入审核模式之后,会看到自动启动了一个 sysprep 的程序,它位于 C:\Windows\System32\Sysprep 目录下。

系统准备工具

在审核模式下,重启也会继续进入审核模式。如果要关闭审核模式,则需要在 sysprep 程序中把下一次的启动选项改为开箱体验。

关于清理选项中的“通用”:如果你只为这台电脑或这个型号的电脑设置开箱体验,那么就关闭“通用”;如果把这个开箱体验做好之后会拷贝副本到其他型号的电脑上,那么就勾选“通用”。区别就是是否清理掉设备的特定的驱动文件。

清理 - 通用

当然,你现在就可以去 C:\Windows\System32\Sysprep 目录中启动 sysprep.exe,然后给你的电脑再带来一次开箱体验。

审核模式有什么作用?

从进入审核模式时打开的 sysprep.exe 程序可以看出来,这个模式主要就是为了准备开箱体验的。

你可以在这里以 Administrator 权限来为此计算机安装驱动,为将来此计算机的所有用户安装应用、存放一些你认为他们需要的文件。而这一切操作都不需要特地创建一个账号。可以说 Administrator 账户内置到系统里,主要的目的就是这个了,临时使用。而目前就是在审核模式中制作开箱体验。


参考资料

如何创建应用程序清单文件 App.Manifest,如何创建不带清单的应用程序

dotnet 职业技术学院 发布于 2019-03-17

如果你的程序对 Windows 运行权限有要求,那么需要设置应用程序清单。本文介绍如何添加应用程序清单,并解释其中各项权限设置的实际效果。


嵌入带默认设置的清单

对于 WPF 和 Windows Forms 程序,如果你什么都不做,那么就已经嵌入了一个带有默认设置的清单。

下图可以在 Visual Studio 中的项目上右键属性插件。

嵌入带默认设置的清单

新建一个自定义的清单文件

在项目上右键,添加,新建项。可以在新建模板中找到“应用程序清单文件”。确认后即添加了一个新的清单文件。这时,项目属性页中的清单也会自动设置为刚刚添加的清单文件。

按照清单模板新建清单

默认的清单中,包含 UAC 清单选项、系统兼容性选项、DPI 感知级别选项和 Windows 公共控件和对话框的主题选项。

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <!-- UAC 清单选项
             如果想要更改 Windows 用户帐户控制级别,请使用
             以下节点之一替换 requestedExecutionLevel 节点。n
        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />

            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
            如果你的应用程序需要此虚拟化来实现向后兼容性,则删除此
            元素。
        -->
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>

  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
           Windows 版本的列表。取消评论适当的元素,
           Windows 将自动选择最兼容的环境。 -->

      <!-- Windows Vista -->
      <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->

      <!-- Windows 7 -->
      <!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->

      <!-- Windows 8 -->
      <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->

      <!-- Windows 8.1 -->
      <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->

      <!-- Windows 10 -->
      <!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->

    </application>
  </compatibility>

  <!-- 指示该应用程序可以感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
       自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
       选择加入。选择加入此设置的 Windows 窗体应用程序(目标设定为 .NET Framework 4.6 )还应
       在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。-->
  <!--
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>
  -->

  <!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
  <!--
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
          type="win32"
          name="Microsoft.Windows.Common-Controls"
          version="6.0.0.0"
          processorArchitecture="*"
          publicKeyToken="6595b64144ccf1df"
          language="*"
        />
    </dependentAssembly>
  </dependency>
  -->

</assembly>

创建不带清单的应用程序

你也可以创建一个不带应用程序清单的应用程序。方法是在属性页中将清单设置为“创建不带清单的应用程序”。

创建不带清单的应用程序

C#/.NET 如何结束掉一个进程

dotnet 职业技术学院 发布于 2019-03-14

本文介绍如何结束掉一个进程。


结束掉特定名字的进程

ProcessInfo 中有 Kill 实例方法可以调用,也就是说如果我们能够拿到一个进程的信息,并且对这个进程拥有访问权限,那么我们就能够结束掉它。

使用 Process.GetProcessesByName(processName) 可以按照名字拿到进程信息。于是我们可以使用这个方法杀掉具有特定名称的进程。

private void KillProcess(string processName)
{
    foreach (var process in Process.GetProcessesByName(processName))
    {
        try
        {
            // 杀掉这个进程。
            process.Kill();

            // 等待进程被杀掉。你也可以在这里加上一个超时时间(毫秒整数)。
            process.WaitForExit();
        }
        catch (Win32Exception ex)
        {
            // 无法结束进程,可能有很多原因。
            // 建议记录这个异常,如果你的程序能够处理这里的某种特定异常了,那么就需要在这里补充处理。
            // Log.Error(ex);
        }
        catch (InvalidOperationException)
        {
            // 进程已经退出,无法继续退出。既然已经退了,那这里也算是退出成功了。
            // 于是这里其实什么代码也不需要执行。
        }
    }
}

结束掉自己

可以是参见林德熙的博客,使用 Environment.FailFast,在结束掉自己的时候记录自己的错误日志。

让你的 VSCode 具备调试 C# 语言 .NET Core 程序的能力

dotnet 职业技术学院 发布于 2019-03-14

如果你是开发个人项目,那就直接用 Visual Studio Community 版本吧,对个人免费,对小团体免费,不需要这么折腾。

如果你是 Mac / Linux 用户,不想用 Visual Studio for Mac 版;或者不想用 Visual Studio for Windows 版那么重磅的 IDE 来开发简单的 .NET Core 程序;或者你就是想像我这么折腾,那我们就开始吧!


安装 .NET Core Sdk、Visual Studio Code 和 C# for Visual Studio Code

  1. 点击这里下载正式或者预览版的 .NET Core 然后安装
  2. 点击这里下载 Visual Studio Code 然后安装
  3. 在 Visual Studio Code 里安装 C# for Visual Studio Code 插件(步骤如下图所示)

安装 C# for Visual Studio Code 插件

搜索的时候,推荐使用 OmniSharp 关键字,因为这可以得到唯一的结果,你不会弄混淆。如果你使用 C# 作为关键字,那需要小心,你得找到名字只有 C#,点开之后是 C# for Visual Studio Code 的那款插件。因为可能装错,所以我不推荐这么做。

对于新版的 Visual Studio Code,装完会自动启用,所以你不用担心。我们可以后续步骤了。

使用 VSCode 创建 .NET Core 项目

本文不会讲解如何使用 VSCode 创建 .NET Core 项目,因为这不是本文的重点。

也许你可以参考我还没有写的另一篇博客。

打开一个现有的 .NET Core 项目

现在假设你已经有一个现成的能用 Visual Studio 跑起来的 .NET Core 控制台项目了(可能是刚克隆下来的,也可能就是用我另一篇博客中的教程创建的),于是我们就在这个项目上进行开发。

本文以我的自动化测试程序 Walterlv.InfinityStartupTest 为例进行说明。如果你找不到合适的例子,可以使用这篇博客创建一个。

在这个文件夹的根目录下右键,然后 使用 Code 打开

使用 Visual Studio Code 打开文件夹

配置编译和调试环境

正常情况下,当你用 Visual Studio Code 打开一个包含 .NET Core 项目的文件夹时,C# 插件会在右下角弹出通知提示,问你要不要为这个项目创建编译和调试文件,当然选择“Yes”。

创建编译和调试文件的提示

这个提示一段时间不点会消失的,但是右下角会有一个小铃铛(上面的图片也可以看得到的),点开可以看到刚刚消失的提示,然后继续操作。

这时,你的项目文件夹中会多出两个文件,都在 .vscode 文件夹中。tasks.json 是编译文件,指导如何进行编译;launch.json 是调试文件,指导如何进行调试。

多出的编译文件和调试文件

开始调试

现在,你只需要按下 F5(就是平时 Visual Studio 调试按烂的那个),你就能使用熟悉的调试方式在 Visual Studio Code 中来调试 .NET Core 程序了。

下图是调试进行中各个界面的功能分区。如果你没看到这个界面,请点击左侧那只被圈在圆圈里面的小虫子。

Visual Studio Code 中的 .NET Core 调试界面

当你按照本文操作,在按下 F5 后有各种报错,那么原因只有一个——你的这个项目本身就是编译不过的,你自己用命令行也会编译不过。你需要解决编译问题,而本文只是入门教程,不会说如何解决编译问题。

手工设置 tasks.json 和 launch.json 文件

如果自动创建的这两个文件有问题,或者你根本就找不到自动创建的入口,可以考虑手工创建这两个文件。

请参见博客:

还补充一句,本文说编译文件和调试文件是不对的,因为在 Visual Studio Code 中没有编译这个概念,编译只是任务中的一种而已。

手工编辑 tasks.json 和 launch.json,让你的 VSCode 具备调试 .NET Core 程序的能力

dotnet 职业技术学院 发布于 2019-03-14

如果 C# for Visual Studio Code 没有办法自动为你生成正确的 tasks.json 和 launch.json 文件,那么可以考虑阅读本文手工创建他们。


前期准备

你需要安装 .NET Core Sdk、Visual Studio Code 和 C# for Visual Studio Code,然后打开一个 .NET Core 的项目。如果你没有准备,请先阅读:

本文主要处理自动生成的配置文件无法满足要求,手工生成。

半自动创建 tasks.json 和 launch.json

这依然是个偷懒的好方案,我喜欢。

  1. 按下 F5;
  2. 在弹出的列表中,选择 .NET Core;

选择 .NET Core

自动生成的 tasks.json 和 launch.json

你不需要再做什么其他的工作了,这时再按下 F5 你已经可以开始调试了。

全手工创建 tasks.json 和 launch.json

tasks.json 定义一组任务。其中我们需要的是编译任务,通常编译一个项目使用的动词是 build。比如 dotnet build 命令就是这样的动词。

于是定义一个名字为 build 的任务,对应 label 标签。commandargs 对应我们在命令行中编译一个项目时使用的命令行和参数。typeprocess 表示此任务是启动一个进程。

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "dotnet",
            "type": "process",
            "args": [
                "build",
                "${workspaceFolder}/Walterlv.InfinityStartupTest/Walterlv.InfinityStartupTest.csproj"
            ],
            "problemMatcher": "$msCompile"
        }
    ]
}

在 launch.json 中通常配置两个启动配置,一个是启动调试,一个是附加调试。

type 是在安装了 C# for Visual Studio Code (powered by OmniSharp) 插件之后才会有的调试类型。preLaunchTask 表示在此启动开始之前需要执行的任务,这里指定的 build 跟前面的 build 任务就关联起来了。program 是调试的程序路径,console 指定调试控制台使用内部控制台。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "调试 Walterlv 的自动化测试程序",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build",
            "program": "${workspaceFolder}/Walterlv.InfinityStartupTest/bin/Debug/netcoreapp3.0/Walterlv.InfinityStartupTest.dll",
            "args": [],
            "cwd": "${workspaceFolder}/Walterlv.InfinityStartupTest",
            "console": "internalConsole",
            "stopAtEntry": false,
            "internalConsoleOptions": "openOnSessionStart"
        },
        {
            "name": "附加进程",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickProcess}"
        }
    ]
}

这样自己手写的方式更灵活但是也更难。

自然码的形码

dotnet 职业技术学院 发布于 2019-03-14

使用拼音/双拼输入法,如果你的打字速度还需要继续提升,那么就不应该再不断地看着候选框打字了。使用双拼形码可以规避相当多字词的选字。

本文整理自然码的形码,然后附带一张我自己制作的自然码形码的键盘图。


输入法的选择

目前各种双拼输入法中,辅码最接近自然码形码的,是手心输入法。所以我选用了手心输入法并重新训练了词库。

这款输入法与 360 有点关系,不过很不 360。这让我有点犹豫,不过中文输入法里面也没有几个让人省心得了。小狼毫实在是没法儿折腾到好用呀。

自然码的形码

自然码的形码主要是部首的声母。

  • A 一 丨 亅 レ 乛 フ ㄥ
  • B 八 丷 卜 冖 宀 匕 比 白 贝 疒 鼻
  • C 艹 卄 廾  廿 屮 卝 寸
  • D 丶 冫 氵 刀 刂 リ ㄍ ⺈ 丁 歹 癶
  • E 二 儿 阝 耳 卩 
  • F 扌 丰 反 方 风 父 缶 巿
  • G 乚  ㄅ ㄋ 勹 弓 工 广 艮 戈 瓜 谷 革 骨 鬼 夬 罓
  • H 灬 火 禾 户 虍 黑 乊 厷
  • I 厂 川 巛 亍 车 虫 臣 辰 赤 齿 髟 豖
  • J 几 九 己 巾 斤 钅 金 见 臼  角
  • K コ 凵 匚 冂 口 囗 丂
  • L 力 六 立 龙 耒 卤 鹿
  • M 木 门 毛 马 米 矛 母 皿 尨 麻 丏
  • N 女 牛 牜 ⺧ 鸟
  • O 日 曰 月 目
  • P ノ 彡 片 皮 疋 ⺪ 攴
  • Q 七 犭 犬 丌 欠 气 且
  • R 亻 人 入 肉
  • S 三 罒 巳 纟 糹 糸 厶 
  • T 土 田
  • U 水 手  食 飠 饣 示 礻 山 石 尸 十 士 矢 殳 舌 身 豕 鼠
  • V 隹 ⺮ 爫 爪 豸 止 至 舟
  • W 文 亠 攵 夂 夊 ㄨ 王 韦 瓦
  • X 彳 小  心 忄  血 彐 夕 习 西 辛
  • Y 乙 又 已 讠 言 幺 尤 尢 冘 衣 衤 羊 牙 业 由 用 页 酉 鱼 雨 羽 聿 乑 乂
  • Z 辶 廴 子 自 走 足 ⻊ 卆

键盘图

将以上的形码整理成一个键盘图,有助于你在练习形码的初期记忆这些形码。

自然码形码的键盘图

图中所用的背景源自微软流畅设计 Fluent Design System 的新版本,Microsoft Office 新图标设计的视频片段。我自己进行了一些高斯模糊处理。


参考链接

我收集的各种公有 NuGet 源

dotnet 职业技术学院 发布于 2019-03-12

本文收集我发现的各种公共 NuGet 源。


如何添加本文介绍的 NuGet 源?

请参见:

官方 NuGet 源

官方网站:https://www.nuget.org/

NuGet 镜像

其他 NuGet 源

私有 NuGet 源

NuGet 网站

呃……这部分只是 NuGet 网站而已,你可以在这里浏览 NuGet 包的各种信息,但是它不提供源。

在 csproj 文件中使用系统环境变量的值(示例将 dll 生成到 AppData 目录下)

dotnet 职业技术学院 发布于 2019-03-12

Windows 系统以及很多应用程序会考虑使用系统的环境变量来传递一些公共的参数或者配置。Windows 资源管理器使用 %var% 来使用环境变量,那么我们能否在 Visual Studio 的项目文件中使用环境变量呢?

本文介绍如何在 csproj 文件中使用环境变量。


遇到的问题

在 Windows 资源管理器中,我们可以使用 %AppData% 进入到用户的漫游路径。我正在为 希沃白板5 为互动教学而生 - 课件制作神器 编写插件,于是需要将插件放到指定目录:

%AppData%\Seewo\EasiNote5\Walterlv.Presentation

在 Windows 资源管理器中可以直接输入以上文字进入对应的目录(当然需要确保存在)。

插件目录

更多关于路径的信息可以参考:UWP 中的各种文件路径(用户、缓存、漫游、安装……) - walterlv

然而,为了调试方便,我最好在 Visual Studio 中编写的时候就能直接输出到插件目录。

于是,我需要将 Visual Studio 的调试目录设置为以上目录,但是以上目录中包含环境变量 %AppData%

在 Visual Studio 中修改输出路径

如果直接在 csproj 中使用 %AppData%,那么 Visual Studio 会原封不动地创建一个这样的文件夹。

一个诡异的文件夹

实际上,Visual Studio 是天然支持环境变量的。直接使用 MSBuild 获取属性的语法即可获取环境变量的值。

也就是说,使用 $(AppData) 即可获取到其值。在我的电脑上是 C:\Users\lvyi\AppData\Roaming

于是,在 csproj 中设置 OutputPath 即可正确输出我的插件到目标路径。

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFrameworks>net472</TargetFrameworks>
        <OutputPath>$(AppData)\Seewo\EasiNote5\Extensions\Walterlv.Presentation</OutputPath>
        <AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
    </PropertyGroup>
</Project>

这里,我额外设置了 AppendTargetFrameworkToOutputPath 属性,这是避免 net472 出现在了目标输出路径中。你可以阅读我的另一篇博客了解更多关于输出路径的问题:

为 WPF 程序添加 Windows 跳转列表的支持

dotnet 职业技术学院 发布于 2019-03-10

Windows 跳转列表是自 Windows 7 时代就带来的功能,这一功能是跟随 Windows 7 的任务栏而发布的。当时应用程序要想用上这样的功能需要调用 shell 提供的一些 API。

然而在 WPF 程序中使用 Windows 跳转列表功能非常简单,在 XAML 里面就能完成。本文将介绍如何让你的 WPF 应用支持 Windows 跳转列表功能。


一个简单的跳转列表程序

新建一个 WPF 程序,然后直接在 App.xaml 中添加跳转列表的代码。这里为了更快上手,我直接贴出整个 App.xaml 的代码。

<Application x:Class="Walterlv.Demo.WindowsTasks.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Walterlv.Demo.WindowsTasks"
             StartupUri="MainWindow.xaml">
    <JumpList.JumpList>
        <JumpList ShowRecentCategory="True" ShowFrequentCategory="True">
            <JumpTask Title="启动新窗口" Description="启动一个新的空窗口" />
            <JumpTask Title="修改 walterlv 的个性化设置" Description="打开个性化设置页面并定位到 walterlv 的设置"
                      IconResourcePath="C:\Windows\System32\wmploc.dll" IconResourceIndex="17"
                      Arguments="--account" />
        </JumpList>
    </JumpList.JumpList>
</Application>

顺便的,我加了一个简单的图标,这样不至于显示一个默认的应用图标。

添加的简单的图标

运行此程序后就可以在任务栏上右击的时候看到跳转列表:

运行后看到的跳转列表

在这段程序中,我们添加了两个“任务”,在跳转列表中有一个“任务”分类。因为我的系统是英文,所以显示的是“Task”。

在任务分类中,有两个“任务”,启动新窗口 以及 修改 walterlv 的个性化设置。第一个任务只设了标题和鼠标移上去的提示信息,于是显示的图标就是应用本身的图标,点击之后也是启动任务自己。第二个任务设置了 Arguments 参数,于是点击之后会带里面设置的参数启动自己;同时设置了 IconResourcePathIconResourceIndex 用于指定图标。

这种图标的指定方式是 Windows 系统中非常常用的方式。你可以在我的另一篇博客中找到各种各样系统自带的图标;至于序号,则是自己去数。

定制跳转列表的功能

JumpList 有两个属性 ShowRecentCategoryShowFrequentCategory,如果指定为 true 则表示操作系统会自动为我们保存此程序最近使用的文件的最频繁使用的文件。

Windows 的跳转列表有两种不同的列表项,一种是“任务”,另一种是文件。至于这两种不同的列表项如何在跳转列表中安排,则是操作系统的事情。

这两种不同的列表项对应的类型分别是:

  • JumpTask
  • JumpPath

JumpTask 可以理解为这就是一个应用程序的快捷方式,可以指定应用程序的路径(ApplicationPath)、工作目录(WorkingDirectory)、启动参数(Arguments)和图标(IconResourcePathIconResourceIndex)。如果不指定路径,那么就默认为当前程序。也可以指定显示的名称(Title)和鼠标移上去可以看的描述(Description)。

JumpPath 则是一个路径,可以是文件或者文件夹的路径。通常用来作为最近使用文件的展示。特别说明:你必须关联某种文件类型这种类型的文件才会显示到 JumpPath 中。

另外,JumpTaskJumpPath 都有一个 CustomCategory 属性可以指定类别。对于 JumpTask,如果不指定类别,那么就会在默认的“任务”(Task)类别中。对于 JumpPath,如果不指定类别,就在最近的文件中。

JumpTask 如果不指定 TitleCustomCategory 属性,那么他会成为一个分隔符。


参考资料

Windows 上的应用程序在运行期间可以给自己改名(可以做 OTA 自我更新)

dotnet 职业技术学院 发布于 2019-03-10

程序如何自己更新自己呢?你可能会想到启动一个新的程序或者脚本来更新自己。然而 Windows 操作系统允许一个应用程序在运行期间修改自己的名称甚至移动自己到另一个文件夹中。利用这一点,我们可以很简单直接地做程序的 OTA 自动更新。

本文将介绍示例程序运行期间改名并解释其原理。


在程序运行期间手工改名

我们写一个简单的程序。

简单的程序

将它运行起来,然后删除。我们会发现无法删除它。

无法删除程序

但是,我们却可以很轻松地在资源管理器中对它进行改名,甚至将它从一个文件夹中移动到另一个文件夹中。

已经成功改名

值得注意的是,你不能跨驱动器移动此文件。

不止是 exe 文件,dll 文件也是可以改名的

实际上,不止是 exe 文件,在 exe 程序运行期间,即使用到了某些 dll 文件,这些 dll 文件也是可以改名的。

当然,一个 exe 的运行不一定在启动期间就加载好了所有的 dll,所以如果你在 exe 启动之后,某个 dll 加载之前改了那个 dll 的名称,那么会出现找不到 dll 的情况,可能导致程序崩溃。

为什么 Windows 上的可执行程序可以在运行期间改名?

Windows 的文件系统由两个主要的表示结构:一个是目录信息,它保存有关文件的元数据(如文件名、大小、属性和时间戳);第二个是文件的数据链。

当运行程序加载一个程序集的时候,会为此程序集创建一个内存映射文件。为了优化性能,往往只有实际用到的部分才会被加入到内存映射文件中;当需要用到程序集文件中的某块数据时,Windows 操作系统就会将需要的部分加载到内存中。但是,内存映射文件只会锁定文件的数据部分,以保证文件文件的数据不会被其他的进程修改。

这里就是关键,内存映射文件只会锁定文件的数据部分,而不会锁住文件元数据信息。这意味着你可以随意修改这些元数据信息而不会影响程序的正常运行。这就包括你可以修改文件名,或者把程序从一个文件夹下移动到另一个文件夹去。

但是跨驱动器移动文件,就意味着需要在原来的驱动器下删除文件,而这个操作会影响到文件的数据部分,所以此操作不被允许。

编写一个程序在运行期间自动改名

一般来说,需要 OTA 更新的程序是客户端程序,所以实际上真正需要此代码的是客户端应用。以下代码中我使用 .NET Core 3.0 来编写一个给自己改名的 WPF 程序。

using System.Diagnostics;
using System.IO;
using System.Windows;

namespace Walterlv.Windows.Updater
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            var fileName = Process.GetCurrentProcess().MainModule.FileName;
            var newFileName = Path.Combine(Path.GetDirectoryName(fileName), "OldUpdater.exe");
            File.Move(fileName, newFileName);
            // 省略的代码:将新下载下载的程序改名成 fileName。
        }
    }
}

于是,程序自己在运行后会改名。

程序已经自己改名

顺便的,以上代码仅适用于 .NET Framework 的桌面应用程序或者 .NET Core 3.0 的桌面应用程序。如果是 .NET Core 2.x,那么以上代码在获取到进程名称的时候可能是 dotnet.exe(已发布的 .NET Core 程序除外)。


参考资料

.NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化

dotnet 职业技术学院 发布于 2019-03-10

最近我大幅度重构了我一个库的项目结构,使之使用最新的项目文件格式(基于 Microsoft.NET.Sdk)并使用 SourceYard 源码包来打包其中的一些公共代码。不过,最终生成了一个新的 dll 之后却心有余悸,不知道我是否删除或者修改了某些 API,是否可能导致我原有库的使用者出现意料之外的兼容性问题。

另外,准备为一个产品级项目更新某个依赖库,但不知道更新此库对我们的影响有多大,希望知道目前版本和希望更新的版本之间的 API 差异。

索性发现了 JustAssembly 可以帮助我们分析程序集 API 的变化。本文将介绍如何使用 JustAssembly 来分析不同版本程序集 API 的变化。


下载和安装 JustAssembly

JustAssemblyTelerik 开源的一款程序集分析工具。

你可以去它的官网下载并安装:Assembly Diff Tool for .NET - JustAssembly

开始比较

启动 JustAssembly,在一开始丑陋(逃)的界面中选择旧的和新的 dll 文件,然后点击 Load

选择旧的和新的 dll 文件

然后,你就能看到新版本的 API 相比于旧版本的差异了。

新版本的 API 相比于旧版本的差异

关于比较结果的说明

在差异界面中,差异有以下几种显示:

  1. 没有差异
    • 以白色底显示
  2. 新增
    • 以绿色底辅以 + 符号显示
  3. 删除
    • 以醒目的红色底辅以 - 符号显示
  4. 有部分差异
    • 以蓝紫色底辅以 ~ 符号显示

这里可能需要说明一下“部分差异”:由于差异是以树状结构显示的,所以如果子节点有新增,那么父节点因为既有新增又存在未修改的节点,所以会以“有部分差异”的方式显示。

对于每一个差异,双击可以去看差异的代码详情。

上图我的 SourceFusion 项目在版本更新的时候只有新增的 API,没有修改和删除的 API,所以还是一个比较健康的 API 更新。


参考资料

详解 .NET 反射中的 BindingFlags 以及常用的 BindingFlags 使用方式

dotnet 职业技术学院 发布于 2019-03-10

使用 .NET 的反射 API 时,通常会要求我们传入一个 BindingFlags 参数用于指定反射查找的范围。不过如果对反射不熟的话,第一次写反射很容易写错导致找不到需要的类型成员。

本文介绍 BindingFlags 中的各个枚举标记的含义、用途,以及常用的组合使用方式。


所有的 BindingFlags

默认值

// 默认值
Default

查找

这些标记用于反射的时候查找类型成员:

// 表示查找的时候,需要忽略大小写。
IgnoreCase

// 仅查找此特定类型中声明的成员,而不会包括这个类继承得到的成员。
DeclaredOnly

// 仅查找类型中的实例成员。
Instance

// 仅查找类型中的静态成员。
Static

// 仅查找类型中的公共成员。
Public

// 仅查找类型中的非公共成员(internal protected private)
NonPublic

// 会查找此特定类型继承树上得到的静态成员。但仅继承公共(public)静态成员和受保护(protected)静态成员;不包含私有静态成员,也不包含嵌套类型。
FlattenHierarchy

调用

这些标记用于为 InvokeMember 方法提供参数,告知应该如何反射调用一个方法:

// 调用方法。
InvokeMethod

// 创建实例。
CreateInstance

// 获取字段的值。
GetField

// 设置字段的值。
SetField

// 获取属性的值。
GetProperty

// 设置属性的值。
SetProperty

其他

接下来下面的部分就不是那么常用的了。

这些标记用于为 InvokeMember 方法提供参数,但是仅在调用一个 COM 组件的时候才应该使用:

PutDispProperty
PutRefDispProperty
ExactBinding
SuppressChangeType
OptionalParamBinding

下面是一些杂项……

// 忽略返回值(在 COM 组件的互操作中使用)
IgnoreReturn

// 反射调用方法时如果出现了异常,通常反射会用 TargetInvocationException 包装这个异常。
// 此标记用于禁止把异常包装到 TargetInvocationException 中。
DoNotWrapExceptions

你可能会有的疑问

  1. 如果 A 程序集对 B 程序集内部可见(InternalsVisibleTo("B")),那么 B 在反射查找 A 的时候,internal 成员的查找应该使用 Public 还是 NonPublic 标记呢?
    • 依然是 NonPublic 标记。
    • 因为反射的是程序集的元数据,这是静态的数据,跟运行时状态是无关的。

常用的组合

从上面的解释中可以发现,这个类型的设计其实是有问题的,不符合单一职责原则。所以我们会在不同的使用场景下使用不同区域的组合。

查找,也就是获取一个类型中的字段、属性、方法等的时候使用的。

拿到所有成员:

BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance

实际上 RuntimeReflectionExtensions.Everything 属性就是这么写的。

拿到公有的实例成员:

BindingFlags.Public | BindingFlags.Instance

附 BindingFlags 的源码

[Flags]
public enum BindingFlags
{
    // NOTES: We have lookup masks defined in RuntimeType and Activator.  If we
    //    change the lookup values then these masks may need to change also.

    // a place holder for no flag specifed
    Default = 0x00,

    // These flags indicate what to search for when binding
    IgnoreCase = 0x01,          // Ignore the case of Names while searching
    DeclaredOnly = 0x02,        // Only look at the members declared on the Type
    Instance = 0x04,            // Include Instance members in search
    Static = 0x08,              // Include Static members in search
    Public = 0x10,              // Include Public members in search
    NonPublic = 0x20,           // Include Non-Public members in search
    FlattenHierarchy = 0x40,    // Rollup the statics into the class.

    // These flags are used by InvokeMember to determine
    // what type of member we are trying to Invoke.
    // BindingAccess = 0xFF00;
    InvokeMethod = 0x0100,
    CreateInstance = 0x0200,
    GetField = 0x0400,
    SetField = 0x0800,
    GetProperty = 0x1000,
    SetProperty = 0x2000,

    // These flags are also used by InvokeMember but they should only
    // be used when calling InvokeMember on a COM object.
    PutDispProperty = 0x4000,
    PutRefDispProperty = 0x8000,

    ExactBinding = 0x010000,    // Bind with Exact Type matching, No Change type
    SuppressChangeType = 0x020000,

    // DefaultValueBinding will return the set of methods having ArgCount or 
    //    more parameters.  This is used for default values, etc.
    OptionalParamBinding = 0x040000,

    // These are a couple of misc attributes used
    IgnoreReturn = 0x01000000,  // This is used in COM Interop
    DoNotWrapExceptions = 0x02000000, // Disables wrapping exceptions in TargetInvocationException
}

参考资料

git subtree 的使用

dotnet 职业技术学院 发布于 2019-03-10

本文收集 git subtree 的使用。


将 B 仓库添加为 A 仓库的一个子目录

在 A 仓库的根目录输入命令:

$ git subtree add --prefix=SubFolder/B https://github.com/walterlv/walterlv.git master

这样,B 仓库的整体,会被作为 A 仓库中一个 SubFolder/B 的子文件夹,同时保留 B 仓库中的整个日志记录。

将 A 仓库中的 B 子目录推送回 B 仓库

$ git subtree push --prefix=SubFolder/B https://github.com/walterlv/walterlv.git master

当然,如果你经常需要使用 subtree 命令,还是建议将那个远端设置一个别名,例如设置 walterlv

$ git remote add walterlv https://github.com/walterlv/walterlv.git

那么,上面的命令可以简单一点:

$ git subtree push --prefix=SubFolder/B walterlv master

后面,我们命令都会使用新的远端名称。

将 B 仓库中的新内容拉回 A 仓库的子目录

$ git subtree pull --prefix=SubFolder/B walterlv master

如何使用 MyGet 这个激进的 NuGet 源体验日构建版本的 .NET Standard / .NET Core

dotnet 职业技术学院 发布于 2019-03-10

很多库都会在 nuget.org 上发布预览版本,不过一般来说这个预览版本也是大多可用的。然而想要体验日构建版本,这个就没有了,毕竟要照顾绝大多数开发者嘛……

本文介绍如何使用 MyGet 这个激进的 NuGet 源,介绍如何使用框架级别的库的预览版本如 .NET Standard 的预览版本。


加入 MyGet 这个 NuGet 源

添加 NuGet 源的方法在我和林德熙的博客中都有说明:

简单点,就是在 Visual Studio 中打开 工具 -> 选项 -> NuGet 包管理器 -> 包源

管理包源

然后把 MyGet 的源添加进去:

如果你想添加其他的 NuGet 源,可以参见我的另一篇博客:我收集的各种公有 NuGet 源 - 吕毅

使用 .NET Standard 的预览版本

因为我们在使用 .NET Standard 库的时候,是直接作为目标框架来选择的,就像下面的项目文件内容一样:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  
</Project>

然而,如果你直接把 TargetFramework 中的值改为预览版本,是无法使用的。因为 TargetFramework 的匹配是按照字符串来匹配的,并不会解析成库和版本号。关于这一点可以如何得知的,可以参考我的另一篇博客(中英双语):

然而实际上的使用方法很简单,就是直接用正常的方法安装对应的 NuGet 包:

PM> Install-Package NETStandard.Library -Version 2.1.0-preview1-27119-01

或者直接去 csproj 中添加 PackageReference

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="NETStandard.Library" Version="2.1.0-preview1-27119-01" />
  </ItemGroup>
  
</Project>

至于版本号如何确定,请直接前往 MyGet 网站查看:dotnet-core - NETStandard.Library - MyGet

这个时候,.NET Standard 的预览版标准库会使用以替换 .NET Standard 2.0 的正式版本库。