dotnet 职业技术学院

博客

dotnet 职业技术学院

Roslyn/MSBuild 在编译期间从当前文件开始查找父级文件夹,直到找到包含特定文件的文件夹

dotnet 职业技术学院 发布于 2019-05-15

大家在进行各种开发的时候,往往都不是写一个单纯项目就完了的,通常都会有一个解决方案,里面包含了多个项目甚至是大量的项目。我们经常会考虑输出一些文件或者处理一些文件,例如主项目的输出目录一般会选在仓库的根目录,文档文件夹一般会选在仓库的根目录。

然而,我们希望输出到这些目录或者读取这些目录的项目往往在很深的代码文件夹中。如果直接通过 ..\..\.. 来返回仓库根目录非常不安全,你会数不过来的。


现在,我们有了一个好用的 API:GetDirectoryNameOfFileAbove,可以直接找到仓库的根目录,无需再用数不清又容易改出问题的 ..\..\.. 了。

你只需要编写这样的代码,即可查找 Walterlv.DemoSolution.sln 文件所在的文件夹的完全路径了。

<PropertyGroup>
  <WalterlvSolutionRoot>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Walterlv.DemoSolution.sln))</BuildRoot>
</PropertyGroup>

而这段代码所在的文件,可能是这样的目录结构(里面的 Walterlv.DemoProject.csproj 文件):

- D:\walterlv\root
    - \src
        - \Walterlv.DemoProject
            + \Walterlv.DemoProject.csproj
        - \Walterlv.DemoProject2
        + README.md
    - \docs
    - \bin
    + \Walterlv.DemoSolution.sln
        + README.md

这样,我们便可以找到 D:\walterlv\root 文件夹。

另外还有一个 API GetPathOfFileAbove,只传入一个参数,找到文件后,返回文件的完全路径:

<PropertyGroup>
  <WalterlvSolutionRoot>$([MSBuild]::GetPathOfFileAbove(Walterlv.DemoSolution.sln))</BuildRoot>
</PropertyGroup>

最终可以得到 D:\walterlv\root\Walterlv.DemoSolution.sln 路径。

需要注意的是:

  1. 此方法不支持通配符,也就是说不能使用 *.sln 来找路径
  2. 此方法不支持通过文件夹去找,也就是说不能使用我们熟知的 .git 等等文件夹去找路径
  3. 此方法传入的文件支持使用路径,也就是说可以使用类似于 \src\README.md 的方式来查找路径

参考资料

Directory Opus 使用命令编辑器添加 PowerShell / CMD / Bash 等多种终端到自定义菜单

dotnet 职业技术学院 发布于 2019-05-15

使用 Directory Opus 替代 Windows 自带的文件资源管理器来管理你计算机上的文件可以极大地提高你的文件处理效率。

本文将教你如何使用 Directory Opus 的命令编辑器功能编写一组菜单,我们将在这组菜单里面集成各种各样的终端。


命令编辑器

如果你是从下面这篇文章阅读过来的,那么你现在应该正好已经打开了一个命令编辑器:

如果你并没有打开命令编辑器,那么可以再阅读上面这篇文章打开一个。

设置 -> 自定义工具栏新建 -> 新建按钮编辑

添加三个终端命令

请参考 Directory Opus 使用命令编辑器集成 TortoiseGit 的各种功能 一文中添加自定义按钮的方法,同样地添加另外的三个按钮。这里,我将三个不同终端的添加参数放到了下面,你可以参考添加:

PowerShell Core

PowerShell Core

Git Bash

Git Bash

特别注意,在函数一栏的参数中,我们传入了一个路径参数。那个参数的末尾必须加上 \.,否则 Git Bash 是无法启动的。

CMD

CMD

添加一个菜单

在添加完上面的三个命令之后,你应该可以在工具栏上看到三个可以启动不同终端的窗口。现在我们需要将它们都集成到一个菜单中。

新建一个菜单

在工具栏上空白处右键,新建 -> 新建菜单,然后右键,编辑这个菜单:

新建菜单

编辑新建菜单

然后,我们又可以弹出一个命令编辑器窗口,由于菜单本身不打开命令只会显示子菜单,所以里面非常简单。设置图标和显示的文字即可。

菜单的命令编辑窗口

然后,依然保持在工具栏的编辑状态,将我们前面创建的三个按钮依次拖入菜单中即可形成一个菜单:

拖入到菜单中

新建一个菜单按钮

在工具栏上空白处右键,新建 -> 新建菜单按钮,这样的菜单除了显示子项之外,还可以执行命令。然后右键,编辑这个菜单:

新建菜单按钮

编辑新建菜单按钮

然后,我们又可以弹出一个命令编辑器窗口,如果我们不打算让这个菜单按钮额外具备一些功能,则值设置图标和文字即可。

菜单按钮的命令编辑窗口

当然,我更期望在这里将你希望默认打开的终端参数设进去,比较方便一些。

然后,依然保持在工具栏的编辑状态,将我们前面创建的三个按钮依次拖入菜单中即可形成一个菜单:

拖入到菜单按钮中

后续

关于命令设置的详细细节,可以继续阅读我的另一篇博客:

最后,在自定义完按钮之后,不要忘了关闭最开始弹出来的“自定义工具栏”的对话框。

“自定义工具栏”对话框

C# 8.0 中开启默认接口实现

dotnet 职业技术学院 发布于 2019-05-15

