dotnet 职业技术学院

博客

dotnet 职业技术学院

使用一句 git 命令将仓库的改动推送到所有的远端

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

git 支持一个本地仓库包含多个远端(remote),这对于开源社区来说是一个很重要的功能,可以实时获取到最新的开源代码且能推送到自己的仓库中提交 pull request。

有时候多个远端都是自己的,典型的就是 GitHub Pages 服务了,推送总是希望这几个远端能够始终和本地仓库保持一致。本文将介绍一个命令推送到所有远端的方法。


我的博客同时发布在 GitHub 仓库 https://github.com/walterlv/walterlv.github.io 和 Gitee 仓库 http://gitee.com/walterlv/walterlv。由于这两个远端的 Pages 服务没有打通,所以我总是需要同时将博客推送到两个不同的远端中。

第一步:设置多个远端(remote)

使用你平常使用的方法添加多个 git 远端。

例如:

git remote add github https://github.com/walterlv/walterlv.github.io.git --no-tags

需要注意,对于不是 origin 的远端,建议不要拉取 tags,所以我加了 --no-tags 选项。

我添加了两个新的远端(github 和 gitee)之后,打开你仓库 .git 文件夹中的 config 文件,应该可以看到如下的内容:

[remote "origin"]
	url = https://github.com/walterlv/walterlv.github.io.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master
