dotnet 职业技术学院

博客

dotnet 职业技术学院

使用 Task 实现提前加载

刘俊杰 发布于 2018-06-16

介绍一种/两种可以提前做点什么事情的方法。

场景

在UI线程中执行耗时操作,如读取大文件,为了不造成UI卡顿,常采用异步加载的方式,即 async/await 。

通常的写法是这样的:

private async Task DoSomething()
{
    // init work
    await Task.Run(()=>
    {
        // IO          
    });
    // after work 
}

问题与需求

这里虽然解决了UI卡顿的问题,但需要得到最终结果(即 after work 中的代码执行),仍然需要等待。
在部分场景中,如果可以提前执行耗时代码,则可以减少等待时间。

设想的代码类似与这样:


private void EarlierWork()
{
    // 提前执行耗时操作,如从一个大文件中查找数据。
}

priavte void DoSomething()
{
    // init
    // 获取之前提前执行的结果,如果执行还没有结束,则异步等待执行完成,获取结果后继续执行。
    // after work 
}

这里适用的场景,需要是 EarlierWorkDoSomething 先执行一段时间,而且在 EasilerWork 执行时,就已经可以获取相关的数据,不缺失必要的参数。(这样的场景……额,可能不并不多)

实现

这里的实现有两种方式,都是基于 C# 的 async/await 异步模型。

实现一: awaiter

using System.Runtime.CompilerServices;  
using System.Threading.Tasks;

private TaskAwaiter<string> _getStringDataAwaiter;

private void EarlierWork()
{
    // 提前执行耗时操作,如从一个大文件中查找数据。
    _getStringDataAwaiter = Task.Run(()=>
    {
        // 耗时操作
        return "Data";
    }).GetAwaiter();
}

private async Task DoSomething()
{
    // init
    // 获取之前提前执行的结果,如果执行还没有结束,则异步等待执行完成,获取结果后继续执行。
    string data = await Task.Run(()=>_getStringDataAwaiter.GetResult());
    // after work 
}

注意,这里需要使用 Task.Run() 的方式调用获取结果的 GetResult 方法,否则会是调用线程卡顿,即,如果调用线程是UI,则会造成UI卡顿。

需要注意的是,TaskAwaiter 这个API是提供给编译器使用的,不建议在生产环境中使用

The System.Runtime.CompilerServices namespace provides functionality for compiler writers who use managed code to specify attributes in metadata that affect the run-time behavior of the common language runtime.


This API supports the product infrastructure and is not intended to be used directly from your code.
TaskAwaiter Struct (System.Runtime.CompilerServices) | Microsoft Docs

实现二:

推荐的实现方式。

using System.Threading.Tasks;

private Task<string> _getStringDataTask

private void EarlierWork()
{
    // 提前执行耗时操作,如从一个大文件中查找数据。
    _getStringDataTask = CreateGetDataTask();    
}

private async Task<string> CreateGetDataTask()
{
    return await Task.Run(()=>
    {
        // 耗时操作
        return "Data";
    }).ConfigureAwait(false);  // 必须写 ConfigureAwait(false) 

    // 此处的代码将不会返回原线程执行;不过这里一般不写代码。
}

private void DoSomething()
{
    // init
    // 获取之前提前执行的结果,如果执行还没有结束,则异步等待执行完成,获取结果后继续执行。
    string data = _getStringDataTask.Result;
    // after work 
}

这种实现方式需要注意死锁问题,如果不使用 ConfigureAwait(false) ,则会造成死锁。

关于死锁的更多内容,可以看这里:


END

如何让 .NET Core 命令行程序接受密码的输入而不显示密码明文

lvyi 更新于 2018-05-26,发布于 2018-05-26 16:51

如果是在 GUI 中要求用户输入密码,各 UI 框架基本都提供了用于输入密码的控件;在这些控件中,用户在输入密码的时候会显示掩码。然而对于控制台程序来说,并没有用于输入密码的原生方法。

本文将讲述一种在控制台中输入密码,并仅显示掩码的方法。


开始简单的程序

让我们开始一个简单的 .NET Core 控制台程序。

static void Main(string[] args)
{
    Console.Write("用户名: ");
    var userName = Console.ReadLine();
    Console.Write("密  码: ");
    var password = Console.ReadLine();
    Console.ReadKey();
}

初步的程序

密码直接显示,暴露无遗。而且,由于我们后面持续不断的有输出,控制台不会清除掉这些输出,所以密码会一直显示到缓冲区中——这显然是不能接受的

写一个让用户输入密码并显示掩码的方法

既然控制台本身并没有提供可以为密码进行掩码的方法,那么我们只能自己来写了:

public static SecureString ReadPassword(string mask = "*")
{
    var password = new SecureString();
    while (true)
    {
        var i = Console.ReadKey(true);
        if (i.Key == ConsoleKey.Enter)
        {
            Console.WriteLine();
            break;
        }

        if (i.Key == ConsoleKey.Backspace)
        {
            if (password.Length > 0)
            {
                password.RemoveAt(password.Length - 1);
                Console.Write("\b \b");
            }
        }
        else
        {
            password.AppendChar(i.KeyChar);
            Console.Write(mask);
        }
    }
    return password;
}

方法内部接受用户的输入——如果是回车,则确认;如果是退格,则删除一个字;其他情况下输出掩码。全程使用安全的字符串 SecureString,这种字符串是没有办法直接通过托管代码获取值的。

这时再输入字符串,将只能看到掩码——再也看不出来 walterlv 是不是一个逗比 了……

有掩码的输入

转换密码

当然,只有对安全级别比较高的库才会接受 SecureString 类型的字符串作为密码;一些简单的库只接受字符串类型的密码。那么在这些简单的库中我们如何才能得到普通的字符串呢?

可以使用 Marshal 来完成:

private static string ConvertToString(SecureString value)
{
    var valuePtr = IntPtr.Zero;
    try
    {
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
        return Marshal.PtrToStringUni(valuePtr);
    }
    finally
    {
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
    }
}

也可以间接使用 NetworkCredential 完成:

private static string ConvertToString(SecureString secureString)
{
    return new NetworkCredential(string.Empty, secureString).Password;
}

因为 NetworkCredential 的内部其实也是使用类似的方式获取到字符串的(详见 SecureStringHelper.CreateString - Reference Source)。

internal static string CreateString(SecureString secureString)
{
    string plainString;
    IntPtr bstr = IntPtr.Zero;

    if (secureString == null || secureString.Length == 0)
        return String.Empty;

    try
    {
        bstr = Marshal.SecureStringToBSTR(secureString);
        plainString = Marshal.PtrToStringBSTR(bstr);
    }
    finally
    {
        if (bstr != IntPtr.Zero)
            Marshal.ZeroFreeBSTR(bstr);
    }
    return plainString;
}

参考资料

win10 uwp 商业游戏

lindexi 发布于 2018-05-23

本文告诉大家去做一个商业游戏,游戏很简单,几乎没有什么技术。

C# 已知点和向量,求距离的点

lindexi 发布于 2018-05-23

已知一个点 P 和向量 v ,求在这个点P按照向量 v 运行距离 d 的点 B 。

神器如 dnSpy,无需源码也能修改 .NET 程序

lvyi 发布于 2018-05-22

dnSpy0xd4d 开发的 .NET 程序调试神器。

说它是神器真的毫不为过!它能在完全没有源码的情况下即时调试程序,甚至还能修改程序!本文讲向大家介绍如何使用 dnSpy 修改 .NET 程序。


dnSpy 的主打功能是无需源码的调试,林德熙 有一篇文章 断点调试 Windows 源代码 介绍了这个方法。而本文主要说其另一项强大的功能 —— 修改程序集。

看看 dnSpy

dnSpy

dnSpy 长着一身 Visual Studio 一样的外观,调试的时候给你熟悉的感觉。

我们只需要讲我们需要调试或修改的程序集拖入左侧的程序集列表中即可(它会自动为我们把此程序集依赖的程序集也添加进来)。我把以前我写过的一个程序 ManipulationDemo 拖进来了。

实操修改程序集

现在我们来修改它,修改什么好呢?为了让效果明显一点,我决定在启动时弹一个窗口。于是我们展开进入到 App 类中。

App 类