当你升级到 C# 8.0 和 .NET Core 3.0 之后,你就可以开始使用默认接口实现的功能了。

从现在开始,你可以在接口里面添加一些默认实现的成员,避免在接口中添加成员导致大量对此接口的实现崩溃。


最低要求

要写出并且正常使用接口的默认实现,你需要:

  • C# 8.0
  • .NET Core 3.0
  • Visual Studio 2019 Preview (16.1 以上版本)

下载安装 Visual Studio 2019 Preview 版本

开启 .NET Core 3.0 的支持

对于预览版的 Visual Studio 2019 来说,.NET Core 的预览版是默认打开且无法关闭的,所以不需要关心。

开启 C# 8.0 支持

请设置你项目的属性,修改 C# 语言版本为 8.0(对于预览版的语言来说,这是必要的):

修改语言版本

或者直接修改你的项目文件,加上 LangVersion 属性的设置,设置为 8.0

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

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

</Project>

默认接口实现

以前的做法

比如,我们现在有下面这样一个简单的接口:

public interface IWalterlv
{
    void Print(string text);
}

这个接口被大量实现了。

现在,我们需要在接口中新增一个方法 DouBPrint,其作用是对 Print 方法进行标准化,避免各种不同实现带来的标准差异。于是我们新增一个方法:

    public interface IWalterlv
    {
        void Print(string text);

++      void DouBPrint(string text);
    }

然而我们都知道,这样的修改是破坏性的:

  1. 会使得所有实现这个接口的代码全部失败(无法编译通过,或者运行时抛出异常)
  2. 我们依然很难将接口的实现标准化,靠文档来规约

默认接口实现

那么现在,我们可以这样来新增此方法:

    public interface IWalterlv
    {
        void Print(string text);
        
--      void DouBPrint(string text);
++      public void DouBPrint(string text) => Print($"Walterlv 逗比 {text}");
    }

在使用此方法来定义此接口中的方法后,那些没来得及实现此方法的类型也可以编译通过并获得标准化的实现。

class Program
{
    static void Main(string[] args)
    {
        IWalterlv walterlv = new Foo();
        walterlv.DouBPrint("walterlv");
    }
}

public class Foo : IWalterlv
{
    public void Print(string text)
    {
    }
}

当然,对于 Foo 类型来说,实现也是可以的:


public class Foo : IWalterlv
{
    public void Print(string text)
    {
    }

    public void DouBPrint(string text) => Print($"Walterlv 逗比 {text}");
}

静态字段和方法

除此之外,在接口中还可以编写静态字段和静态方法,这可以用来统一接口中的一些默认实现。

意味着,如果类没有实现接口中带有默认实现的方法,那么具有默认的实现;而如果类中打算实现接口中的带有默认实现的方法,那么也可以调用接口中的静态方法来进行实现。

    public interface IWalterlv
    {
        void Print(string text);

--      public void DouBPrint(string text) => Print($"Walterlv 逗比 {text}");
++      public void DouBPrint(string text) => DefaultDouBPrint(this, text);
++
++      private static readonly string _name = "walterlv";
++
++      protected static void DefaultDouBPrint(IWalterlv walterlv, string text)
++          => walterlv.Print($"{_name} 逗比 {text}");
    }

然后,对于实现方,则需要使用接口名来调用接口中的静态成员:

    public class Foo : IWalterlv
    {
        public void Print(string text)
        {
        }

--      public void DouBPrint(string text) => Print($"Walterlv 逗比 {text}");
++      public void DouBPrint(string text)
++      {
++          // Do Other things.
++          IWalterlv.DefaultDouBPrint(this, text);
++      }
++  }

参考资料

Directory Opus 使用命令编辑器集成 TortoiseGit 的各种功能

dotnet 职业技术学院 发布于 2019-05-15

使用 Directory Opus 替代 Windows 自带的文件资源管理器来管理你计算机上的文件可以极大地提高你的文件处理效率。

本文将教你如何使用 Directory Opus 的命令编辑器功能创建一个命令——跟 TortoiseGit 进行集成。


命令编辑器

如果你是从下面这篇文章阅读过来的,那么你现在应该正好已经打开了一个命令编辑器:

如果你并没有打开命令编辑器,那么可以再阅读上面这篇文章打开一个。

设置 -> 自定义工具栏新建 -> 新建按钮编辑

寻找命令

我在 Windows 系统上使用任务管理器查看进程的各项属性 一文中告诉大家可以在任务管理器中查看某个正在运行中的进程的命令行参数,于是我们可以通过这样的方式得知如何集成 TortoiseGit 的各项功能。

比如,我们在一个文件夹中从文件资源管理器中右键,选择 Git 克隆...,等待打开一个 TortoiseGit 的克隆窗口。

TortoiseGit 的 Git 克隆菜单项

这时,我们去任务管理器中查看此任务的命令行参数:

TortoiseGit 克隆的命令行参数

> TortoiseGitProc.exe /command:clone /path:"D:\walterlv" /hwnd:0000000000161264

那么接下来,我们将这些信息逐一填入到命令编辑器窗格中。

填写命令

命令编辑器

函数

函数一栏,如果你只是简单地希望启动一个程序传入参数的话,那么称之为“启动程序”可能更合适一些。

但是,我依然倾向于在后面继续保持“函数”的称呼,因为这才能体现出 Directory Opus 自定义按钮命令的强大。所以如果你后面看到我提及“函数”,那么指的就是这里的功能。