[remote "github"]
	url = https://github.com/walterlv/walterlv.github.io.git
	fetch = +refs/heads/*:refs/remotes/github/*
	tagopt = --no-tags
[remote "gitee"]
	url = https://gitee.com/walterlv/walterlv.git
	fetch = +refs/heads/*:refs/remotes/gitee/*
	tagopt = --no-tags

第二步:添加一个名为 all 的新远端

现在,我们要添加一个名为 all 的新远端,并且在里面添加两个 url。由于这个步骤没有 git 命令行的帮助,所以你需要手工修改 config 文件中的内容。

[remote "all"]
	url = https://github.com/walterlv/walterlv.github.io.git
	url = https://gitee.com/walterlv/walterlv.git
	tagopt = --no-tags

如果你有更多需要同步的远端,那么就在里面添加更多的 url。

开始使用一个命令同步所有的仓库

现在,你可以使用一句命令将本地的修改推送到所有的远端了。

git push all

我现在自己的博客仓库就是这样的推送方式。于是你可以在以下多个地址打开阅读我的博客:

.NET/C# 获取一个正在运行的进程的命令行参数

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

在自己的进程内部,我们可以通过 Main 函数传入的参数,也可以通过 Environment.GetCommandLineArgs 来获取命令行参数。

但是,可以通过什么方式来获取另一个运行着的程序的命令行参数呢?


进程内部获取传入参数的方法,可以参见我的另一篇博客:.NET 命令行参数包含应用程序路径吗?

.NET Framework / .NET Core 框架内部是不包含获取其他进程命令行参数的方法的,但是我们可以在任务管理器中看到,说明肯定存在这样的方法。

任务管理器中的命令行参数

实际上方法是有的,不过这个方法是 Windows 上的专属方法。

对于 .NET Framework,需要引用程序集 System.Management;对于 .NET Core 需要引用 Microsoft.Windows.Compatibility 这个针对 Windows 系统准备的兼容包(不过这个兼容包目前还是预览版本)。

<ItemGroup Condition="$(TargetFramework) == 'netcoreapp2.1'">
    <PackageReference Include="Microsoft.Windows.Compatibility" Version="2.1.0-preview.19073.11" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'net472'">
    <Reference Include="System.Management" />
</ItemGroup>

然后,我们使用 ManagementObjectSearcherManagementBaseObject 来获取命令行参数。

为了简便,我将其封装成一个扩展方法,其中包括对于一些异常的简单处理。

using System;
using System.Diagnostics;
using System.Linq;
using System.Management;

namespace Walterlv
{
    /// <summary>
    /// 为 <see cref="Process"/> 类型提供扩展方法。
    /// </summary>
    public static class ProcessExtensions
    {
        /// <summary>
        /// 获取一个正在运行的进程的命令行参数。
        /// 与 <see cref="Environment.GetCommandLineArgs"/> 一样,使用此方法获取的参数是包含应用程序路径的。
        /// 关于 <see cref="Environment.GetCommandLineArgs"/> 可参见:
        /// .NET 命令行参数包含应用程序路径吗?https://blog.walterlv.com/post/when-will-the-command-line-args-contain-the-executable-path.html
        /// </summary>
        /// <param name="process">一个正在运行的进程。</param>
        /// <returns>表示应用程序运行命令行参数的字符串。</returns>
        public static string GetCommandLineArgs(this Process process)
        {
            if (process is null) throw new ArgumentNullException(nameof(process));

            try
            {
                return GetCommandLineArgsCore();
            }
            catch (Win32Exception ex) when ((uint) ex.ErrorCode == 0x80004005)
            {
                // 没有对该进程的安全访问权限。
                return string.Empty;
            }
            catch (InvalidOperationException)
            {
                // 进程已退出。
                return string.Empty;
            }

            string GetCommandLineArgsCore()
            {
                using (var searcher = new ManagementObjectSearcher(
                    "SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id))
                using (var objects = searcher.Get())
                {
                    var @object = objects.Cast<ManagementBaseObject>().SingleOrDefault();
                    return @object?["CommandLine"]?.ToString() ?? "";
                }
            }
        }
    }
}

使用此方法得到的命令行参数是一个字符串,而不是我们通常使用字符串时的字符串数组。如果你需要将其转换为字符串数组,可以使用我在另一篇博客中使用的方法:


参考资料

WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑定了

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

如果你写了一个 MarkupExtension 在 XAML 当中使用,你会发现你在 MarkupExtension 中定时的属性是无法使用 XAML 绑定的,因为 MarkupExtension 不是一个 DependencyObject

本文将给出解决方案,让你能够在任意的类型中写出支持 XAML 绑定的属性;而不一定要依赖对象(DependencyObject)和依赖属性(DependencyProperty)。


问题

下面是一个很简单的 MarkupExtension,用户设置了什么值,就返回什么值。拿这么简单的类型只是为了避免额外引入复杂的理解难度。

public class WalterlvExtension : MarkupExtension
{
    private object _value;

    public object Value
    {
        get => _value;
        set => _value = value;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Value;
    }
}

可以在 XAML 中直接赋值:

<Button Content="{local:Walterlv Value=blog.walterlv.com" />

但不能绑定:

<TextBox x:Name="SourceTextBox" Text="blog.walterlv.com" />
<Button Content="{local:Walterlv Value={Binding Text, Source={x:Reference SourceTextBox}}}" />

因为运行时会报错,提示绑定必须被设置到依赖对象的依赖属性中。在设计器中也可以看到提示不能绑定。

运行时报错

设计器的警告

解决

实际上这个问题是能够解决的(不过也花了我一些时间思考解决方案)。

既然绑定需要一个依赖属性,那么我们就定义一个依赖属性。非依赖对象中不能定义依赖属性,于是我们定义附加属性。

// 注意:这一段代码实际上是无效的。

public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
    "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));

public object Value
{
    get => ???.GetValue(ValueProperty);
    set => ???.SetValue(ValueProperty, value);
}

这里问题来了,获取和设置附加属性是需要一个依赖对象的,那么我们哪里去找依赖对象呢?直接定义一个新的就好了。

于是我们定义一个新的依赖对象:

// 注意:这一段代码实际上是无效的。

public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
    "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object)));

public object Value
{
    get => _dependencyObject.GetValue(ValueProperty);
    set => _dependencyObject.SetValue(ValueProperty, value);
}

private readonly DependencyObject _dependencyObject = new DependencyObject();

现在虽然可以编译通过,但是我们会遇到两个问题:

  1. ValueProperty 的变更通知的回调函数中,我们只能找到 _dependencyObject 的实例,而无法找到外面的类型 WalterlvExtension 的实例;这几乎使得 Value 的变更通知完全失效。
  2. Valueset 方法中得到的 value 值是一个 Binding 对象,而不是正常依赖属性中得到的绑定的结果;这意味着我们无法直接使用 Value 的值。

为了解决这两个问题,我必须自己写一个代理的依赖对象,用于帮助做属性的变更通知,以及处理绑定产生的 Binding 对象。在正常的依赖对象和依赖属性中,这些本来都不需要我们自己来处理。

方案

于是我写了一个代理的依赖对象,我把它命名为 ClrBindingExchanger,意思是将 CLR 属性和依赖属性的绑定进行交换。

代码如下:

public class ClrBindingExchanger : DependencyObject
{
    private readonly object _owner;
    private readonly DependencyProperty _attachedProperty;
    private readonly Action<object, object> _valueChangeCallback;

    public ClrBindingExchanger(object owner, DependencyProperty attachedProperty,
        Action<object, object> valueChangeCallback = null)
    {
        _owner = owner;
        _attachedProperty = attachedProperty;
        _valueChangeCallback = valueChangeCallback;
    }

    public object GetValue()
    {
        return GetValue(_attachedProperty);
    }

    public void SetValue(object value)
    {
        if (value is Binding binding)
        {
            BindingOperations.SetBinding(this, _attachedProperty, binding);
        }
        else
        {
            SetValue(_attachedProperty, value);
        }
    }

    public static void ValueChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ClrBindingExchanger) d)._valueChangeCallback?.Invoke(e.OldValue, e.NewValue);
    }
}

这段代码的意思是这样的:

  1. 构造函数中的 owner 参数完全没有用,我只是拿来备用,你可以删掉。
  2. 构造函数中的 attachedProperty 参数是需要定义的附加属性。
    • 因为前面我们说过,有一个附加属性才可以编译通过,所以附加属性是一定要定义的
    • 既然一定要定义附加属性,那么就可以用起来,接下来会用
  3. 构造函数中的 valueChangeCallback 参数是为了指定变更通知的,因为前面我们说变更通知不好做,于是就这样代理做变更通知。
  4. GetValueSetValue 这两个方法是用来代替 DependencyObject 自带的 GetValueSetValue 的,目的是执行我们希望特别执行的方法。
  5. SetValue 中我们需要自己考虑绑定对象,如果发现是绑定,那么就真的进行一次绑定。
  6. ValueChangeCallback 是给附加属性用的,因为用我的这种方法定义附加属性时,只能写出相同的代码,所以干脆就提取出来。

而用法是这样的:

public class WalterlvExtension : MarkupExtension
{
    public WalterlvExtension()
    {
        _valueExchanger = new ClrBindingExchanger(this, ValueProperty, OnValueChanged);
    }

    private readonly ClrBindingExchanger _valueExchanger;

    public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached(
        "Value", typeof(object), typeof(WalterlvExtension),
        new PropertyMetadata(null, ClrBindingExchanger.ValueChangeCallback));

    public object Value
    {
        get => _valueExchanger.GetValue();
        set => _valueExchanger.SetValue(value);
    }

    private void OnValueChanged(object oldValue, object newValue)
    {
        // 在这里可以处理 Value 属性值改变的变更通知。
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Value;
    }
}

对于一个属性来说,代码确实多了些,这实在是让人难受。可是,这可以达成目的呀!

解释一下:

  1. 定义一个 _valueExchanger,就是在使用我们刚刚写的那个新类。
  2. 在构造函数中对 _valueExchanger 进行初始化,因为要传入 this 和一个实例方法 OnValueChanged,所以只能在构造函数中初始化。
  3. 定义一个附加属性(前面我们说了,一定要有依赖属性才可以编译通过哦)。
    • 注意属性的变更通知方法,需要固定写成 ClrBindingExchanger.ValueChangeCallback
  4. 定义普通的 CLR 属性 Value
    • GetValue 方法要换成我们自定义的 GetValue
    • SetValue 方法也要换成我们自定义的 SetValue 哦,这样绑定才可以生效
  5. OnValueChanged 就是我们实际的变更通知,这里得到的 oldValuenewValue 就是你期望的值,而不是我面前面奇怪的绑定实例。

于是,绑定就这么在一个普通的类型和一个普通的 CLR 属性中生效了,而且还获得了变更通知。

参考资料

本文没有任何参考资料,所有方法都是我(walterlv)的原创方法,因为真的找不到资料呀!不过在找资料的过程中发现了一些没解决的文档或帖子:

在 Snoop 中使用 PowerShell 脚本进行更高级的 UI 调试

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

在 WPF 开发时,有 Snoop 的帮助,UI 的调试将变得非常轻松。使用 Snoop,能轻松地查看 WPF 中控件的可视化树以及每一个 Visual 节点的各种属性,或者查看数据上下文,或者监听查看事件的引发。

不过,更强大的是支持使用 PowerShell 脚本。这使得它即便 UI 没有给你提供一些入口,你也能通过各种方式查看或者修改 UI。


Snoop PowerShell 入口

常规 Snoop 的使用方法,将狮子瞄准镜拖出来对准要调试 UI 的 WPF 窗口松开。这里我拿 Visual Studio 2019 的窗口做试验。

调试 Visual Studio 2019 的 UI

在打开的新的 Snoop 窗口中我们打开 PowerShell 标签。

打开 PowerShell 标签

本文的内容将从这里开始。

自带的 PowerShell 变量

在 Snoop 的 PowerShell 提示窗口中,我们可以得知有两个变量可以使用:$root$selected。包含这两个,还有其他的可以使用:

  • $root 拿到当前 Snoop 窗口顶层元素类型的实例
  • $selected 拿到当前 Snoop 用鼠标或键盘选中的元素的实例
  • $parent 拿到当前 Snoop 选中元素的可视化树父级
  • $null 就是 .NET 中的 null

当然,你也可以定义和使用其他的变量,后面会说。

`$root`

`$selected`

基本的 PowerShell 命令

属性

# 获取属性
$selected.Visual.Content
# 将属性设置为 null
$selected.Visual.Content = $null

直接像 C# 语法那样一直在后面使用 . 可以访问实例中的属性。不需要关心实例是什么类型的,只要拥有那个属性,就可以访问到。

比如下面,上面的例子我们选中的是 MainWindow,于是我们使用 $selected.Visual.Content 访问到 MainWindowContent 属性,而后面 $selected.Visual.Content = $null 则是将 Window 的内容清空了。

获取 Content 属性

设置 Content 属性

创建对象

# 创建对象
$button = New-Object System.Windows.Controls.Button -property @{ Content = "欢迎访问 blog.walterlv.com" }

创建一个 Button

调用方法

$selected.Visual.Children.Add($button)

顶部的那个按钮就是通过上面的命令添加上去的。

调用实例方法

调用静态方法用的是 [类名]::方法名(参数)

$button.Content = [System.Environment]::Version.ToString() + " running for blog.walterlv.com"

调用静态方法


参考资料

四种方法获取可执行程序的文件路径(.NET Core / .NET Framework)

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

本文介绍四种不同的获取可执行程序文件路径的方法。适用于 .NET Core 以及 .NET Framework。


使用程序集信息获取

var executablePath = Assembly.GetEntryAssembly().Location;

这种方式的思路是获取入口程序集所在的路径。不过 Assembly.GetEntryAssembly() 能获取到的程序集是入口托管程序集;使用此方法会返回第一个托管程序集。

只有 .NET Framework 程序的入口才是托管程序(exe)。而对于 .NET Core 程序,如果直接发布成带环境依赖声明的 dll,那么实际运行的进程是 dotnet.exe;而如果发布成自包含的 exe 程序,其主 exe 也是一个非托管的 CLR 启动器而已,并不是托管程序集。

所以此方法适用条件:

  1. 必须是 .NET Framework 程序(.NET Core 程序不适用)

使用应用程序域信息获取

var executablePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

这种方式的思路是获取当前 AppDomain 所在的文件夹。不过此方法也只是获取到文件夹而已,不包含文件名。

所以此方法适用条件:

  1. 你不需要知道文件名,只是要一个程序所在的文件夹而已。

当然,此方法因为不涉及到托管和非托管程序集,所以与编译结果无关,适用于 .NET Core 和 .NET Framework 程序。

使用进程信息获取

var executablePath = Process.GetCurrentProcess().MainModule.FileName;

这种方式的思路是获取当前进程可执行程序的完全路径。

对于 .NET Framework 程序,其 exe 就是这个路径。

对于 .NET Core 程序来说:

  1. 如果发布成带环境依赖声明的 dll,那么此方法获取到的可执行程序名将是 dotnet.exe,这显然不会是我们预期的行为;
  2. 如果发布成自包含的 exe,那么此方法获取到的可执行程序名就是程序自己的名称,这是期望的结果。

所以此方法适用条件:

  1. 适用于 .NET Framework 程序;
  2. 适用于发布成自包含的 .NET Core 程序。

使用命令行参数获取

我在另一篇博客中提到命令行参数中包含应用程序路径:

于是我们也可以通过命令行参数来获取到可执行程序的路径。

var executablePath = Environment.GetCommandLineArgs()[0];

这种方法的效果和前面使用进程信息获取的效果是相同的,会获取到相同的可执行程序路径。

总结靠谱的方法

通过以上方法的说明,我们可以知道目前没有 100% 可靠的获取当前可执行程序文件路径的方法,不过可以组合多种方法达到 100% 可靠的目的。

  1. 如果我们只需要获取程序所在的文件夹
    • 那么请直接使用 AppDomain.CurrentDomain.SetupInformation.ApplicationBase
  2. 如果我们需要获取到可执行程序的完整路径
    • 先通过进程或者命令行参数的方式获取
      • Process.GetCurrentProcess().MainModule.FileName
      • Environment.GetCommandLineArgs()[0]
    • 如果得到的进程是 dotnet.exe,那么再通过程序集信息获取
      • Assembly.GetEntryAssembly().Location

另外,关于以上方法的性能对比,你可以参阅林德熙的博客:dotnet 获取路径各种方法的性能对比

为什么 C# 的 string.Empty 是一个静态只读字段,而不是一个常量呢?

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

使用 C# 语言编写字符串常量的时候,你可能会发现可以使用 "" 而不能使用 string.Empty。进一步可以发现 string.Empty 实际上是一个静态只读字段,而不是一个常量。

为什么这个看起来最适合是常量的 string.Empty,竟然使用静态只读字段呢?


string.Empty

这个问题,我们需要去看 .NET Core 的源码(当然 .NET Framework 也是一样的)。

[Intrinsic]
public static readonly string Empty;

值得注意的是上面的 Intrinsic 特性。

Intrinsic 特性

Intrinsic 特性的注释是这样的:

Calls to methods or references to fields marked with this attribute may be replaced at some call sites with jit intrinsic expansions.
Types marked with this attribute may be specially treated by the runtime/compiler.

翻译过来是:对具有此 Intrinsic 特性标记的字段的方法或引用的调用可以在某些具有 JIT 内部扩展的调用点处替换,标记有此属性的类型可能被运行时或编译器特殊处理。

也就是说,string.Empty 字段并不是一个普通的字段,对它的调用会被特殊处理。但是是如何特殊处理呢?

JIT 编译器

string.Empty 的注释是这样描述的:

The Empty constant holds the empty string value. It is initialized by the EE during startup. It is treated as intrinsic by the JIT as so the static constructor would never run. Leaving it uninitialized would confuse debuggers.
We need to call the String constructor so that the compiler doesn’t mark this as a literal. Marking this as a literal would mean that it doesn’t show up as a field which we can access from native.

翻译过来是:

Empty 常量保存的是空字符串的值,它在启动期间由执行引擎初始化。它被 JIT 视为内在的,因此静态构造函数永远不会运行。将它保持为未初始化的状态将会使得调试器难以解释此行为。
于是我们需要调用 String 的构造函数,以便编译器不会将其标记为文字。将其标记为文字将意味着它不会显示为我们可以从本机代码访问的字段。

说明一下:

  1. 注释里的 EE 是 Execution Engine 的缩写,其实也就是 CLR 运行时。
  2. 那个 literal 我翻译成了文字。实际上这里说的是 IL 调用字符串时的一些区别:
    • 在调用 "" 时使用的 IL 是 ldstr ""(Load String Literal)
    • 而在调用 string.Empty 时使用的 IL 是 ldsfld string [mscorlib]System.String::Empty(Load Static Field)
  3. 虽然 IL 在调用 ""string.Empty 时生成的 IL 不同,但是在 JIT 编译成本机代码的时候,生成的代码完全一样。

string.Empty 字段在整个 String 类型中你都看不到初始化的代码,String 类的静态构造函数也不会执行。也就是说,String 类中的所有静态成员都不会被托管代码初始化。String 的静态初始化过程都是由 CLR 运行时进行的,而这部分的初始化是本机代码实现的。

那本机代码又是如何初始化 String 类型的呢?在 CLR 运行时的 AppDomain::SetupSharedStatics() 方法中实现,可前往 GitHub 阅读这部分的源码:

// This is a convenient place to initialize String.Empty.
// It is treated as intrinsic by the JIT as so the static constructor would never run.
// Leaving it uninitialized would confuse debuggers.

// String should not have any static constructors.
_ASSERTE(g_pStringClass->IsClassPreInited());

FieldDesc * pEmptyStringFD = MscorlibBinder::GetField(FIELD__STRING__EMPTY);
OBJECTREF* pEmptyStringHandle = (OBJECTREF*)
    ((TADDR)pLocalModule->GetPrecomputedGCStaticsBasePointer()+pEmptyStringFD->GetOffset());
SetObjectReference( pEmptyStringHandle, StringObject::GetEmptyString(), this );

总结:为什么 string.Empty 需要是一个静态只读字段而不是常量?

从上文中 string.Empty 的注释描述中可以知道:

  1. 编译器会将 C# 语言编译成中间语言 MSIL;
  2. 如果这是一个常量,那么编译器在不做特殊处理的情况下,就会生成 ldstr "",而这种方式不会调用到 String 类的构造函数(注意不是静态构造函数,String 类的静态构造函数是特殊处理不会调用的);
  3. 而如果这是一个静态字段,那么编译器可以在不做特殊处理的情况下,生成 ldsfld string [mscorlib]System.String::Empty,这在首次执行时会触发 String 类的构造函数,并在本机代码(非托管代码)中完成初始化。

当然,事实上编译器也可以针对此场景做特殊处理,但为什么不是在编译这一层进行特殊处理,我已经找不到出处了。

本文引申的其他问题

能否反射修改 string.Empty 的值?

不行!

实际上,在 .NET Framework 4.0 及以前是可以反射修改其值的,这会造成相当多的基础组件不能正常工作,在 .NET Framework 4.5 和以后的版本,以及 .NET Core 中,CLR 运行时已经不允许你做出这么出格儿的事了。

不过,如果你使用不安全代码(unsafe)来修改这个字段的值就当我没说。关于使用不安全代码转换字符串的方法可以参见:

""string.Empty 到底有什么区别?

从前文你可以得知,在运行时级别,这两者 没有任何区别

于是,当你需要一个代表 “空字符串” 含义的时候,使用 string.Empty;而当你必须要一个常量时,就使用 ""


参考资料

C#/.NET 调试的时候显示自定义的调试信息(DebuggerDisplay 和 DebuggerTypeProxy)

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

使用 Visual Studio 调试 .NET 程序的时候,在局部变量窗格或者用鼠标划到变量上就能查看变量的各个字段和属性的值。默认显示的是对象 ToString() 方法调用之后返回的字符串,不过如果 ToString() 已经被占作它用,或者我们只是希望在调试的时候得到我们最希望关心的信息,则需要使用 .NET 中调试器相关的特性。

本文介绍使用 DebuggerDisplayAttributeDebuggerTypeProxyAttribute 来自定义调试信息的显示。(同时隐藏我们在背后做的这些见不得人的事儿。)


示例代码

比如我们有一个名为 CommandLine 的类型,表示从命令行传入的参数;内有一个字典,包含命令行参数的所有信息。

public class CommandLine
{
    private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;
    private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)
        => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));
}

现在,我们在 Visual Studio 里面调试得到一个 CommandLine 的实例,然后使用调试器查看这个实例的属性、字段和集合。

然后,这样的一个字典嵌套列表的类型,竟然需要点开 4 层才能知道命令行参数究竟是什么。这样的调试效率显然是太低了!

原生的调试显示

DebuggerDisplay

使用 DebuggerDisplayAttribute 可以帮助我们直接在局部变量窗格或者鼠标划过的时候就看到对象中我们最希望了解的信息。

现在,我们在 CommandLine 上加上 DebuggerDisplayAttribute

// 此段代码非最终版本。
[DebuggerDisplay("CommandLine: {DebuggerDisplay}")]
public class CommandLine
{
    private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;
    private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)
        => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));

    private string DebuggerDisplay => string.Join(' ', _optionArgs
        .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));
}

效果有了:

使用 DebuggerDisplay

不过,展开对象查看的时候可以看到一个 DebuggerDisplay 的属性,而这个属性我们只是调试使用,这是个垃圾属性,并不应该影响我们的查看。

里面有一个 DebuggerDisplay 垃圾属性

我们使用 DebuggerBrowsable 特性可以关闭某个属性或者字段在调试器中的显示。于是代码可以改进为:

--  [DebuggerDisplay("CommandLine: {DebuggerDisplay}")]
++  [DebuggerDisplay("CommandLine: {DebuggerDisplay,nq}")]
    public class CommandLine
    {
        private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;
        private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)
            => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));
    
++      [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private string DebuggerDisplay => string.Join(' ', _optionArgs
            .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));
    }

添加了从不显示此字段(DebuggerBrowsableState.Never),在调试的时候,展开后的属性列表里面没有垃圾 DebuggerDisplay 属性了。

另外,我们在 DebuggerDisplay 特性的中括号中加了 nq 标记(No Quote)来去掉最终显示的引号。

DebuggerTypeProxy

虽然我们使用了 DebuggerDisplay 使得命令行参数一眼能看出来,但是看不出来我们把命令行解析成什么样了。于是我们需要更精细的视图。

然而,上面展开 _optionArgs 字段的时候,依然需要展开 4 层才能看到我们的所有信息,所以我们使用 DebuggerTypeProxyAttribute 来优化调试器实例内部的视图。

class CommandLineDebugView
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly CommandLine _owner;

    public CommandLineDebugView(CommandLine owner)
    {
        _owner = owner;
    }

    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    private string[] Options => _owner._optionArgs
        .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}")
        .ToArray();
}

我面写了一个新的类型 CommandLineDebugView,并在构造函数中允许传入要优化显示的类型的实例。在这里,我们写一个新的 Options 属性把原来字典里面需要四层才能展开的值合并成一个字符串集合。

但是,我们在 Options 上标记 DebuggerBrowsableState.RootHidden

  1. 如果这是一个集合,那么这个集合将直接显示到调试视图的上一级视图中;
  2. 如果这是一个普通对象,那么这个对象的各个属性字段将合并到上一级视图中显示。

别忘了我们还需要禁止 _owner 在调试器中显示,然后把 [DebuggerTypeProxy(typeof(CommandLineDebugView))] 加到 CommandLine 类型上。

这样,最终的显示效果是这样的:

使用 DebuggerTypeProxy

点击 Raw View 可以看到我们没有使用 DebuggerTypeProxyAttribute 视图时的属性和字段。

最终代码

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Walterlv.Framework.StateMachine;

namespace Walterlv.Framework
{
    [DebuggerDisplay("CommandLine: {DebuggerDisplay,nq}")]
    [DebuggerTypeProxy(typeof(CommandLineDebugView))]
    public class CommandLine
    {
        private readonly Dictionary<string, IReadOnlyList<string>> _optionArgs;
        private CommandLine(Dictionary<string, IReadOnlyList<string>> optionArgs)
            => _optionArgs = optionArgs ?? throw new ArgumentNullException(nameof(optionArgs));

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private string DebuggerDisplay => string.Join(' ', _optionArgs
            .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}"));

        private class CommandLineDebugView
        {
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            private readonly CommandLine _owner;

            public CommandLineDebugView(CommandLine owner) => _owner = owner;

            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            private string[] Options => _owner._optionArgs
                .Select(pair => $"{pair.Key}{(pair.Key == null ? "" : " ")}{string.Join(' ', pair.Value)}")
                .ToArray();
        }
    }
}

参考资料

透明度叠加算法:如何计算半透明像素叠加到另一个像素上的实际可见像素值(附 WPF 和 HLSL 的实现)

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

本文介绍透明度叠加算法(Alpha Blending Algorithm),并用 C#/WPF 的代码,以及像素着色器的代码 HLSL 来实现它。


算法

对于算法,我只是搬运工,可以随意搜索到。算法详情请查看:Alpha compositing - Wikipedia

对于完全不透明的背景和带有透明度的前景,合并算法为:

float r = (foreground.r * alpha) + (background.r * (1.0 - alpha));

这是红色。然后绿色 g 和蓝色 b 通道进行一样的计算。最终合成图像的透明通道始终设置为 1。

在 C# 代码中实现

多数 UI 框架对于颜色值的处理都是用一个 byte 赛表单个通道的一个像素。于是计算会采用 0xff 即 255。

for (int i = 0; i + 4 < length; i = i + 4)
{
    var backB = background[i];
    var backG = background[i + 1];
    var backR = background[i + 2];
    var foreB = foreground[i];
    var foreG = foreground[i + 1];
    var foreR = foreground[i + 2];
    double alpha = foreground[i + 3];

    blue = 0;

    output[i] = (foreB * alpha) + (backB * (1.0 - alpha));
    output[i + 1] = (foreG * alpha) + (backG * (1.0 - alpha));
    output[i + 2] = (foreR * alpha) + (backR * (1.0 - alpha));
    output[i + 3] = 1.0;
}

这段代码当然是跑不起来的,因为是下面两篇博客的魔改代码。你需要阅读以下两篇博客了解如何在 WPF 中按像素修改图像,然后应用上面的透明度叠加代码。

话说,一般 UI 框架都自带有透明度叠加,为什么还要自己写一份呢?

当然是因为某些场景下我们无法使用到 UI 框架的透明度叠加特性的时候。例如使用 HLSL 编写像素着色器的一个实现。

下面使用像素着色器的实现是我曾经写过的一个特效的一个小部分,我把透明度叠加的部分单独摘取出来。

在像素着色器中实现

以下是 HLSL 代码的实现。Background 是从采样寄存器 0 取到的颜色采样,Foreground 是从采样寄存器 1 取到的颜色采样。

这里的计算中,背景是不带透明度的,而前景是带有透明度的。

/// <description>透明度叠加效果。</description>

sampler2D Background : register(s0);
sampler2D Foreground : register(s1);

float4 main(float2 uv : TEXCOORD) : COlOR
{
    float4 background = tex2D(Background, uv);
    float4 foreground = tex2D(Foreground, uv);
    float alpha = foreground.a;

    float r = (foreground.r * alpha) + (background.r * (1.0 - alpha));
    float g = (foreground.g * alpha) + (background.g * (1.0 - alpha));
    float b = (foreground.b * alpha) + (background.b * (1.0 - alpha));
    float a = 1.0;
    
    return float4(r, g, b, a);
}

叠加了一个带有透明度的图片

如果要测试的图片都是不带透明度的,那么可以通过自己设一个透明度来模拟,传入透明度值 Alpha。

/// <description>透明度叠加效果。</description>

/// <type>Double</type>
/// <summary>采样 2 的叠加透明度。</summary>
/// <minValue>0.0</minValue>
/// <maxValue>1.0</maxValue>
/// <defaultValue>0.75</defaultValue>
float Alpha : register(C0);

sampler2D Background : register(s0);
sampler2D Foreground : register(s1);

float4 main(float2 uv : TEXCOORD) : COlOR
{
    float4 background = tex2D(Background, uv);
    float4 foreground = tex2D(Foreground, uv);
    float alpha = Alpha;

    float r = (foreground.r * alpha) + (background.r * (1.0 - alpha));
    float g = (foreground.g * alpha) + (background.g * (1.0 - alpha));
    float b = (foreground.b * alpha) + (background.b * (1.0 - alpha));
    float a = 1.0;
    
    return float4(r, g, b, a);
}

为第二张采样设定透明度


参考资料

使用 Xamarin 开发 iOS 键盘扩展(含网络访问)

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

作为一位 .NET 技术的死忠,开发 iOS 应用当然要使用 Xamarin 啦!

本文用我的阅读的文档和实践为素材,介绍如何使用 Xamarin 开发一个 iOS 的键盘扩展。


你可以在 Walterlv.CloudKeyboard 仓库中获得本文所述的全部源代码。

搭建环境

本文不会花篇幅来讲如何搭建 Xamarin iOS 开发的环境,不然这篇文章就没有重点。

于是,请阅读这一篇来了解如何搭建 Xamarin iOS 的开发环境:

  1. 安装调试工具:Mac 部分 Xamarin开发(Mac开发)环境搭建 - 简书
  2. 安装调试工具:Windows 部分 vs2017开发IOS(vs2017 xamarin 连接mac) - ManGo.XYZ - CSDN博客
  3. 申请开发者账号:https://developer.apple.com/register/阅读这里了解坑
  4. 准备一根 Type-C 到 Lightning 的数据线,用于 Mac 从 Mac 部署到真机进行调试

你需要了解的 iOS 键盘扩展的背景知识

了解以下背景知识,有助于我们接下来开发的时候少踩一些坑。当然我不会在这里说 iOS 应用开发的所有背景知识,只会说与 iOS 键盘扩展相关的部分。

  1. iOS 键盘扩展是 iOS 扩展的一种,而 iOS 扩展是 iOS 8.0 才开始引入的概念。
  2. iOS 扩展需要有一个 iOS 普通应用作为容器一起打包;所以,你需要创建两个项目来完成 iOS 键盘扩展的开发。
    • 在后文,我们将直接使用 iOS 容器应用来描述这个概念
    • 扩展的包标识符(Bundle Identifier)必须以容器应用的包标识符字符串作为开头
  3. iOS 扩展和 iOS 容器应用会被视为两款完全不同的应用,互相之前不能共享任何数据。
    • 如果真的要共享数据,就需要像其他两款不同应用共享数据一样的处理方式
  4. iOS 键盘扩展默认是不能访问网络的,你需要声明允许访问网络,并获得用户的同意才行。

创建 iOS 键盘扩展项目

第一步:创建 Xamarin.Forms 项目。

这个不用太在意里面的实现,因为它只是我们的“容器项目”(前面有介绍)。实际上在本文我们完全不会碰这个项目里面的代码,只是为了配置我们的 iOS 应用包而已。未来你可以在这个容器应用里面做键盘的个性化设置。

创建 Xamarin.Forms 项目

然后,选择 iOS 平台。

我们只需要 iOS 端。因为对于键盘,不同系统的实现差异很大,之间共享的代码只能是非键盘部分的代码了。

我们只选择 iOS 平台

第二步:创建 iOS 键盘扩展项目

创建新项目

创建 Custom Keyboard Extension 项目

创建完成之后的三个项目

当你创建完之后,你会看到三个不同的项目。

你可能发现 Walterlv.KeyboardExtension.Keyboard 项目有些奇怪,里面有 Main 函数和 AppDelegate,按道理这是一个主程序包。然而实际测试中单独有这个项目是跑不起来的(这可能是一个 Bug,如果修复了,请在下面评论或者邮件告知我,谢谢了)。

于是,Main 和 AppDelegate 这两个文件是可以删除的。如果你强迫症,就删掉吧。当然不删掉也不影响,不过我删掉了。

第三步:引用 iOS 键盘扩展项目

在 iOS 容器应用上面添加键盘扩展项目作为引用。

在 iOS 容器应用上添加引用

引用键盘扩展项目

如果你感兴趣去查看 Walterlv.KeyboardExtension.iOS 项目中对 Walterlv.KeyboardExtension.Keyboard 项目的引用节点的话,你会发现 Xamarin 已经自动为这个项目标记上了 <IsAppExtension />。只有加上了 AppExtension 标记,Xamarin 才会把这个项目作为 iOS 扩展项目进行打包。

<ProjectReference Include="..\..\Walterlv.KeyboardExtension.Keyboard\Walterlv.KeyboardExtension.Keyboard.csproj">
    <Project>{d6f006e7-3c98-4b97-b2d5-4d2e3bc2f945}</Project>
    <Name>Walterlv.KeyboardExtension.Keyboard</Name>
    <IsAppExtension>true</IsAppExtension>
    <IsWatchApp>false</IsWatchApp>
</ProjectReference>

在以上三个步骤完成之后,理论上你是可以正常编译此项目的。

可以编译通过

配置包信息

iOS 应用的包信息存储在 plist 中。所以在这一节,你需要正确配置两个项目的 plist。

没错!是两个项目。还记得前面背景知识里面我们说到容器项目和扩展项目就是两个不同的应用吗?

配置 plist 的方法,就是在 Visual Studio 里面双击这个文件。

按照下图这样配置:

配置两个项目的 plist 文件

说明:

  1. Application Name 对应 plist 中的 CFBundleDisplayName 属性,也就是应用的显示名称。
    • 对于容器应用,就是 iOS 图标下面的名称,对于键盘,就是切换键盘的时候所用的名称。
    • 下图中 iOS 应用图标下面的名称 CloudKeyboard 就是我在 Walterlv.CloudKeyboard 项目中的容器应用的名称。
    • 下图中在 iOS 切换键盘时,Cloud 就是我在 Walterlv.CloudKeyboard 项目中的键盘名称。
  2. 扩展项目的 Bundle Identifier 名称必须以容器项目的 Bundle Identifier 名称作为前缀。
    • 如果不满足要求,部署时扩展将不会生效。

iOS 应用图标

iOS 上切换键盘

至此,你的项目可以直接编译了。如果你有真机部署环境,都可以直接部署到真机上看效果了。

真机部署调试

本文不会花篇幅来讲如何真机部署调试,不然这篇文章就没有重点。

但是你可以阅读:使用 Xamarin 在 iOS 真机上部署应用进行调试

当然这是 Mac 版本的(毕竟我在 Windows 上实际也没有成功真机调试过,我是 git 同步到 Mac 上用 Visual Studio for Mac 来真机调试的)。

只是你需要注意做这些内容:

  1. 你需要同意一份开发者证书(不然打不开应用):
    • 设置 -> 通用 -> 设备管理 -> [自己的开发者账号] -> 信任
  2. 还需要打开这个键盘(不然看不到键盘):
    • 设置 -> 通用 -> 键盘 -> 添加新键盘… -> [选择我们刚刚开发的键盘]

下面是我部署到真机上之后,在亮暗两种不同的界面下的键盘截图(就是上面的项目,没有改任何代码):

键盘真机部署后的运行效果

处理键盘的文字输入、退格和确定

我们把 Walterlv.CloudKeyboard.iOS.Extension 也就是那个键盘扩展项目删除得只剩下 KeyboardViewController.cs 了,我们也只需要在这个类中写代码而已。

要控制文字输入,就是使用 TextDocumentProxy 实例。我们的 KeyboardViewController 继承自 UIInputViewController,于是我们能够在类中直接使用 TextDocumentProxy 实例。

在光标处插入文字:

TextDocumentProxy.InsertText("walterlv");

如果要插入换行或者确认输入,则使用:

TextDocumentProxy.InsertText("\n");

在光标处删除前一个字:

TextDocumentProxy.DeleteBackward();

如果想要清空文本,则可以循环删除:

while (TextDocumentProxy.HasText)
{
    TextDocumentProxy.DeleteBackward();
}

你没有办法删除后一个字,也不能获取到用户输入的任何内容。

关于换行,特别注意:如果文本框被设置为发送或者其他非换行的功能,那么使用 InsertText 单独插入换行时才能正常执行这些功能。如果调用此代码之前还有其他的插入文字,那么最终就只会是换行,而不会执行其他的功能。实际上我在这一点上踩了坑,导致在 QQ 或者其他工具中只能实现换行,而无法发送消息。

iOS 的键盘有不同种类的确认,需要键盘针对 TextDocumentProxy. 我还没有找到办法直接完成文本的输入,例如执行确认按钮的逻辑。而确认按钮有这么些不同的情况:

// 我当然是写 C# 语言版本的枚举,而不是 Object-C 版本的啦。
public enum UIReturnKeyType : long
{
    Default,
    Go,
    Google,
    Join,
    Next,
    Route,
    Search,
    Send,
    Yahoo,
    Done,
    EmergencyCall,
    Continue,
}

添加键盘的网络访问支持

允许完全访问(包括网络)

纯本地的键盘很难在打字速度上获得优势,各种主流的输入法也通常借助网络来提高自身的输入准确度。

用户需要在键盘设置里面开启键盘的“允许完全访问”才能让对应的输入法获得网络访问的权限。如果用户没有给权限,那么网络访问的时候键盘扩展就会出现异常,然后闪退。

允许完全访问

然而如果你去我们刚刚开发的输入法中看,你会发现我们的输入法没有提供这样的选项可以设置。那么如何能够添加这个设置以便进行网络访问呢?

方法是修改键盘扩展项目的 Info.plist 文件。这个时候的修改,我们就不能使用 Visual Studio 中自带的 plist 编辑器了,我们需要使用文本编辑器来编辑 plist 文件。

在你的 Info.plist 文件中找到 RequestsOpenAccess 属性,然后将它分值从 false 改为 true

    <key>RequestsOpenAccess</key>
--  <false/>
++  <true/>

这个属性设为 true 之后,再次部署,你将可以在你的键盘设置里面看到“允许完全访问”的设置项。开启之后,你就能在你的键盘里面访问网络了。

允许访问 http 不安全网络

一般来说你不用阅读这一小节的内容。因为现在基本上各种服务都已经是 https 了,http 基本已经绝迹。但是如果你需要临时部署一个服务,没来得及申请 https 证书的话,那么就需要使用本小结的内容让你的键盘支持 http 的访问。

继续打开你的键盘扩展项目的 Info.plist 文件,在根字典的最后添加一个完整的字典属性 NSAppTransportSecurity

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>walterlv.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

特别注意,里面的 walterlv.com 需要换成你自己的域名。是域名,不用包含端口号。

这样,你就能在键盘中访问 http://walterlv.com 了。

本文总结

  1. 本文介绍了使用 Xamarin 开发 iOS 键盘插件的背景知识。
    • 必须了解这些知识才不会在一些不太重要的坑上耗费太长时间。
  2. 本文教大家如何开发 iOS 键盘插件,主要是项目组织以及写代码。
    • 至少,使用文本编写出来的代码,能够在不作任何修改的情况下部署到真机。(实际上我们只在 KeyboardViewController.cs 中加了寥寥几行代码。)
  3. 本文不涉及到搭建开发环境,不涉及如何连接真机调试。

如果你还遇到了一些其他诡异的问题:


参考资料

如何为你的 Windows 应用程序关联一种或多种文件类型

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

对于 Windows 桌面应用来说,让应用关联一种或多种文件类型是通过修改注册表来实现的。

本文介绍如何为你的应用关联自定义的文件类型或者关联被广泛使用的文件类型。


文件关联

Windows 上的文件关联是通过文件的扩展名来实现的。有些文件类型是被广泛使用的公共类型,例如 .txt、.png、.mp4 文件;有些则是你自己的应用程序使用的私有类型,例如我自己定义一个 .lvyi 扩展名的文件类型。

我们会关联这些广泛使用的类型可能是因为我们自己写了一个自己的文本编辑器,于是我们会关联 .txt 或者 .md 类型。而我们关联自定义的文件类型是因为我们需要为我们自己的应用生态产生一些文件数据。

那么问题来了,我怎么知道我现在准备使用的扩展名是不是已经被广泛使用的公共类型呢?请进入此网站查看:Media Types

注册一个文件类型

要在 Windows 系统上注册一个文件类型,你需要做三个步骤:

  1. 取一个应用程序标识符(ProgID
  2. 在注册表中添加文件关联(用于告知 Windows 这个文件已经被关联)
  3. 为关联的程序添加谓词(用于打开这个文件)

取一个应用程序标识符

没错,我说的就是取名字,而且要求在 Windows 系统上全局唯一;所以这里取名字也是有讲究的。关于应用程序标识符的相关内容,可以阅读微软的官方文档:Programmatic Identifiers - Windows applications - Microsoft Docs

微软建议的 ProgID 的取名方式是这样的:

厂商名.应用名.版本号

这里的版本号通常是指的大版本号。例如版本号为 1.6.0.97 的应用,通常只取第一位,即 1。一个典型的建议的取名示例是这样的:

Walterlv.Foo.1

还是看微软自己的命名示例会更权威一点:

来自微软的 ProgID 命名示例

竟然取一个名字也能写这么多篇幅,看来程序员的命名果然是世界上的一大难题呀!赶紧试用一下我的命名神器吧 —— 点击下载,其原理可阅读 冷算法:自动生成代码标识符(类名、方法名、变量名) - 吕毅

在注册表中添加文件关联

你需要在注册表的 HKEY_LOCAL_MACHINE\Software\Classes 或者 HKEY_CURRENT_USER\Software\Classes 添加一些子键:

HKEY_CURRENT_USER\Software\Classes
    .walv
        (Default) = Walterlv.Foo.1
    .lvyi
        (Default) = Walterlv.Foo.1
    Walterlv.Foo.1
        (Default) = 吕毅的示例文件
        Shell
            Open
                Command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" "%1"
                      

前面的 .walvlvyi 是我自己定义的两种文件类型,我将它们的 (Default) 值设置成 Walterlv.Foo.1;而 Walterlv.Foo.1 就是前面说的应用程序标识符(ProgID)。后面的又新建了一个 Walterlv.Foo.1 的键,其 (Default) 值设置成了我们这个应用关联时使用的名称,也就是资源管理器中显示这个文件的时候使用的名称。

在注册表中的 Walterlv.Foo.1

只要我们完成了以上的步骤,我们就能在资源管理器中看到我们的文件关联(虽然双击打不开):

在资源管理器中看到的文件关联

关于注册表路径的说明

HKEY_LOCAL_MACHINE 主键是此计算机上的所有用户共享的注册表键值,而 HKEY_CURRENT_USER 是当前用户使用的注册表键值。而我们在注册表的 HKEY_CLASSES_ROOT 中也可以看到跟 HKEY_LOCAL_MACHINE\Software\ClassesHKEY_CURRENT_USER\Software\Classes 中一样的文件关联项,是因为 HKEY_CLASSES_ROOTHKEY_LOCAL_MACHINE\Software\ClassesHKEY_CURRENT_USER\Software\Classes 合并之后的一个视图,其中用户键值会覆盖此计算机上的相同键值。

也就是说,如果你试图修改文件关联,那么需要去 HKEY_LOCAL_MACHINE\Software\ClassesHKEY_CURRENT_USER\Software\Classes 中,但如果只是去查看文件关联的情况,则只需要去 HKEY_CLASSES_ROOT 中。

写入计算机范围内的注册表项需要管理员权限,而写入用户范围内的注册表项不需要管理员权限;你可以酌情选用。

为关联的程序添加谓词

我们需要为关联的程序添加谓词才能够使用我们的程序打开这个文件。通常进行文件关联时最常用的谓词是 open,添加路径为 HKEY_CURRENT_USER\Software\Classes\Walterlv.Foo.1\shell\Open\Command。添加后,我们可以在文件资源管理器中通过双击打开这个文件。

Walterlv.Foo.1
    (Default) = 吕毅的示例文件
    shell
        Open
            Command
                (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" -f "%1"

其中路径后面的 "%1" 是文件资源管理器传入的参数,其实就是文件的完整路径。我们加上了引号是避免解析命令行的时候把包含空格的路径拆成了多个参数。

还可以添加其他谓词,有一些是预定义的谓词,你也可以随便写其他的谓词。另外,还可以定义文件的图标。

Walterlv.Foo.1
    (Default) = 吕毅的示例文件
    DefaultIcon = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\lvyi-icon.ico"
    shell
        Open
            Command
                (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" open -f "%1"
        用逗比的方式打开
            Command
                (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" open -f "%1" --doubi

反注册文件类型

当你卸载你的程序的时候,需要反注册之前注册过的文件类型;而反注册的过程并不是把以上的过程完全反过来。

微软推荐我们只删除 ProgID 的键,而不删除文件扩展名的键;因为其他的程序可能已经关联了我们的文件扩展名。就算我们使用的是私有的格式,也有可能是我们程序的未来版本会关联这个扩展名。

总之,你需要做的,只是删除 ProgID 的键,文件扩展名的键不要去动它,Windows 自己会处理好 ProgID 删除之后文件关联的问题的。

一个完整的文件关联示例

HKEY_CLASSES_ROOT
    .walv
        (Default) = Walterlv.Foo.1
    .lvyi
        (Default) = Walterlv.Foo.1
        Content Type = text/xml
    Walterlv.Foo.1
        (Default) = Walterlv Foo
        AlwaysShowExt = 1
        DefaultIcon = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\lvyi-icon.ico"
        FriendlyTypeName = 吕毅的示例文件
        shell
            Open
                Command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" open -f "%1"
            用逗比的方式打开
                Command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" open -f "%1" --doubi
            Edit
                Command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" edit -f "%1"
            print
                command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" print -f "%1"
            printto
                command
                    (Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\walterlv.exe" print -f "%1" -t "%2"

参考资料

通过重写预定义的 Target 来扩展 MSBuild / Visual Studio 的编译过程

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

MSBuild 的编译过程提供了一些可以被重写的 Target,通过重写这些 Target 可以扩展 MSBuild 的编译过程。


重写预定义的 Target

有这些预定义的 Target 可以重写:

  • BeforeCompile, AfterCompile
  • BeforeBuild, AfterBuild
  • BeforeRebuild, AfterRebuild
  • BeforeClean, AfterClean
  • BeforePublish, AfterPublish
  • BeforeResolveReference, AfterResolveReferences
  • BeforeResGen, AfterResGen

你可以在 Microsoft.NET.Sdk 中找到各种富有创意的 Target 用来扩展,以上这些也是 Microsoft.NET.Sdk 的一部分,在那个文件夹的 Microsoft.Common.targets 或者 Microsoft.Common.CurrentVersion.targets 中。

而写法是这样的:

<Project>
    ...
    <Target Name="BeforeResGen">
        <!-- 这里可以写在生成资源之前执行的 Task 或者修改属性和集合。 -->
    </Target>
    <Target Name="AfterCompile">
        <!-- 这里可以写在 C# 文件以及各种资源文件编译之后执行的 Task 或者修改属性和集合。 -->
    </Target>
</Project>

是的,相比于你全新定义一个 Target 来说,你不需要去写 BeforeTargets 或者 AfterTargets。

那么以上那些 Target 都是什么时机呢?

BeforeCompile, AfterCompile

在 C# 文件以及各种资源文件被编译成 dll 的之前或之后执行。你可以在之前执行以便修改要编译的 C# 文件或者资源文件,你也可以在编译之后做一些其他的操作。

由于我们可以在 BeforeCompile 这个时机修改源码,所以我们很多关于代码级别的重新定义都可以在这个时机去完成。

BeforeBuild, AfterBuild

在整个编译之前或者之后执行。对于普通的编译来说,一般来说不会有比 BeforeBuild 更前以及比 AfterBuild 更后的时机了,不过如果有其他 Import 进来的 Target 或者通过 NuGet 自动引入进来的其他 Target 也使用了类似这样的时机,那么你就不一定比他们更靠前或者靠后。

BeforeRebuild, AfterRebuild

如果编译时采用了 /t:Rebuild 方案,也就是重新编译,那么 BeforeRebuild 和 AfterRebuild 就会被触发。一旦触发,会比前面更加提前和靠后。

执行顺序为:BeforeRebuild -> Clean -> Build -> AfterRebuild

BeforeClean, AfterClean

在清理开始和结束时执行。如果是重新编译,那么也会有 Clean 的过程。顺序见上面。

BeforePublish, AfterPublish

在发布之前执行和发布之后执行。对应到 Visual Studio 右键菜单中的发布按钮。

BeforeResolveReference, AfterResolveReferences

在程序集的引用被解析之前和之后执行。你可以通过重写这两个时机的 Target 来修改程序集的引用关系或者利用引用执行一些其他操作。

BeforeResGen, AfterResGen

在资源被生成之前和之后执行。

通过改写 DependsOn 的值扩展编译

有这些预定义的 DependsOn 可以改写:

  • BuildDependsOn
  • CleanDependsOn
  • CompileDependsOn

这几个属性的时机跟上面是一样的,你可以直接通过阅读上面一节中对应名字的 Target 的解释来获得这几个属性所对应的时机。

而这几个属性影响编译过程的写法是这样的:

<PropertyGroup>
    <BuildDependsOn>WalterlvDemoTarget1;$(BuildDependsOn);WalterlvDemoTarget1</BuildDependsOn>
</PropertyGroup>
<Target Name="WalterlvDemoTarget1">  
    <Message Text="正在运行 WalterlvDemoTarget1……"/>  
</Target>  
<Target Name="WalterlvDemoTarget1">  
    <Message Text="正在运行 WalterlvDemoTarget2……"/>  
</Target>

更推荐使用 DependsOn 属性的改写而不是像本文第一节那样直接重写 Target,是因为一个 Target 的重写很容易被不同的开发小伙伴覆盖。比如一个小伙伴在一处代码里面写了一个 Target,但另一个小伙伴不知道,在另一个地方也写了相同名字的 Target,那么这两个 Target 也会相互覆盖,导致其中的一个失效。

虽然同名的属性跟 Target 一样的会被覆盖,但是我们可以通过在改写属性的值的时候同时获取这个属性之前设置的值,可以把以前的值保留下来。

正如上面的例子那样,我们通过写了两个新的 Target 的名字,分别叠加到 $(BuildDependsOn) 这个属性原有值的两边,使得我们可以在编译前后执行两个不同的 Target。如果有其他的小伙伴使用了相同的方式去改写这个属性的值,那么它获取原有值的时候就会把这里已经赋过的值放入到它新的值的中间。也就是说,一个也不会丢。


参考资料

在 Target 中获取项目引用的所有依赖(dll/NuGet/Project)的路径

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

在项目编译成 dll 之前,如何分析项目的所有依赖呢?可以在在项目的 Target 中去收集项目的依赖。

本文将说明如何在 Target 中收集项目依赖的所有 dll 的文件路径。


编写 Target

<Target Name="WalterlvDemoTarget" BeforeTargets="CoreCompile">
    <Message Text="References:" />
    <Message Text="@(Reference)" />
  </Target>

这个 Target 的作用是将项目的所有 Reference 节点作为集合输出出来。然而实际上如果真的编译这个项目,会发现我们得到的结果有一些问题:

  1. 实际上其值就是写到每一个 Reference 里面的字符串的集合
    • 比如引用了 System.Xaml,那么这里就会是 System.Xaml
  2. 如果引用是通过 ProjectReference 进行的项目引用,那么这里就没有目标项目的 dll

所以,我们需要一个新的属性来查找引用的 dll。通过 研究 Microsoft.NET.Sdk 的源码,我发现有 ReferencePath 属性可以使用,于是将 Target 改为这样:

<Target Name="WalterlvDemoTarget" BeforeTargets="CoreCompile;ResolveAssemblyReference">
    <Message Text="ReferencePaths:" />
    <Message Text="@(ReferencePath)" />
  </Target>

现在得到的所有依赖字符串则没有以上的问题。

注意,我在 BeforeTargets 上增加了一个 ResolveAssemblyReference

以上 Target 的输出

引用通常很多,所以我将以上的输出单独放到这里来,避免影响到上面一节知识的阅读。

Reference 的输出

可以看到,Reference 的输出几乎就是 Reference 中写的字符串本身。

CefSharp, Version=57.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86
CefSharp.Core, Version=57.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86
CefSharp.WinForms, Version=57.0.0.0, Culture=neutral, PublicKeyToken=40c4b6fc221f4138, processorArchitecture=x86
Microsoft.Expression.Interactions, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL
System.IO.Compression.FileSystem
System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL
WindowsFormsIntegration
C:\Users\walterlv\.nuget\packages\walterlv.demopackage\1.0.0.0\lib\net47\Walterlv.DemoPackageLibrary.dll
PresentationCore
System.ComponentModel.Composition
System.Configuration
System.Windows.Forms
WindowsBase
PresentationFramework
System.Xaml
System.ServiceModel
System
System.Data
System.Data.DataSetExtensions
System.Management
System.Net.Http
System.Runtime.Serialization
System.ServiceProcess
System.Web
System.Xml
System.Xml.Linq
System.Drawing
Microsoft.CSharp
System.Core

ReferencePath 的输出

可以看到,ReferencePath 则是将所有的 dll 的路径也输出了,而且即便是项目引用,项目编译好的 dll 的路径也在。

D:\Walterlv\Demo\Walterlv.Demo\Code\_Externals\Refs\Cef\x86\CefSharp.Core.dll
D:\Walterlv\Demo\Walterlv.Demo\Code\_Externals\Refs\Cef\x86\CefSharp.dll
D:\Walterlv\Demo\Walterlv.Demo\Code\_Externals\Refs\Cef\x86\CefSharp.WinForms.dll
C:\Users\walterlv\.nuget\packages\walterlv.demopackage\1.0.0.0\lib\net47\Walterlv.DemoPackageLibrary.dll
D:\Walterlv\Demo\Walterlv.Demo\Walterlv.Library1\bin\Debug\Walterlv.Library1.dll
D:\Walterlv\Demo\Walterlv.Demo\Walterlv.Library2\bin\Debug\Walterlv.Library2.dll
D:\Walterlv\Demo\Walterlv.Demo\Walterlv.Library3\bin\Debug\Walterlv.Library3.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Microsoft.CSharp.dll
D:\Walterlv\Demo\Walterlv.Demo\Code\_Externals\Refs\Microsoft.Expression.Interactions.dll
C:\Users\walterlv\.nuget\packages\windowsapicodepackshell\1.1.0.8\lib\NET45\Microsoft.WindowsAPICodePack.dll
C:\Users\walterlv\.nuget\packages\windowsapicodepackshell\1.1.0.8\lib\NET45\Microsoft.WindowsAPICodePack.Shell.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\mscorlib.dll
C:\Users\walterlv\.nuget\packages\newtonsoft.json\11.0.2\lib\net45\Newtonsoft.Json.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\PresentationCore.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\PresentationFramework.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.ComponentModel.Composition.dll
C:\Users\walterlv\.nuget\packages\system.composition.attributedmodel\1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll
C:\Users\walterlv\.nuget\packages\system.composition.convention\1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll
C:\Users\walterlv\.nuget\packages\system.composition.hosting\1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll
C:\Users\walterlv\.nuget\packages\system.composition.runtime\1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll
C:\Users\walterlv\.nuget\packages\system.composition.typedparts\1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Configuration.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Core.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Data.DataSetExtensions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Data.dll
C:\Users\walterlv\.nuget\packages\system.data.sqlite.core\1.0.97\lib\net45\System.Data.SQLite.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Drawing.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.IO.Compression.FileSystem.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Management.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Net.Http.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Runtime.Serialization.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.ServiceModel.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.ServiceProcess.dll
C:\Users\walterlv\.nuget\packages\system.valuetuple\4.5.0\ref\portable-net40+sl4+win8+wp8\System.ValueTuple.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Web.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Windows.Forms.dll
D:\Walterlv\Demo\Walterlv.Demo\Code\_Externals\Refs\System.Windows.Interactivity.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Xaml.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Xml.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.Xml.Linq.dll
C:\Users\walterlv\.nuget\packages\texteditorplus\1.0.0.903\lib\NET45\TextEditorPlus.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\WindowsBase.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\WindowsFormsIntegration.dll
C:\Users\walterlv\.nuget\packages\wpfmediakit\3.0.2.78\lib\NET45\WPFMediaKit.dll
obj\Debug\Interop.IWshRuntimeLibrary.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Collections.Concurrent.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Collections.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ComponentModel.Annotations.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ComponentModel.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ComponentModel.EventBasedAsync.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Diagnostics.Contracts.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Diagnostics.Debug.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Diagnostics.Tools.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Diagnostics.Tracing.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Dynamic.Runtime.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Globalization.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.IO.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Linq.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Linq.Expressions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Linq.Parallel.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Linq.Queryable.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Net.NetworkInformation.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Net.Primitives.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Net.Requests.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ObjectModel.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.Emit.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.Emit.ILGeneration.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.Emit.Lightweight.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.Extensions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Reflection.Primitives.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Resources.ResourceManager.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.Extensions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.InteropServices.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.InteropServices.WindowsRuntime.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.Numerics.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.Serialization.Json.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.Serialization.Primitives.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Runtime.Serialization.Xml.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Security.Principal.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ServiceModel.Duplex.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ServiceModel.Http.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ServiceModel.NetTcp.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ServiceModel.Primitives.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.ServiceModel.Security.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Text.Encoding.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Text.Encoding.Extensions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Text.RegularExpressions.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Threading.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Threading.Tasks.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Threading.Tasks.Parallel.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Xml.ReaderWriter.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Xml.XDocument.dll
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\Facades\System.Xml.XmlSerializer.dll

解读原因

解析引用的 dll 的路径的 Task 是 ResolveAssemblyReference,你可以在 Microsoft.NET.Sdk 文件夹 中找到它。如果想知道 Task 是什么意思,可以阅读:理解 C# 项目 csproj 文件格式的本质和编译流程

<ResolveAssemblyReference
    Assemblies="@(Reference)"
    AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"
    TargetFrameworkDirectories="@(_ReferenceInstalledAssemblyDirectory)"
    InstalledAssemblyTables="@(InstalledAssemblyTables);@(RedistList)"
    IgnoreDefaultInstalledAssemblyTables="$(IgnoreDefaultInstalledAssemblyTables)"
    IgnoreDefaultInstalledAssemblySubsetTables="$(IgnoreInstalledAssemblySubsetTables)"
    CandidateAssemblyFiles="@(Content);@(None)"
    SearchPaths="$(AssemblySearchPaths)"
    AllowedAssemblyExtensions="$(AllowedReferenceAssemblyFileExtensions)"
    AllowedRelatedFileExtensions="$(AllowedReferenceRelatedFileExtensions)"
    TargetProcessorArchitecture="$(ProcessorArchitecture)"
    AppConfigFile="@(_ResolveAssemblyReferencesApplicationConfigFileForExes)"
    AutoUnify="$(AutoUnifyAssemblyReferences)"
    SupportsBindingRedirectGeneration="$(GenerateBindingRedirectsOutputType)"
    IgnoreVersionForFrameworkReferences="$(IgnoreVersionForFrameworkReferences)"
    FindDependencies="$(_FindDependencies)"
    FindSatellites="$(BuildingProject)"
    FindSerializationAssemblies="$(BuildingProject)"
    FindRelatedFiles="$(BuildingProject)"
    Silent="$(ResolveAssemblyReferencesSilent)"
    TargetFrameworkVersion="$(TargetFrameworkVersion)"
    TargetFrameworkMoniker="$(TargetFrameworkMoniker)"
    TargetFrameworkMonikerDisplayName="$(TargetFrameworkMonikerDisplayName)"
    TargetedRuntimeVersion="$(TargetedRuntimeVersion)"
    StateFile="$(ResolveAssemblyReferencesStateFile)"
    InstalledAssemblySubsetTables="@(InstalledAssemblySubsetTables)"
    TargetFrameworkSubsets="@(_ReferenceInstalledAssemblySubsets)"
    FullTargetFrameworkSubsetNames="$(FullReferenceAssemblyNames)"
    FullFrameworkFolders="$(_FullFrameworkReferenceAssemblyPaths)"
    FullFrameworkAssemblyTables="@(FullFrameworkAssemblyTables)"
    ProfileName="$(TargetFrameworkProfile)"
    LatestTargetFrameworkDirectories="@(LatestTargetFrameworkDirectories)"
    CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
    DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"
    ResolvedSDKReferences="@(ResolvedSDKReference)"
    WarnOrErrorOnTargetArchitectureMismatch="$(ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch)"
    IgnoreTargetFrameworkAttributeVersionMismatch ="$(ResolveAssemblyReferenceIgnoreTargetFrameworkAttributeVersionMismatch)"
    FindDependenciesOfExternallyResolvedReferences="$(FindDependenciesOfExternallyResolvedReferences)"
    ContinueOnError="$(ContinueOnError)"
    Condition="'@(Reference)'!='' or '@(_ResolvedProjectReferencePaths)'!='' or '@(_ExplicitReference)' != ''"
    >

    <Output TaskParameter="ResolvedFiles" ItemName="ReferencePath"/>
    <Output TaskParameter="ResolvedFiles" ItemName="_ResolveAssemblyReferenceResolvedFiles"/>
    <Output TaskParameter="ResolvedDependencyFiles" ItemName="ReferenceDependencyPaths"/>
    <Output TaskParameter="RelatedFiles" ItemName="_ReferenceRelatedPaths"/>
    <Output TaskParameter="SatelliteFiles" ItemName="ReferenceSatellitePaths"/>
    <Output TaskParameter="SerializationAssemblyFiles" ItemName="_ReferenceSerializationAssemblyPaths"/>
    <Output TaskParameter="ScatterFiles" ItemName="_ReferenceScatterPaths"/>
    <Output TaskParameter="CopyLocalFiles" ItemName="ReferenceCopyLocalPaths"/>
    <Output TaskParameter="SuggestedRedirects" ItemName="SuggestedBindingRedirects"/>
    <Output TaskParameter="FilesWritten" ItemName="FileWrites"/>
    <Output TaskParameter="DependsOnSystemRuntime" PropertyName="DependsOnSystemRuntime"/>
    <Output TaskParameter="DependsOnNETStandard" PropertyName="_DependsOnNETStandard"/>
</ResolveAssemblyReference>

从这个 Task 中可以看出,它还输出了以下这些属性或集合:

  • ReferenceDependencyPaths
  • ReferenceSatellitePaths
  • ReferenceCopyLocalPaths
    • 这是需要拷贝到本地的那些 dll 的路径(不含框架自带的 dll)
  • SuggestedBindingRedirects
  • FileWrites
    • 要写入的一些缓存文件
  • DependsOnSystemRuntime
    • 以上都是集合,唯独这是一个布尔值,表示是否依赖系统运行时

如何在命令行中监听用户输入文本的改变?

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

这真是一个诡异的需求。为什么我需要在命令行中得知用户输入文字的改变啊!实际上我希望实现的是:在命令行中输入一段文字,然后不断地将这段文字发往其他地方。

本文将介绍如何监听用户在命令行中输入文本的改变。


在命令行中输入有三种不同的方法:

  • Console.Read()
    • 用户可以一直输入,在用户输入回车之前,此方法都会一直阻塞。而一旦用户输入了回车,你后面的 Console.Read 就不会一直阻塞了,直到把用户在这一行输入的文字全部读完。
  • Console.ReadKey()
    • 用户输入之前此方法会一直阻塞,用户只要按下任何一个键这个方法都会返回并得到用户按下的按键信息。
  • Console.ReadLine()
    • 用户可以一直输入,在用户输入回车之前,此方法都会一直阻塞。当用户输入了回车之后,此方法会返回用户在这一行输入的字符串。

从表面上来说,以上这三个方法都不能满足我们的需求,每一个方法都不能直接监听用户的输入文本改变。尤其是 Console.Read()Console.ReadLine() 方法,在用户输入回车之前,我们都得不到任何信息。看起来我们似乎只能通过 Console.ReadKey() 来完成我们的需求了。

但是,一旦我们使用了 Console.ReadKey(),我们将不能获得另外两个方法中的输入体验。例如,我们按下退格键(BackSpace)可以删除光标的前一个字符,按下删除键(Delete)可以删除光标的后一个字符,按下左右键可以移动光标到合适的文本上。

然而,不幸的是,除了这三个方法,我们还真的没有原生的方法来实现命令行的输入监听了。所以看样子我们需要自己来使用 Console.ReadKey() 实现用户输入文字的监听了。

我在 如何让 .NET Core 命令行程序接受密码的输入而不显示密码明文 - walterlv 一问中有说到如何在命令行中输入密码而不会显示明文。我们用到的就是此博客中所述的方法。

var builder = new StringBuilder();
while (true)
{
    var i = Console.ReadKey(true);

    if (i.Key == ConsoleKey.Enter)
    {
        Console.WriteLine();
        // 用户在这里输入了回车,于是我们需要结束输入了。
    }

    if (i.Key == ConsoleKey.Backspace)
    {
        if (builder.Length > 0)
        {
            Console.Write("\b \b");
            builder.Remove(builder.Length - 1, 1);
        }
    }
    else
    {
        builder.Append(i.KeyChar);
        Console.Write(i.KeyChar);
    }
}

然而实际上在使用此方法的时候并不符合预期,因为退格的时候我们得到了半个字:

我们得到了半个字

额外的,我们还不支持左右键移动光标,而且按住控制键的时候也会输入一个字符;这些都是我还没有处理的。

这就意味着我们使用 "\b \b" 来删除我们输入的字符的时候,有可能在一些字符的情况下我们需要删除两个字符宽度。

然而如何获取一个字的字符宽度呢?还是很复杂的。于是我很暴力地使用 OnChar函数的中文处理问题,退格键时,怎么处理-CSDN论坛 论坛中使用的方法直接通过编码范围判断中文的方式来推测字符宽度。如果你有更正统的方法,非常欢迎指导我。

简单起见,我写了一个类来封装输入文本改变。阅读以下代码,或者访问 Walterlv.CloudKeyboard/ConsoleLineReader.cs 阅读此类型的最新版本的代码。

using System;
using System.Text;

namespace Walterlv.Demo
{
    public sealed class ConsoleLineReader
    {
        public event EventHandler<ConsoleTextChangedEventArgs> TextChanged;

        public string ReadLine()
        {
            var builder = new StringBuilder();
            while (true)
            {
                var i = Console.ReadKey(true);

                if (i.Key == ConsoleKey.Enter)
                {
                    var line = builder.ToString();
                    OnTextChanged(line, i.Key);
                    Console.WriteLine();
                    return line;
                }

                if (i.Key == ConsoleKey.Backspace)
                {
                    if (builder.Length > 0)
                    {
                        var lastChar = builder[builder.Length - 1];
                        Console.Write(lastChar > 0xA0 ? "\b\b  \b\b" : "\b \b");
                        builder.Remove(builder.Length - 1, 1);
                    }
                }
                else
                {
                    builder.Append(i.KeyChar);
                    Console.Write(i.KeyChar);
                }

                OnTextChanged(builder.ToString(), i.Key);
            }
        }

        private void OnTextChanged(string line, ConsoleKey key)
        {
            TextChanged?.Invoke(this, new ConsoleTextChangedEventArgs(line, key));
        }
    }

    public class ConsoleTextChangedEventArgs : EventArgs
    {
        public ConsoleTextChangedEventArgs(string line, ConsoleKey consoleKey)
        {
            Line = line;
            ConsoleKey = consoleKey;
        }

        public string Line { get; }
        public ConsoleKey ConsoleKey { get; }
    }
}

那么使用的时候,则会简单很多:

var reader = new ConsoleLineReader();
reader.TextChanged += (sender, args) =>
{
    // 这里可以在用户每次输入的文本改变的时候执行。
};

while (true)
{
    // 我在这里循环执行,于是即便用户按了回车,也会继续输入。
    reader.ReadLine();
}

参考资料

仅反射加载(ReflectionOnlyLoadFrom)的 .NET 程序集,如何反射获取它的 Attribute 元数据呢?

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

平时我们获取一个程序集或者类型的 Attribute 是非常轻松的,只需要通过 GetCustomAttribute 方法就能拿到实例然后获取其中的值。但是,有时我们仅为反射加载一些程序集的时候,获取这些元数据就不那么简单了,因为我们没有加载目标程序集中的类型。

本文介绍如何为仅反射加载的程序集读取 Attribute 元数据信息。


仅反射加载一个程序集

使用 ReflectionOnlyLoadFrom 可以仅以反射的方式加载一个程序集。

var extensionFilePath = @"C:\Users\walterlv\Desktop\Walterlv.Extension.dll";
var assembly = Assembly.ReflectionOnlyLoadFrom(extensionFilePath);

获取程序集的 Attribute(例如获取程序集版本号)

Assembly.GetCustomAttributesData() 得到的是一个 CustomAttributeData 的列表,而这个列表中的每一项都与普通反射中拿到的特性集合不同,这里拿到的只是特性的信息(以下循环中的 data 变量)。

CustomAttributeData 中有 AttributeType 属性,虽然此属性是 Type 类型的,但是实际上它只会是 RuntimeType 类型,而不会是真实的 Attribute 的类型(因为不能保证宿主程序域中已经加载了那个类型)。

var customAttributesData = assembly.GetCustomAttributesData();
foreach (CustomAttributeData data in customAttributesData)
{
    // 这里可以针对每一个拿到的慝的信息进行操作。
}

比如我们要获取这个程序集的版本号,正常我们写 assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version,但是这里我们无法生成 AssemblyFileVersionAttribute 的实例,我们只能这么写:

var versionString = assembly.GetCustomAttributesData()
    .FirstOrDefault(x => x.AttributeType.FullName == typeof(AssemblyFileVersionAttribute).FullName)
    ?.ConstructorArguments[0].Value as string ?? "0.0";
var version = new Version(versionString);

代码解读是这样的:

  1. 我们从拿到的所有的 Attribute 元数据中找到第一个名称与 AssemblyFileVersionAttribute 相同的数据;
  2. 从数据的构造函数参数中找到传入的参数值,而这个值就是我们定义 AssemblyFileVersionAttribute 时传入的参数的实际值。

因为我们知道 AssemblyFileVersionAttribute 的构造函数只有一个,所以我们确信可以从第一个参数中拿到我们想要的值。

顺便一提,我们使用 AssemblyFileVersionAttribute 而不是使用 AssemblyVersionAttribute 是因为使用 .NET Core 新格式(基于 Microsoft.NET.Sdk)编译出来的程序集默认是不带 AssemblyVersionAttribute 的。详见:语义版本号(Semantic Versioning) - walterlv


参考资料

让你的 Windows 应用程序在任意路径也能够直接通过文件名执行

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

我们可以在任何路径下输入 explorer 来启动资源管理器,可以在任何路径中输入 git 来使用 git 相关的命令。我们知道可以通过将一个应用程序加入到环境变量中来获得这个效果,但是还有其他的方式吗?

我们将这个过程称之为向 Windows 注册一个应用程序路径。本文介绍向 Windows 注册一个应用程序路径的各种方法。


Windows 如何查找程序路径?

当我们在任意目录中输入一个命令的时候,Windows 会按照如下顺序寻找这个命令对应的可执行程序:

  • 当前的工作目录
  • Windows 文件夹(仅此文件夹,不会搜索子文件夹)
  • Windows\System32 文件夹
  • 环境变量 Path 值中的所有文件夹
  • 注册表 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths

微软 推荐使用 App Paths 即修改此注册表项来添加可执行程序。

当然,你也可以使用当前用户键下的注册表项来实现同样的目的,程序使用当前用户路径写注册表是不需要管理员权限的。HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\App Paths

使用 App Paths 添加可执行程序

在注册表中打开 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths 子键,你可以在里面找到当前通过此方法注册的所有可执行程序。

比如下图是 PowerShell Core 的 msi 包安装后添加的 pwsh.exe 键。

PowerShell Core

现在我们添加一个我们自己开发的程序 walterlv.exe,于是就直接在 App Paths 子键下添加一个 walterlv.exe 的键,并将其默认值设为 walterlv.exe 的完整路径。

新增的 walterlv.exe


参考资料