然后在类中右键“Edit class (C#)”:

右键 -> 编辑类

在里面重写 OnStartup 方法。发现,它竟然连智能感知提示都做了!

重写 OnStartup 方法

MessageBox.Show

改完只需要点击一下右下角的编译,即可讲修改应用到我们刚刚打开的程序集中。

编译

保存修改的程序集

如果只是修改了可以立刻运行,那么充其量只是可以辅助调试。但是 dnSpy 是可以将程序集另存到本地的。

点击“File”->“Save Module”:

保存模块

为了以示区分,我写了一个新的名字:

保存

保存完之后,运行:

运行

我们会发现,我们刚刚新增的对话框已经弹出来了。“OK”之后原来的窗口才会显示出来。

发挥想象力的时候到了

既然有如此简单的修改程序集的方法,那么我们可以用来做什么事儿呢?用来做什么事儿呢?做什么事儿呢?什么事儿呢?事儿呢?呢?

想象力时间

顺便说一下,就算程序集被混淆了也难不倒它。

新 csproj 对 WPF/UWP 支持不太好?有第三方 SDK 可以用!MSBuild.Sdk.Extras

lvyi 发布于 2018-05-22

自从微软推出 .NET Core 以来,新的项目文件格式以其优秀的可扩展性正吸引着更多项目采用。然而——微软官方的 WPF/UWP 项目模板依然还在采用旧的 csproj 格式!

这只是因为——官方 SDK 依然对 WPF/UWP 支持不够友好。


为什么要使用第三方的 SDK?

关于项目文件格式的迁移,我和 林德熙 都写过文章:

不过,这两篇文章中的迁移方法都是手动或半自动迁移的。而且迁移完毕之后,对新增的 WPF/UWP XAML 文件的支持非常不友好——新增的 XAML 文件是看不见的,除非手工去 csproj 文件中去掉自动生成的 Remove XAML 的代码。

这确实阻碍着我们在 WPF/UWP 项目中体会到新风格 csproj 的好处。

微软在 Build 2018 大会上宣布,WPF/UWP 将能够在 .NET Core 3 中运行。想必,微软会为未来版本的 Microsoft.NET.Sdk 这样的官方 SDK 添加更多的 WPF/UWP 这类格式的支持吧!即便没有这样的原生支持,想必也会提供官方的扩展方案。

但在此之前呢?感谢小伙伴 KodamaSakuno (神樹桜乃) 提醒我第三方 SDK 的存在 —— MSBuild.Sdk.Extras。我想,在 .NET Core 3 推出之前,这是一种不错的中转方案。既能体会到新风格 csproj 格式的好处,也能在将来 .NET Core 3 官方支持后较快地迁移成官方版本。

如何使用 MSBuild.Sdk.Extras

虽说是第三方 SDK,但实际使用的方便程度却如官方般简洁!只需要将 SDK 替换成 MSBuild.Sdk.Extras/1.5.4 即可。1.5.4 是目前 MSBuild.Sdk.Extras 在 NuGet 上的最新版本,建议访问 NuGet Gallery - MSBuild.Sdk.Extras 使用最新稳定版本。

以下是最简同时支持 WPF 和 UWP 双框架的代码:

<Project Sdk="MSBuild.Sdk.Extras/1.5.4">
  <PropertyGroup>
    <TargetFrameworks>net47;uap10.0</TargetFrameworks>
  </PropertyGroup>
</Project>

没错,真的如此简单!在我们猜测的 .NET Core 3 支持 WPF/UWP 项目格式之前,这应该算是最简单的迁移方案了!

至于项目结构的效果,可以看下图所示:

net47 和 uap10.0

相比于此前的手工迁移,使用此新格式创建出来的 XAML 文件是可见的,而且 .xaml.cs 也是折叠在 .xaml 之下,且能正常编译!(当然,咱们还得考虑 UWP 和 WPF 在 XAML 书写上的细微差异)

官方提供了更多的使用方法,例如更简单的是安装 NuGet 包,而不修改 SDK。详见:onovotny/MSBuildSdkExtras: Extra properties for MSBuild SDK projects

参考资料

如何编写基于 Microsoft.NET.Sdk 的跨平台的 MSBuild Target

lvyi 发布于 2018-05-20

我之前写过一篇 理解 C# 项目 csproj 文件格式的本质和编译流程,其中,Target 节点就是负责编译流程的最关键的节点。但因为篇幅限制,那篇文章不便详说。于是,我在本文说说 Target 节点。


Target 的节点结构

<Target> 内部几乎有着跟 <Project> 一样的节点结构,内部也可以放 PropertyGroupItemGroup,不过还能放更加厉害的 Task。按照惯例,我依然用思维导图将节点结构进行了总结:

Target 的节点结构
▲ 上面有绿线和蓝线区分,仅仅是因为出现了交叉,怕出现理解歧义

<Hash><WriteCodeFragment> 都是 Task。我们可以看到,Task 是多种多样的,它可以占用一个 xml 节点。而本例中,WriteCodeFragment Task 就是生成代码文件,并且将生成的文件作为一项 Compile 的 Item 和 FileWrites 的 Item。在 理解 C# 项目 csproj 文件格式的本质和编译流程 中我们提到 ItemGroup 的节点,其作用由 Target 指定。所有 Compile 会在名为 CoreCompileTarget 中使用,而 FileWrites 在 Microsoft.NET.Sdk 的多处都生成了这样的节点,不过目前从我查看到的全部 Microsoft.NET.Sdk 中,发现内部并没有使用它。

Target 执行的时机和先后顺序

既然 <Target> 内部节点很大部分跟 <Project> 一样,那区别在哪里呢?<Project> 里的 <PropertyGroup><ItemGroup> 是静态的状态,如果使用 Visual Studio 打开项目,那么所有的状态将会直接在 Visual Studio 的项目文件列表和项目属性中显示;而 <Target> 内部的 <PropertyGroup><ItemGroup> 是在编译期间动态生成的,不会在 Visual Studio 中显示;不过,它为我们提供了一种在编译期间动态生成文件或属性的能力。

总结起来就是——Target 是在编译期间执行的

不过,同样是编译期间,那么多个 Target,它们之间的执行时机是怎么确定的呢?

一共有五个属性决定了 Target 之间的执行顺序:

  • Project 的属性
    • InitialTargets 项目初始化的时候应该执行的 Target
    • DefaultTargets 如果没有指定执行的 Target,那么这个属性将指定执行的 Target
  • Target 的属性
    • DependsOnTargets 在执行此 Target 之前应该执行的另一个或多个 Target
    • BeforeTargets 这是 MSBuild 4.0 新增的,指定应该在另一个或多个 Target 之前执行
    • AfterTargets 这也是 MSBuild 4.0 新增的,指定应该在另一个或多个 Target 之后执行

通过指定这些属性,我们的 Target 能够被 MSBuild 自动选择合适的顺序进行执行。例如,当我们希望自定义版本号,那么就需要赶在我们此前提到的 GenerateAssemblyInfo 之前执行。

Microsoft.NET.Sdk 为我们提供的现成可用的 Task

有 Microsoft.NET.Sdk 的帮助,我们可以很容易地编写自己的 Target,因为很多功能它都帮我们实现好了,我们排列组合一下就好。

提供的 Task 还有更多,如果上面不够你写出想要的功能,可以移步至官方文档翻阅:MSBuild Task Reference - Visual Studio - Microsoft Docs

使用自己写的 Task

我有另外的一篇文章来介绍如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包 - 吕毅。如果希望自己写 Ta

差量编译

如果你认为自己写的 Target 执行比较耗时,那么就可以使用差量编译。我另写了一篇文章专门来说 Target 的差量编译:每次都要重新编译?太慢!让跨平台的 MSBuild/dotnet build 的 Target 支持差量编译 - 吕毅


参考资料

WPF 使用RPC调用其他进程

lindexi 发布于 2018-05-19

如果在 WPF 需要用多进程通信,一个推荐的方法是 WCF ,因为 WCF 是 RPC 计算。先来讲下 RPC (Remote Procedure Call) 远程过程调用,他是通过特定协议,包括 tcp 、http 等对其他进程进行调用的技术。详细请看百度

从以前的项目格式迁移到 VS2017 新项目格式

lindexi 发布于 2018-05-18

以前的项目格式使用的是 csproj 的格式,但是 .net core 支持使用 project.json 格式的项目文件,后来还是决定不使用这个格式。 VS2017 的项目格式更好读、更简单而且减少了 git 冲突。 本文来告诉大家如何从 VS2015 和以前的项目格式修改为 VS2017 项目格式。

WPF 使用 SharpDX

lindexi 发布于 2018-05-04

本文告诉大家如何在 WPF 使用 SharpDX ,只是入门。

WPF 使用 SharpDX 在 D3DImage 显示

lindexi 发布于 2018-05-04

本文告诉大家如何使用 SharpDX 在 D3DImage 显示。在上一篇WPF 使用 SharpDX只是使用窗口,也就是无法使用其它的 WPF 控件。所以这一篇就来告诉大家如何使用 WPF 控件和使用 SharpDX 。

WPF 使用 SharpDx 异步渲染

lindexi 发布于 2018-05-04

本文告诉大家如何通过 SharpDx 进行异步渲染,但是因为在 WPF 是需要使用 D3DImage 画出来,所以渲染只是画出图片,最后的显示还是需要 WPF 在他自己的主线程渲染。

win10 uwp 渲染原理 DirectComposition 渲染

lindexi 发布于 2018-04-23

本文来告诉大家一个新的技术DirectComposition,在 win7 之后(实际上是 vista),微软正在考虑一个新的渲染机制。

WPF 使用 Direct2D1 画图入门

lindexi 发布于 2018-04-20

本文来告诉大家如何在 WPF 使用 D2D 画图。

WPF 使用 Direct2D1 画图 绘制基本图形

lindexi 发布于 2018-04-20

本文来告诉大家如何在 Direct2D1 绘制基本图形,包括线段、矩形、椭圆