Directory Opus 相比于 Total Commander 的一大特点便是其鼠标支持,这在“函数”一栏的填写中也有所体现。你可以在函数一栏的最右侧看到一个文件夹图标,点击之后可以选择你想启动的程序。

现在,我们通过这个按钮找到 TortoiseGitProc.exe 程序,于是我们就可以在“函数”一栏中自动填入 TortoiseGitProc.exe 的程序路径。

我们在任务管理器中看到了应该给 TortoiseGitProc.exe 传入的参数,所以我们直接在此文本框的后面继续添加参数。添加后的整个文本框中的内容应该是下面这样的:

"C:\Program Files\TortoiseGit\bin\TortoiseGitProc.exe" /command:clone /path:"{sourcepath}"

这里出现了一个 {sourcepaht},这是一个表示当前路径的变量,稍后会作详细的说明。

图标、说明、显示说明、提示信息

我们在“函数”一栏中添加了一个可以启动的程序之后,Directory Opus 的命令编辑窗口会自动帮我们从主程序中获取一个可以显示的图标。

选择了程序之后,出现了图标

点击一下这个图标,可以选择此程序的其他图标。我们现在正在做的是一个 Git Clone 的按钮,所以我们选择一个表示克隆仓库的图标:

选择图标

接着,我们需要进行一些基础的设置:

  • 图标:将显示大图标打勾,可以使用更大更清晰的图标,这对于我这种 UI 党来说会更加友好。
  • 说明:这是最终会出现在按钮上的文字。我填写了“Git 克隆…”,后面的三个点在 Windows 系统中是一种交互惯例,表示点击后还需要用户给出额外的信息才能完成指定的任务。
  • 显示说明:说明文字会出现在图标的哪个方向。我选择了右侧,这跟 Directory Opus 上的多数已有工具栏是保持一致的。
  • 提示信息:上你把鼠标移动到按钮上的时候,将显示的工具提示说明。可以使用比较长的一段话清晰地说明这个按钮是干什么用的。

设置了基础属性的命令

开始于

开始于,指的是点击此按钮运行我们指定的“函数”时,如果函数打开了一个进程,那么此进程的工作路径是什么。

我们先填入 {sourcepath}。这里,我们再次使用了 {sourcepath} 这个变量。稍后会进行说明。

实际上到此为止,如果你按下“确定”按钮,你将在工具栏上看见一个“Git 克隆…”按钮。

“Git 克隆...”按钮

高级

如果你没有关闭此窗口,那么点击“高级…”,我们将打开高级的命令编辑器。现在我们可以注意到下面出现了一个非常大的函数编辑窗口,而此前的“函数”、“开始于”、“运行”选项都消失了。这是因为此函数编辑窗口涵盖了消失的这些按钮的所有功能,而且更为强大,因为可以使用更多种类的命令。

高级命令编辑器

函数类型

留意命令编辑器中的命令,我们此前用鼠标点击的操作实际上对应了两行命令:

cd {sourcepath}
C:\Program Files\TortoiseGit\bin\TortoiseGitProc.exe /command:clone /path:"{sourcepath}"

第一行是将路径转换到 {sourcepath} 变量所指示的路径中,第二行是启动一个程序并传入适当的参数。

而这个参数是什么意思呢?如何可以输入呢?

请点击命令编辑器上面的“参数”按钮,这时会弹出一个菜单,对各种各样可以输入的参数放在一起进行了分类存放。

因为我们要克隆 Git 仓库需要现在 Directory Opus 里面先进入一个文件夹,然后将 Git 仓库克隆到此仓库中,所以我们实际上是希望拿到 Directory Opus 当前正在浏览的文件夹。

{sourcepath} 表示正在操作的源路径,而正在操作的源路径就是 Directory Opus 的当前文件夹(如果你有多个文件夹窗格,则是当前激活的那个窗口所在的文件夹)。所以我们选用了此参数。

Directory Opus 可以输入的参数

最后一步

在自定义完按钮之后,不要忘了关闭最开始弹出来的“自定义工具栏”的对话框。

“自定义工具栏”对话框

在 Directory Opus 中添加自定义的工具栏按钮提升效率

dotnet 职业技术学院 发布于 2019-05-15

使用 Directory Opus 替代 Windows 自带的文件资源管理器来管理你计算机上的文件可以极大地提高你的文件处理效率。

Directory Opus 自定义的工具栏按钮可以执行非常复杂的命令,所以充分利用自定义工具栏按钮的功能可以更大程度上提升工作效率。


Directory Opus 的工具栏

这是我的 Directory Opus 的界面(暂时将左侧的树关掉了):

Directory Opus

下图是我目前添加的一些工具栏按钮:

Directory Opus 的工具栏按钮

自定义工具栏按钮

自定义的方法是,点击顶部的 设置 -> 自定义工具栏

自定义工具栏菜单

这时,会弹出自定义工具栏的对话框,并且所有可以被定制的工具栏现在都会进入编辑状态等待着我们对其进行编辑:

正在自定义工具栏

添加一个自定义按钮

你并不需要在自定义工具栏对话框上进行任何操作,只需要在一个现有的工具栏上点击右键,然后点击 新建 -> 新建按钮

新建按钮

这时,你会看到一个新的按钮已经出现在了工具栏上:

新建的按钮

现在,在此按钮上点击右键,“编辑”,就打开了 Directory Opus 的命令编辑器:

