|
|
单元格输出的格式化
|
|
|
================
|
|
|
## 格式化输出
|
|
|
基于 .NET Interactive 的工具(包括 Polyglot 笔记本、Jupyter 和其他工具)的输出是通过 .NET Interactive 格式化器生成的,这是一组位于 Microsoft.DotNet.Interactive.Formatting 命名空间下的 API。这些 API 可以作为一个 NuGet 包独立于笔记本使用。
|
|
|
格式化器创建对象的字符串表示,这些字符串表示可以从纯文本到 HTML,再到像 JSON 和 CSV 这样的机器可读格式。
|
|
|
|
|
|
以下是一些你可以在笔记本中编写的代码示例,这些代码会导致对象被格式化以供显示:
|
|
|
- C# 单元格末尾的返回语句或尾随表达式
|
|
|
- F# 单元格末尾的尾随表达式
|
|
|
- 调用 Display 和 ToDisplayString 扩展方法,这些方法对 C# 和 F# 中的所有对象都可用
|
|
|
- 在 PowerShell 单元格中调用 Out-Display
|
|
|
|
|
|
格式化器还用于在多语言笔记本变量视图中格式化.NET对象的输出显示。其他语言的值格式化并不依赖于.NET。
|
|
|
> `格式化` 指的是创建对象字符串表示的过程。此过程由.NET Interactive内核通过此处所述的API实现。当格式化后的字符串在 VS Code 或 JupyterLab 中的笔记本上显示时,这一过程被称为`渲染`
|
|
|
## MIME类型 与 Display显示
|
|
|
对于任何一个给定的对象,可以有多种不同的字符串表示方式。这些不同的表示方式都有对应的MIME类型,这些类型由简短的字符串标识,例如`text/html`或`application/json`。MIME类型可以用来通过Display扩展方法请求特定格式的对象显示,这个方法可以用于任何对象。
|
|
|
|
|
|
例如,我们可以调用rect.Display()来显示分配给变量rect的Rectangle对象
|
|
|
```csharp
|
|
|
//注意:System.Drawing不能跨平台,只在Windows系统中使用。
|
|
|
//不知道为啥:官方要选个不跨平台的示例
|
|
|
using System.Drawing;
|
|
|
var ract = new Rectangle
|
|
|
{
|
|
|
Height = 50,
|
|
|
Width = 100,
|
|
|
};
|
|
|
|
|
|
//默认为 text/html,下面两种方式的效果是一样的
|
|
|
ract.Display();
|
|
|
ract.Display("text/html");
|
|
|
```
|
|
|
注意,Polyglot 笔记本中默认的 MIME 类型是 text/html。这可能会因 .NET 类型的不同而有所变化,但在上述示例中,矩形类型尚未应用任何自定义设置。(将在下面展示更多关于如何进行自定义设置的内容。)
|
|
|
> 注意:对于 C# 或 F# 中单元格的返回值,只能使用默认 MIME 类型的格式化程序。
|
|
|
在使用Display时,可以指定不同于默认的MIME类型。只需将所需的MIME类型作为参数传递即可,例如:rect.Display("text/plain")
|
|
|
```csharp
|
|
|
ract.Display("text/plain");
|
|
|
```
|
|
|
另一种常见的MIME类型是application/json。在Polyglot笔记本中使用此MIME类型时,对象使用System.Text.Json进行格式化。
|
|
|
|
|
|
执行下面的单元格,会以Json格式输出。JSON输出中的代码颜色由 VS Code 笔记本渲染器针对`application/json`类型提供。
|
|
|
```csharp
|
|
|
ract.Display("application/json");
|
|
|
```
|
|
|
## 自定义格式化
|
|
|
.NET Interactive的格式化API是高度可配置的。
|
|
|
|
|
|
可以使用`Microsoft.DotNet.Interactive.Formatting`中的代码,调整格式化行为的方式。
|
|
|
### 限制输出数量
|
|
|
在格式化序列时,例如数组或实现IEnumerable的对象,.NET Interactive的格式化器会将它们展开,以便你可以看到其中的值。
|
|
|
|
|
|
如果执行结果集太多,默认情况下只会输出前面很少项,剩余的数据以 `...More` 代替。
|
|
|
```csharp
|
|
|
var names = new string[]
|
|
|
{
|
|
|
"吕宇宁", "韦子异", "姚宇宁", "傅子异", "朱子韬", "林杰宏", "胡璐", "周璐", "田秀英", "姜詩涵", "蔡秀英", "张睿", "金嘉伦", "萧致远", "赵致远", "蒋宇宁", "顾震南", "余安琪", "熊岚", "卢子韬",
|
|
|
"孔杰宏", "周子韬", "黄睿", "史璐", "赵震南", "杜安琪", "赵致远", "石詩涵", "龚杰宏", "丁安琪", "黄杰宏", "傅睿", "戴嘉伦", "郝杰宏", "傅晓明", "孟嘉伦", "段睿", "戴致远", "石安琪", "汪詩涵",
|
|
|
"贾云熙", "邱子韬", "吴杰宏", "贾岚", "曾震南", "许云熙", "吴宇宁", "唐岚", "常嘉伦", "曾岚", "袁嘉伦", "黄晓明", "韦致远", "莫安琪", "丁子韬", "雷云熙", "许秀英", "朱宇宁", "黎詩涵", "贾晓明",
|
|
|
"孔詩涵", "秦宇宁", "方子韬", "邵秀英", "冯宇宁", "何晓明", "方嘉伦", "熊秀英", "沈云熙", "顾秀英", "许致远", "胡宇宁", "陶子异", "叶安琪", "邱震南", "刘子异", "周宇宁", "黄云熙", "龚杰宏", "杜秀英",
|
|
|
"向子异", "马睿", "黄安琪", "于安琪", "金嘉伦", "龚璐", "杨致远", "戴嘉伦", "钟詩涵", "阎詩涵", "雷安琪", "宋杰宏", "田致远", "冯致远", "雷杰宏", "雷子异", "叶璐", "王子异", "冯子韬", "史宇宁"
|
|
|
};
|
|
|
|
|
|
var students = Enumerable.Range(1,50).Select(s => new {Id = s, Age = Random.Shared.Next(1,100), Name = names[s]});
|
|
|
students.Display();
|
|
|
```
|
|
|
Formatter.ListExpansionLimit 更改为自定义值
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.ListExpansionLimit = 5;
|
|
|
students.Display();
|
|
|
```
|
|
|
上面的示例中,通过设置Formatter.ListExpansionLimit = 5,然后显示相同的列表对象,.NET Interactive现在仅显示前五项,后面跟着 ...(more)。
|
|
|
|
|
|
也可以通过设置`Formatter<T>.ListExpansionLimit`来限制特定类型的输出。需要注意的是,这里的类型T必须与列表中的项完全匹配。
|
|
|
|
|
|
以下是一个使用int类型的示例:
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter<int>.ListExpansionLimit = 3;
|
|
|
Enumerable.Range(1, 10).Display();
|
|
|
|
|
|
```
|
|
|
注意:有些以 ...(more)结尾,有些以(数字 more)结尾。
|
|
|
|
|
|
这是因为:List、List<T>、数组等,列举前就知道元素确切的数量,以 `(数字 more)`结尾;而像 IEnumerable<T>(Enumerable.Range 返回类型是IEnumerable<T>)之类的,因此在枚举整个序列之前,无法知道元素确切的数量;在这种情况下,.NET交互式格式化器在达到配置的ListExpansionLimit时会停止,并不会继续计数剩余的序列, 以 `...(more)` 结尾
|
|
|
### 限制对象循环引用次数
|
|
|
有些对象存在引用循环。虽然.NET Interactive格式化器会遍历对象图,但它为了避免输出过大和无限递归,只会递归到特定深度。
|
|
|
|
|
|
以下是一个C#代码示例,它定义了一个简单的Node类,创建了一个引用循环,并使用C#脚本的尾随表达式(相当于返回语句)对其进行格式化:
|
|
|
```csharp
|
|
|
public class Node
|
|
|
{
|
|
|
public Node Next { get; set; }
|
|
|
}
|
|
|
|
|
|
Node node1 = new();
|
|
|
Node node2 = new();
|
|
|
|
|
|
node1.Next = node2;
|
|
|
node2.Next = node1;
|
|
|
|
|
|
node1
|
|
|
```
|
|
|
这表明格式化器在格式化到深度6后停止了递归。这个深度可以通过Formatter.RecursionLimit方法进行更改:
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.RecursionLimit = 2;
|
|
|
node1
|
|
|
```
|
|
|
### 首选 MIME 类型
|
|
|
前面提到,Polyglot 笔记本中用于格式化的默认 MIME 类型是 text/html。当使用 Display() 方法时,如果没有向 mimeType 参数传递值,或者在 C# 或 F# 中使用 return 语句或尾随表达式时,就会应用这个默认设置。这个默认设置可以全局更改,也可以针对特定类型更改。
|
|
|
|
|
|
以下示例将 Rectangle 的默认值更改为 text/plain。
|
|
|
```csharp
|
|
|
public class Student
|
|
|
{
|
|
|
public int Id {get;set;}
|
|
|
public string Name {get;set;}
|
|
|
public int Age {get;set;}
|
|
|
}
|
|
|
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.SetPreferredMimeTypesFor(typeof(Student), "text/plain");
|
|
|
|
|
|
new Student
|
|
|
{
|
|
|
Id = 1,
|
|
|
Name = "张三",
|
|
|
Age = 55
|
|
|
}
|
|
|
```
|
|
|
该方法可用于设置多个首选MIME类型。第二个参数是params参数,它允许您传递多个值。
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.SetPreferredMimeTypesFor
|
|
|
(
|
|
|
typeof(Student),
|
|
|
"text/plain",
|
|
|
"application/json"
|
|
|
);
|
|
|
|
|
|
new Student
|
|
|
{
|
|
|
Id = 1,
|
|
|
Name = "张三",
|
|
|
Age = 55
|
|
|
}
|
|
|
```
|
|
|
注册多个MIME时,可以切换输出格式:
|
|
|
单击 执行结果单元格的最左侧`...`, 选择 `更改演示文稿`后,在VS Code最上访,会弹出选择窗口,选择注册项中的一个,就可以重新以选中项的格式重新显示结果。
|
|
|
### 替换默认的格式化类型
|
|
|
默认格式化器通常通过打印列表和属性来显示对象的值。输出主要是文本形式。如果你希望以不同的方式显示某种类型的内容,无论是不同的文本输出、图像还是图表,你可以通过为特定类型注册自定义格式化器来实现。这些类型可以是你自己定义的,也可以是其他.NET库中定义的。
|
|
|
|
|
|
一个常见的使用自定义格式化器的场景是渲染图表。一些NuGet包,如Plotly.NET,提供了.NET Interactive扩展,利用此功能以交互式的HTML和JavaScript为基础生成输出。
|
|
|
|
|
|
注册自定义格式化器最简单的方法是使用Formatter.Register<T>方法,它有几个不同的重载版本。在笔记本中使用最友好、最方便的一个接受两个参数:
|
|
|
+ 委托:它接受你要注册的类型的对象,并返回一个字符串。在这里,你可以指定所需的字符串转换;
|
|
|
+ MIME类型:只有当使用此MIME类型时,才会调用你的自定义格式化器
|
|
|
|
|
|
下面的示例将 Rectangles 类的实例格式化为SVG矩形。
|
|
|
```csharp
|
|
|
public class Rectangle
|
|
|
{
|
|
|
public int Width {get;set;}
|
|
|
public int Height {get;set;}
|
|
|
}
|
|
|
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.Register<Rectangle>
|
|
|
(
|
|
|
formatter: //格式化方法:把类型对象,转化为输出字符串
|
|
|
rect => $"""
|
|
|
<svg width="100" height="100">
|
|
|
<rect
|
|
|
width="{rect.Width}"
|
|
|
height="{rect.Height}"
|
|
|
style="fill:rgb(0,255,200)"
|
|
|
/>
|
|
|
</svg>
|
|
|
""",
|
|
|
mimeType: //输出媒体类型
|
|
|
"text/html"
|
|
|
);
|
|
|
```
|
|
|
```csharp
|
|
|
/*
|
|
|
运行此代码后,Rectangle对象将显示为图形矩形,而不是属性值列表。
|
|
|
(以下示例使用C#脚本尾随表达式语法,该语法通常配置为在笔记本中使用text/html MIME类型。)
|
|
|
*/
|
|
|
new Rectangle(){Width = 100, Height = 50}
|
|
|
```
|
|
|
特别指出:当在列表中遇到自定义类型或作为对象属性时,仍将调用自定义格式化程序
|
|
|
```csharp
|
|
|
new[]
|
|
|
{
|
|
|
new Rectangle(){Width = 200, Height = 50},
|
|
|
new Rectangle(){Width = 200, Height = 100}
|
|
|
}
|
|
|
```
|
|
|
```csharp
|
|
|
//具有 Rectangle 类型属性的匿名对象
|
|
|
new {
|
|
|
Name = "Example",
|
|
|
Rectangle = new Rectangle(){ Width=100, Height=50 }
|
|
|
}
|
|
|
```
|
|
|
### 打开泛型类型
|
|
|
可以通过使用开放泛型类型定义作为键来指定格式化程序。以下代码将为所有类型的 T 的 List<T> 变体注册一个格式化程序,并打印每个元素及其哈希代码。(请注意,必须强制转换对象才能迭代其项。
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.Register
|
|
|
(
|
|
|
type: typeof(List<>),
|
|
|
formatter: (list, writer) =>
|
|
|
{
|
|
|
foreach (var obj in (IEnumerable)list)
|
|
|
{
|
|
|
writer.WriteLine($"{obj} ({obj.GetHashCode()})");
|
|
|
}
|
|
|
}, "text/html"
|
|
|
);
|
|
|
```
|
|
|
```csharp
|
|
|
//运行上述代码后,以下内容将不再仅打印列表中的值 ["one","two","three"]
|
|
|
//变为:one (254814599) two (656421459) three (-1117028319)
|
|
|
var list = new List<string> { "one", "two", "three" };
|
|
|
list
|
|
|
```
|
|
|
### TypeFormatterSource 特性类
|
|
|
Formatter.Register方式外,另一种注册自定义格式化程序的方式是:使用 TypeFormatterSourceAttribute 修饰类型。如果您想直接在笔记本中重新定义格式化程序设置,这不是最方便的方法。但是,如果您正在编写 .NET Interactive 扩展,或者编写包含某些类型的自定义格式的库或应用程序,则建议使用此方法。其中一个原因是基于属性的方法更简单。另一个原因是,调用 Formatter.ResetToDefault() 时不会清除基于属性的格式化程序自定义,而使用 Formatter.Register 配置的格式化程序会被清除。您可以将基于属性的注册方法视为为类型设置默认格式的一种方式。
|
|
|
|
|
|
基于属性的格式化程序注册有两种方法:一种用于项目引用时,另一种用于项目不引用 Microsoft.DotNet.Interactive.Formatting 时
|
|
|
|
|
|
如果您已经引用了 Microsoft.DotNet.Interactive.Formatting ,例如,因为您正在编写 .NET Interactive 扩展,那么您可以使用 中定义的 TypeFormatterSourceAttribute 来修饰需要自定义格式的类型 Microsoft.DotNet.Interactive.Formatting 。下面是一个示例:
|
|
|
```csharp
|
|
|
[Microsoft.DotNet.Interactive.Formatting.TypeFormatterSource(typeof(MyFormatterSource))]
|
|
|
public class MyTypeWithCustomFormatting
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
//带Mime类型
|
|
|
[Microsoft.DotNet.Interactive.Formatting.TypeFormatterSource
|
|
|
(
|
|
|
typeof(MyFormatterSource), PreferredMimeTypes = new[] { "text/html", "application/json" }
|
|
|
)]
|
|
|
public class MyTypeWithCustomFormatting2
|
|
|
{
|
|
|
}
|
|
|
|
|
|
//TypeFormatterSourceAttribute 指定的格式化程序源必须实现 ITypeFormatterSource,并且必须具有空构造函数。它不需要是 public 类型
|
|
|
public class MyFormatterSource : Microsoft.DotNet.Interactive.Formatting.ITypeFormatterSource
|
|
|
{
|
|
|
public IEnumerable<Microsoft.DotNet.Interactive.Formatting.ITypeFormatter> CreateTypeFormatters()
|
|
|
{
|
|
|
return new Microsoft.DotNet.Interactive.Formatting.ITypeFormatter[]
|
|
|
{
|
|
|
new Microsoft.DotNet.Interactive.Formatting.PlainTextFormatter<MyTypeWithCustomFormatting>(context =>
|
|
|
$"Hello from {nameof(MyFormatterSource)} using MIME type text/plain"),
|
|
|
new Microsoft.DotNet.Interactive.Formatting.HtmlFormatter<MyTypeWithCustomFormatting>(context =>
|
|
|
$"Hello from {nameof(MyFormatterSource)} using MIME type text/html")
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
一个完整例子:
|
|
|
```csharp
|
|
|
[AttributeUsage(AttributeTargets.Class)]
|
|
|
internal class TypeFormatterSourceAttribute : Attribute
|
|
|
{
|
|
|
public TypeFormatterSourceAttribute(Type formatterSourceType)
|
|
|
{
|
|
|
FormatterSourceType = formatterSourceType;
|
|
|
}
|
|
|
|
|
|
public Type FormatterSourceType { get; }
|
|
|
|
|
|
public string[] PreferredMimeTypes { get; set; }
|
|
|
}
|
|
|
|
|
|
internal class MyConventionBasedFormatterSource
|
|
|
{
|
|
|
public IEnumerable<object> CreateTypeFormatters()
|
|
|
{
|
|
|
yield return new MyConventionBasedFormatter { MimeType = "text/html" };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
internal class MyConventionBasedFormatter
|
|
|
{
|
|
|
public string MimeType { get; set; }
|
|
|
|
|
|
public bool Format(object instance, System.IO.TextWriter writer)
|
|
|
{
|
|
|
if (instance is MyTypeWithCustomFormatting myObj)
|
|
|
{
|
|
|
writer.Write($"<div>Custom formattering for {myObj}</div>");
|
|
|
return true;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
### 重置格式设置
|
|
|
在尝试不同的格式设置配置时,您可能会发现需要将所有内容重置为首次启动内核时看到的默认值。您可以轻松执行此作:
|
|
|
```csharp
|
|
|
Microsoft.DotNet.Interactive.Formatting.Formatter.ResetToDefault();
|
|
|
```
|
|
|
### 如何选择格式化程序
|
|
|
可以注册多个可能适用于同一类型的格式化程序。例如,可以为 object、IEnumerable<string> 和 IList<string> 注册格式化程序,其中任何一个都可能合理地应用于 List<string> 的实例。由于这些原因,了解如何选择 formatter 可能很有用。
|
|
|
为 A 类型的对象选择适用的格式化程序,如下所示:
|
|
|
+ 选择 MIME 类型:
|
|
|
+ 选择与 A 相关的最具体的用户注册 MIME 类型首选项
|
|
|
+ 如果没有相关的用户注册的 MIME 类型,则使用配置的默认 MIME 类型
|
|
|
+ 选择一个格式化程序:
|
|
|
+ 选择与 A 相关的最具体的用户注册格式化程序
|
|
|
+ 如果没有相关的用户注册格式化程序,则选择默认格式化程序
|
|
|
|
|
|
> 在这里,“最具体”是指类和接口层次结构。如果顺序完全一致或存在其他冲突,则首选较新的注册。当泛型类型的类型定义相同时,泛型类型的类型实例化优先于泛型格式化程序。
|
|
|
> MIME 类型的默认格式化程序集始终包括 object 的格式化程序
|
|
|
```csharp
|
|
|
using System.IO;
|
|
|
using Microsoft.DotNet.Interactive.Formatting;
|
|
|
|
|
|
ITypeFormatter formatter = Formatter.GetPreferredFormatterFor( typeof(Rectangle), Formatter.DefaultMimeType);
|
|
|
|
|
|
var rect = new Rectangle { Width = 100, Height = 50 };
|
|
|
|
|
|
var writer = new StringWriter();
|
|
|
|
|
|
formatter.Format(rect, writer);
|
|
|
|
|
|
Console.WriteLine(writer.ToString());
|
|
|
```
|
|
|
Examples 例子, 用于说明 Formatter 选择的工作原理
|
|
|
|
|
|
+ 如果您为类型 A 注册格式化程序,则该格式化程序将用于类型 A 的所有对象(直到稍后指定类型 A 的替代格式化程序)
|
|
|
+ 如果为 System.Object 注册格式化程序,则它优先于所有其他格式化程序,但其他更具体的用户定义的格式化程序除外
|
|
|
+ 如果为任何 sealed 类型注册格式化程序,则它优先于所有其他格式化程序(除非为该类型指定了更多格式化程序)
|
|
|
+ 如果注册 List<> 和 List<int> 格式化程序,则 List<int> 格式化程序优先用于 List<int> 类型的对象,而 List<> 格式化程序优先用于其他泛型实例化,例如 List<string>
|