命令编辑器

接下来,我们的操作就进入了本文的主要内容,也是最复杂的一部分内容了。

命令编辑器

要定义一个能够极大提升效率的按钮,命令编辑器中的多数框我们都是要使用的。

接下来我会通过两个示例来说明如何使用这个命令编辑器。

  1. Directory Opus 使用命令编辑器集成 TortoiseGit 的各种功能
  2. Directory Opus 使用命令编辑器添加 PowerShell / CMD / Bash 等多种终端到自定义菜单

在自定义完按钮之后,不要忘了关闭最开始弹出来的“自定义工具栏”的对话框。

一切皆命令

在阅读上面的博客定义完一些自己的命令之后,你再观察 Directory Opus 的其他工具栏按钮,包括左上角的菜单,你会发现其实 Directory Opus 中所有的功能按钮和菜单都是使用相同的机制建立起来的。

一切皆命令。

这些命令组成了 Directory Opus 主界面的绝大多数功能。

让 Directory Opus 支持 Windows 10 的暗色主题

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

使用 Directory Opus 替代 Windows 自带的文件资源管理器来管理你计算机上的文件可以极大地提高你的文件处理效率。

由于我自己的 Windows 10 系统使用的是暗色主题,所以我希望 Directory Opus 也能搭配我系统的纯暗色主题。

本文介绍如何将 Directory Opus 打造成搭配 Windows 10 的暗色主题。


Directory Opus 主题支持

Directory Opus 在安装完之后的默认主题样式是下面这样的:

Directory Opus 默认主题

然而,我的 Windows 10 的主要界面都是暗黑色的:

Windows 10 中的主题

那么,请在 Directory Opus 顶部菜单中选择 设置 -> 主题

设置 -> 主题

然后点击左下角的下载主题去网上下载一款主题。

主题选择页面

Windows 10 暗色风格的主题

你可以直接使用下面的链接下载 Windows 10 暗色风格的主题:

然后,依然进入我们一开始说的 设置 -> 主题 对话框中,导入刚刚我们下载好的主题:

导入主题

点击“应用”,随后 Directory Opus 会重新启动,你将看到全新的 Windows 10 暗色风格主题。

微调主题样式

等等!为什么重启之后看起来样式怪怪的?有一些文件的文字其实在暗色主题下看不太清。

这当然是主题设计者没有考虑到所有的情况导致的,实际上你下载的任何一款主题可能都有各种考虑不周的情况,那么如何修复这些考虑不周的细节呢?

我们需要前往 设置 -> 选项 中微调这些细节。在“选项”对话框中,选择“颜色和字体”标签。

设置 -> 选项

颜色和字体

微调文件组标题

在我一开始的暗色主题应用后,我们注意到我的文件是分组的,组标题是深蓝色,看不清。于是修改“文件组标题”中的颜色:

文件组标题

微调压缩的文件和文件夹

另外,我的多数文件是加入了 NTFS 压缩的,这部分文件被主题设置了很难看清的深紫色,我将它改为其他的颜色:

文件和文件夹

微调其他部件

里面还有大量可以微调的部件,如果你遇到了不符合你要求的颜色设置,则将其修改即可。

以下是我进行了微调之后的主题效果预览:

修改后的主题效果

还原成默认的主题

你可能会注意到在主题选择窗格中只有我们刚刚下载的那一个主题,我们不能选择回默认的主题样式。那如果一个主题被我们改残了,或者就是想重新体验原生效果的时候该如何做呢?

我们依然需要进入到 设置 -> 选项 中,然后选择“颜色和字体”标签。

这时,选择顶部的 文件 -> 重置该页到默认值。于是,我们的主题就会还原到最初没有修改任何字体和颜色的版本。

重置主题的设置

如果主题涉及到图标等其他资源,也需要进入对应的标签页然后还原对应标签页的设置。


参考资料

通过分析 WPF 的渲染脏区优化渲染性能

dotnet 职业技术学院 发布于 2019-05-13

本文介绍通过发现渲染脏区来提高渲染性能。


脏区 Dirty Region

在计算机图形渲染中,可以每一帧绘制全部的画面,但这样对计算机的性能要求非常高。

脏区(Dirty Region)的引入便是为了降低渲染对计算机性能的要求。每一帧绘制的时候,仅仅绘制改变的部分,在软件中可以节省大量的渲染资源。而每一帧渲染时,改变了需要重绘的部分就是脏区。

以下是我的一款 WPF 程序 Walterlv.CloudKeyboard 随着交互的进行不断需要重绘的脏区。

较多的脏区

可以看到,脏区几乎涉及到整个界面,而且刷新非常频繁。这显然对渲染性能而言是不利的。

当然这个程序很小,就算一直全部重新渲染性能也是可以接受的。不过当程序中存在比较复杂的部分,如大量的 Geometry 以及 3D 图形的时候,重新渲染这一部分将带来严重的性能问题。

WPF 性能套件

先下载 WPF 性能套件:

脏区监视

启动 WPF Performance Suite,选择工具 Perforator,然后在 Action 菜单中启动一个待分析的 WPF 进程。虽然工具很久没有更新,但依然可以支持基于 .NET Core 3 版本的 WPF 程序。

启动一个进程

当程序运行起来后,可以看到 WPF 程序的各种性能数据图表。

WPF 性能收集工具

现在将 Show dirty-region update overlay 选项打勾即可看到本文一开始的脏区叠加层的显示。

与脏区有关的选项有三个:

  • Show dirty-region update overlay
    • 显示脏区叠加层,每一次脏区出现需要重新渲染时会叠加一层新的半透明颜色。
  • Disable dirty region support
    • 禁用脏区支持。这时,每次渲染都将重绘整个窗口。
  • Clear back-buffer before rendering
    • 每次重绘之前都将清除之前所有的绘制,使用此选项,你可以迅速找到界面中频繁刷新的部分,而重绘频率不高的部分多数时候都是纯黑。

优化脏区重绘

一开始的程序中,因为我使用了模拟 UWP 的高光效果,导致大量的控件在重绘高光部分,这是导致每一帧都在重新渲染的罪魁祸首。

于是我将高光渲染关闭,脏区的重新渲染将仅仅几种在控件样式改变的时候(例如焦点改变):

稍微正常一点的脏区

光照效果可以参见我的另一篇博客:


参考资料

Roslyn/MSBuild 在编译期间处理路径中的斜杠与反斜杠

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

本文介绍如何在项目文件 csproj,或者 MSBuild 的其他文件(props、targets)中处理路径中的斜杠与反斜杠。


路径中的斜杠与反斜杠

我们都知道文件路径的层级之间使用斜杠(/)或者反斜杠(\)来分隔,具体使用哪一个取决于操作系统。本文不打算对具体使用哪一种特别说明,不过示例都是使用 Windows 操作系统中的反斜杠(\)。

对于一个文件夹的路径,末尾无论是否有反斜杠都不会影响找到这个路径对应的文件夹,但是有时我们又因为一些特殊的用途需要知道末尾的反斜杠的情况。

在 MSBuild 中,通常有一个在文件夹路径末尾添加反斜杠 \ 的惯例,这样可以直接使用属性拼接来形成新的路径而不用担心路径中的不同层级的文件夹会连接在一起。

例如属性 WalterlvPath1 的值为 bin,属性 WalterlvPath2 的值为 Debug。为了确保两个可以直接使用 $(WalterlvPath1)$(WalterlvPath2) 来拼接,我们需要在这两个属性的末尾都加上反斜杠 \。不过由于需要照顾到各式各样的开发者,包括大多数的那些从来不看文档的开发者,我们需要进行本文所述的处理。

判断路径末尾是否有斜杠或反斜杠

如果路径末尾没有反斜杠,那么我们现在就添加一个反斜杠。

<WalterlvPath Condition="!HasTrailingSlash('$(WalterlvPath)')">$(WalterlvPath)\</WalterlvPath>

这样,如果 WalterlvPath 的值为 bin,则会在这一个属性重新计算值的时候变成 bin\;如果已经是 bin\,则不会重新计算值,于是保持不变。

确保路径末尾有斜杠或反斜杠

另外,也有方法可以不用做判断,直接给末尾根据情况加上反斜杠。

通过调用 MSBuild.EnsureTrailingSlash 可以确保路径的末尾已经有一个斜杠或者反斜杠。

例如,我们有一个 WalterlvPath 属性,值可能是 bin\Debug 也有可能是 bin\Debug\,那么可以统一将其处理成 bin\Debug\

<WalterlvPath>$([MSBuild]::EnsureTrailingSlash('$(WalterlvPath)'))</WalterlvPath>

确保路径末尾没有斜杠或反斜杠

正常情况下,我们都是需要 MSBuild 中文件夹路径的末尾有斜杠或者反斜杠。不过,当我们需要将这个路径作为命令行参数的一部分传给一个可执行程序的时候,就没那么容易了。

因为为了确保路径中间的空格不会被命令行参数解析给分离,我们需要在路径的周围加上引号。具体来说,是使用 &quot; 转义字符来添加引号:

<Target Name="WalterlvDemoTarget" BeforeTargets="BeforeBuild">
    <Exec Command="&quot;$(WalterlvDemoTool)&quot; --option &quot;$(WalterlvPath)&quot;" />
</Target>

以上的 Target 是我在另一篇博客中的简化版本:如何创建一个基于命令行工具的跨平台的 NuGet 工具包 - walterlv

但是这样,如果 WalterlvPath 中存在反斜杠,那么这个命令行将变成这样:

> "walterlv.tool.exe" --option "bin\"

后面的 \" 将使得引号成为路径中的一部分,而这样的路径是不合法的路径!

我们可以确保路径的末尾添加一个空格来避免将引号也解析成命令行的一部分:

<Target Name="WalterlvDemoTarget" BeforeTargets="BeforeBuild">
    <Exec Command="&quot;$(WalterlvDemoTool)&quot; --option &quot;$([MSBuild]::EnsureTrailingSlash('$(BasePathInInstaller)')) &quot;" />
</Target>

不过也可以通过 SubString 来对末尾的斜杠或反斜杠进行裁剪。

<WalterlvPath Condition="HasTrailingSlash('$(WalterlvPath)')">$(WalterlvPath.Substring(0, $([MSBuild]::Add($(WalterlvPath.Length), -1))))</WalterlvPath>

解释一下这里 $(WalterlvPath.Substring(0, $([MSBuild]::Add($(WalterlvPath.Length), -1)))) 所做的事情:

  1. $(WalterlvPath.Length) 计算出 WalterlvPath 属性的长度;
  2. $([MSBuild]::Add(length, -1)) 调用加法,将前面计算所得的长度 -1,用于提取无斜杠或反斜杠的路径长度。
  3. $(WalterlvPath.Substring(0, length-1) 将路径字符串取出子串。

这里的解释里面,length 只是表意,并不是为了编译通过。要编译的代码还是上面代码块中的完整代码。

更多关于在 Roslyn/MSBuild 中进行数学运算的内容,可以阅读我的另一篇博客:

在 Roslyn/MSBuild 中进行基本的数学运算

dotnet 职业技术学院 发布于 2019-05-11

在任何一种编程语言中,做基本的数学运算都是非常容易的事情。不过,不知道 .NET 项目的项目文件 csproj 文件中进行数学运算就不像一般的编程语言那样直观了,毕竟这不是一门语言,而只是一种项目文件格式而已。

本文介绍如何在 Roslyn/MSBuild 的项目文件中使用基本的数学运算。


Roslyn/MSBuild 中的数学运算

在 MSBuild 中,数学运算需要使用 MSBuild 内建的方法调用来实现。

你只需要给 MSBuild 中那些数学计算方法中传入看起来像是数字的属性,就可以真的计算出数字出来。

加减乘除模

  • Add 两个数相加,实现 a + b
  • Subtract 第一个数减去第二个数,实现 a - b
  • Multiply 两个数相乘,实现 a * b
  • Divide 第一个数除以第二个数,实现 a / b
  • Modulo 第一个数与第二个数取模,实现 a % b

而具体到 MSBuild 中的使用,则是这样的:

<!-- 计算 5 - 1 的数学运算结果 -->
<Walterlv>$([MSBuild]::Subtract(5, 1))</Walterlv>
<!-- 取出 Walterlv 属性的字符串值,然后计算其长度减去 1,将数学运算结果存入 Walterlv2 属性中 -->
<Walterlv>walterlv is a 逗比</Walterlv>
<Walterlv2>$([MSBuild]::Subtract($(Walterlv.Length), 1))</Walterlv2>

不要试图在 MSBuild 中使用传统的数学运算符号

不同于一般编程语言可以写的 + - * /,如果你直接在项目文件中使用这样的符号来进行数学计算,要么你将得到一个数学运算的字符串,要么你将得到编译错误。

例如,如果你在你的项目文件中写了下面这样的代码,那么无一例外全部不能得到正确的数学运算结果。

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>

    <!-- 这个属性将得到一个 “1 + 1” 字符串 -->
    <Walterlv>1 + 1</Walterlv>

    <!-- 无法编译此属性 -->
    <!-- 无法计算表达式“"1 + 1".Length + 1”。未找到方法“System.String.Length + 1” -->
    <Walterlv2>$(Walterlv.Length + 1)</Walterlv2>

    <!-- 这个属性将得到一个 “5 + 1” 字符串 -->
    <Walterlv3>$(Walterlv.Length) + 1</Walterlv3>

  </PropertyGroup>

</Project>

参考资料

使用 DISM 工具检查并修复 Windows 系统文件

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

DISM,Deployment Image Servicing and Management,部署映像服务和管理。本文介绍使用此工具检查并修复 Windows 的系统文件。


系统要求

Windows 8/8.1 和 Windows 10 开始提供 DISM 工具。

相比于我在另一篇博客中提及的 sfc,DISM 利用 Windows 系统镜像来完成修复,所以更容易修复成功。关于 sfc(System File Check)可以参见:

使用方法

使用管理员权限启动 CMD,然后输入命令:

DISM.exe /Online /Cleanup-image /Restorehealth

运行后等待其运行完成。

DISM 修复系统的命令

使用本地镜像

上面的命令依赖于 Windows Update 服务来获取在线的镜像进行恢复。如果 Windows Update 服务已经挂了,那么这个命令是无法正常完成的。

这时需要额外添加 /Source: 来指定修复所使用的本地文件:

DISM.exe /Online /Cleanup-Image /RestoreHealth /Source:C:\RepairSource\Windows /LimitAccess

C:\RepairSource\Windows 需要换成自己的本地镜像路径。


参考资料

使用 System File Check (SFC) 工具检查并修复 Windows 系统文件

dotnet 职业技术学院 发布于 2019-05-09

sfc.exe 这个程序的名称指的是 System File Check,用于做系统文件检查。本文介绍使用此命令检查并修复 Windows 系统文件。


系统要求

Windows Vista 及以上的操作系统才具有 sfc.exe 工具。 相比于 Windows 7 开始提供 dism 工具。

当然,虽然系统要求如此,但如果你使用的是 Windows 8/8.1 或者 Windows 10,那么便建议使用 DISM。可以阅读:

使用方法

使用管理员权限启动 CMD,然后输入命令:

sfc /scannow

接下来等待命令执行完成即可。

sfc /scannow

命令结果

如果以上命令可以正常完成,那么你可能会遇到三种不同的提示(以下为中英双语版本)

  • Windows Resource Protection did not find any integrity violations.
    • Windows 资源保护找不到任何完整性冲突。
  • Windows Resource Protection could not perform the requested operation.
    • Windows 资源保护无法执行请求的操作。
  • Windows Resource Protection found corrupt files and successfully repaired them. Details are included in the CBS.Log %WinDir%\Logs\CBS\CBS.log.
    • Windows 资源保护找到了损坏的文件并已成功将其修复。 详细信息包含在 CBS.Log(路径为 %WinDir%\Logs\CBS\CBS.log)中。
  • Windows Resource Protection found corrupt files but was unable to fix some of them. Details are included in the CBS.Log %WinDir%\Logs\CBS\CBS.log.
    • Windows 资源保护找到了损坏的文件但无法修复其中的某些文件。 详细信息包含在 CBS.Log(路径为 %WinDir%\Logs\CBS\CBS.log)中。

出现第一种提示,则说明没有任何丢失或损坏的系统文件。如果系统存在其他问题,则需要找其他方法来修复。

出现第二种提示,你需要确保 %WinDir%\WinSxS\Temp 下存在 PendingDeletes 和 PendingRenames 文件夹;然后去安全模式中重新尝试此命令。

出现第三种提示,则已经修复了损坏的文件。

而出现第四种提示的话,你可以多次尝试执行此命令。可能多次执行后逐渐修复了所有的文件,也可能毫无作用。这个时候需要考虑其他的方法来修复系统了。

此工具的其他命令

可以只做检查而不用尝试修复。

sfc /verifyonly

参考资料

Visual Studio 使用 Parallel Builds Monitor 插件迅速找出编译速度慢的瓶颈,优化编译速度

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

嫌项目编译太慢?不一定是 Visual Studio 的问题,有可能是你项目的引用关系决定这个编译时间真的省不下来。

可是,编译瓶颈在哪里呢?本文介绍 Parallel Builds Monitor 插件,帮助你迅速找出编译瓶颈。


下载安装 Parallel Builds Monitor

前往 Parallel Builds Monitor - Visual Studio Marketplace 下载插件安装。

之后启动 Visual Studio 2019,你就能在 “其他窗口” 中找到 “Parallel Builds Monitor” 窗口了。请点击打开它。

编译项目

现在,使用 Visual Studio 编译一个项目,点开这个窗口,一个正在进行中的甘特图将呈现出来:

并行编译窗口

寻找瓶颈

我们可以通过此插件寻找到多种可能的瓶颈:

  1. 项目依赖瓶颈
  2. CPU 瓶颈
  3. IO 瓶颈

项目依赖瓶颈

看上面的那张图,这里存在典型的项目依赖瓶颈。因为在编译的中后期,几个编译时间最长的项目,其编译过程完全是串联起来编译的。

这里串联起来的每一个项目,都是依赖于前一个项目的。所以要解决掉这部分的性能瓶颈,我们需要断开这几个项目之间的依赖关系,这样它们能变成并行的编译。

CPU 瓶颈

通常,CPU 成为瓶颈在编译中是个好事情,这意味着无关不必要的编译过程非常少,主要耗时都在编译代码的部分。当然,如果你有一些自定义的编译过程浪费了 CPU 占用那是另外一回事。

比如我之前写过自己可以做一个工具包,在编译期间会执行一些代码:

IO 瓶颈

IO 本不应该成为瓶颈。如果你的项目就是存在非常多的依赖文件需要拷贝,那么应该尽可能利用差量编译来避免重复拷贝文件。


参考资料

使用 EnumWindows 找到满足你要求的窗口

dotnet 职业技术学院 发布于 2019-04-30

在 Windows 应用开发中,如果需要操作其他的窗口,那么可以使用 EnumWindows 这个 API 来枚举这些窗口。

本文介绍使用 EnumWindows 来枚举并找到自己关心的窗口(如 QQ/TIM 窗口)。


EnumWindows

你可以在微软官网了解到 EnumWindows

要在 C# 代码中使用 EnumWindows,你需要编写平台调用 P/Invoke 代码。使用我在另一篇博客中的方法可以自动生成这样的平台调用代码:

我这里直接贴出来:

[DllImport("user32.dll")]
public static extern int EnumWindows(WndEnumProc lpEnumFunc, int lParam);

遍历所有的顶层窗口

官方文档对此 API 的描述是:

Enumerates all top-level windows on the screen by passing the handle to each window, in turn, to an application-defined callback function.

遍历屏幕上所有的顶层窗口,然后给回调函数传入每个遍历窗口的句柄。

不过,并不是所有遍历的窗口都是顶层窗口,有一些非顶级系统窗口也会遍历到,详见:EnumWindows 中的备注节

所以,如果需要遍历得到所有窗口的集合,那么可以使用如下代码:

public static IReadOnlyList<int> EnumWindows()
{
    var windowList = new List<int>();
    EnumWindows(OnWindowEnum, 0);
    return windowList;

    bool OnWindowEnum(int hwnd, int lparam)
    {
        // 可自行加入一些过滤条件。
        windowList.Add(hwnd);
        return true;
    }
}

遍历具有指定类名或者标题的窗口

我们需要添加一些可以用于过滤窗口的 Win32 API。以下是我们即将用到的两个:

// 获取窗口的类名。
[DllImport("user32.dll")]
private static extern int GetClassName(int hWnd, StringBuilder lpString, int nMaxCount);

// 获取窗口的标题。
[DllImport("user32")]
public static extern int GetWindowText(int hwnd, StringBuilder lptrString, int nMaxCount);

于是根据类名找到窗口的方法:

public static IReadOnlyList<int> FindWindowByClassName(string className)
{
    var windowList = new List<int>();
    EnumWindows(OnWindowEnum, 0);
    return windowList;

    bool OnWindowEnum(int hwnd, int lparam)
    {
        var lpString = new StringBuilder(512);
        GetClassName(hwnd, lpString, lpString.Capacity);
        if (lpString.ToString().Equals(className, StringComparison.InvariantCultureIgnoreCase))
        {
            windowList.Add(hwnd);
        }

        return true;
    }
}

使用此方法,我们可以传入 "txguifoundation" 找到 QQ/TIM 的窗口:

var qqHwnd = FindWindowByClassName("txguifoundation");

要获取窗口的标题,或者把标题作为过滤条件,则使用 GetWindowText

在 QQ/TIM 中,窗口的标题是聊天对方的名字或者群聊名称。

var lptrString = new StringBuilder(512);
GetWindowText(hwnd, lptrString, lptrString.Capacity);

参考资料

安装和运行 .NET Core 版本的 PowerShell

dotnet 职业技术学院 发布于 2019-04-30

Windows 自带一个强大的 PowerShell,不过自带的 PowerShell 一直是基于 .NET Framework 的版本。你可以下载安装一个 .NET Core 版本的 PowerShell,以便获得 .NET Core 的各种好处。包括跨平台,以及更好的性能。

本文将介绍在你的 Windows 系统上安装一个 .NET Core 版本的 PowerShell。


PowerShell Core 的图标

下载和安装

前往 .NET Core 版本 PowerShell 的发布页面来下载 PowerShell 全平台的安装包:

Windows 平台上建议下载 msi 格式的安装包,这样它可以帮助你完成大多数的安装任务。

PowerShell 安装界面

PowerShell 安装配置

运行

在安装完成之后启动新的 .NET Core 版本的 PowerShell 可以看见新的 PowerShell。

.NET Core 版本的 PowerShell

在任何一个文件夹中右键可打开 PowerShell 或者以管理员权限打开 PowerShell。这与自带的 PowerShell 的玩法是类似的。

使用右键菜单打开 PowerShell

在其他终端使用 PowerShell Core

如果你要在其他的终端使用 PowerShell Core,直接输入 pwsh 即可。其原理可以参考我的另一篇博客:

在 cmd 中启动 PowerShell Core

XAML 很少人知道的科技

dotnet 职业技术学院 发布于 2019-04-30

本文介绍不那么常见的 XAML 相关的知识。


Thickness 可以用空格分隔

当你用设计器修改元素的 Margin 时,你会看到用逗号分隔的 Thickness 属性。使用设计器或者属性面板时,使用逗号是默认的行为。

不过你有试过,使用空格分隔吗?

<Button Margin="10 12 0 0" />

使用逗号(,)设置多值枚举

有一些枚举标记了 [Flags] 特性,这样的枚举可以通过位运算设置多个值。

[Flags]
enum NonClientFrameEdges
{
    // 省略枚举内的值。
}

那么在 XAML 里面如何设置多个枚举值呢?使用逗号(,)即可,如下面的例子:

<WindowChrome NonClientFrameEdges="Left,Bottom,Right" GlassFrameThickness="0 64 0 0" UseAeroCaptionButtons="False" />

使用加号(+)设置多值枚举

使用逗号(,) 设置多值枚举是通用的写法,但是在 WPF/UWP 中设置按键/键盘快捷键的时候又有加号(+)的写法。如下面的例子:

<KeyBinding Command="{x:Static WalterlvCommands.Foo}" Modifiers="Control+Shift" Key="W" />

这里的 Modifiers 属性的类型是 ModifierKeys,实际上是因为这个类型特殊地编写了一个 TypeConverter 来转换字符串,所以键盘快捷键多值枚举使用的位或运算用的是加号(+)。

设置 Url 型的 XAML 命名空间(xmlns)

WPF/UWP 中原生控件的 XAML 命名空间是 http://schemas.microsoft.com/winfx/2006/xaml/presentation,与 XAML 编译器相关的 XAML 命名空间是 http://schemas.microsoft.com/winfx/2006/xaml,还有其他 Url 形式的 XAML 命名空间。

只需要在库中写如下特性(Attribute)即可将命名空间指定为一个 url:

using System.Windows.Markup;
[assembly: XmlnsDefinition("http://walterlv.github.io/demo", "Walterlv.NewCsprojDemo")]

详情请阅读博客:

此写法要生效,定义的组件与使用的组件不能在同一程序集。

设置默认的 XAML 命名空间前缀

WPF/UWP XAML 编译器的命名空间前缀是 x。如果你写了自己的控件,希望给控件指定一个默认的命名空间前缀,那么可以通过在库中写如下特性(Attribute)实现:

using System.Windows.Markup;
[assembly: XmlnsPrefix("http://walterlv.github.io/demo", "w")]

这样,当 XAML 设计器帮助你自动添加命名空间时,将会使用 w 前缀。虽然实际上你也能随便改。

详情请阅读博客:

此写法要生效,定义的组件与使用的组件不能在同一程序集。

让你做的控件库不需要 XAML 命名空间前缀

自己写了一个 DemoPage,要在 XAML 中使用,一般需要添加命名空间前缀才可以。但是也可以不写:

<UserControl
    x:Class="HuyaHearhira.UserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <DemoPage />
    </Grid>
</UserControl>

方法是在库中定义命名空间前缀为 http://schemas.microsoft.com/winfx/2006/xaml/presentation

using System.Windows.Markup;
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Walterlv.NewCsprojDemo")]

此写法要生效,定义的组件与使用的组件不能在同一程序集。