自定义文件存储和 XML 序列化
当用户在 Visual Studio 中保存域特定语言 (DSL) 的实例或模型时,将创建一个 XML 文件或对其进行更新。 可以重新加载文件以在 Store 中重新创建模型。
可以通过在 DSL 资源管理器中调整“Xml 序列化行为”下的设置来自定义序列化方案。 每个域类、属性和关系都有 Xml 序列化行为下的节点。 关系位于其源类下。 还有对应于形状、连接器和关系图类的节点。
还可以编写程序代码以实现更高级的自定义。
备注
如果要以特定格式保存模型,但不需要从该窗体重新加载它,请考虑使用文本模板从模型生成输出,而不是使用自定义序列化方案。 有关详细信息,请参阅从域特定语言生成代码。
模型和关系图文件
每个模型保存在两个文件中:
模型文件的名称如下
Model1.mydsl
。 它存储模型元素和关系及其属性。 文件扩展名(例如.mydsl
,由 DSL 定义中编辑器节点的 FileExtension 属性确定)。关系图文件的名称如下
Model1.mydsl.diagram
。 它存储形状、连接器及其位置、颜色、线条粗细以及关系图外观的其他详细信息。 如果用户删除文件.diagram
,模型中的基本信息不会丢失。 只有关系图的布局会丢失。 打开模型文件时,将创建一组默认的形状和连接线。
更改 DSL 的文件扩展名
打开 DSL 定义。 在 DSL 资源管理器中,单击“编辑器”节点。
在“属性”窗口中,编辑“FileExtension”属性。 不要包含
.
文件扩展名的初始名称。在“解决方案资源管理器”中,更改 DslPackage\ProjectItemTemplates 中两个项模板文件的名称。 这些文件的名称遵循以下格式:
myDsl.diagram
myDsl.myDsl
默认序列化方案
为了创建本主题示例,使用了以下 DSL 定义。
此 DSL 用于创建在屏幕上具有以下外观的模型。
此模型已保存,然后在 XML 文本编辑器中重新打开:
<?xml version="1.0" encoding="utf-8"?>
<familyTreeModel xmlns:dm0="http://schemas.microsoft.com/VisualStudio/2008/DslTools/Core" dslVersion="1.0.0.0" Id="f817b728-e920-458e-bb99-98edc469d78f" xmlns="http://schemas.microsoft.com/dsltools/FamilyTree">
<people>
<person name="Henry VIII" birthYear="1491" deathYear="1547" age="519">
<children>
<personMoniker name="/f817b728-e920-458e-bb99-98edc469d78f/Elizabeth I" />
<personMoniker name="/f817b728-e920-458e-bb99-98edc469d78f/Mary" />
</children>
</person>
<person name="Elizabeth I" birthYear="1533" deathYear="1603" age="477" />
<person name="Mary" birthYear="1515" deathYear="1558" age="495" />
</people>
</familyTreeModel>
注意关于序列化模型的以下几点:
每个 XML 节点的名称都与域类名相同,但初始字母为小写。 例如,
familyTreeModel
和person
。Name 和 BirthYear 等域属性在 XML 节点中序列化为特性。 同样,属性名称的初始字符将转换为小写。
每个关系都序列化为嵌套在关系的源端内的 XML 节点。 节点与源角色属性同名,但初始字符为小写。
例如,在 DSL 定义中,名为“People”的角色来源于 FamilyTree 类。 在 XML 中 ,People 角色用一个名为
people
嵌套在节点内的familyTreeModel
节点表示。每个嵌入关系的目标端序列化为嵌套在关系下的节点。 例如,
people
节点包含多个person
节点。每个引用关系的目标端序列化为名字对象,用于编码对目标元素的引用。
例如,在
person
节点下,可以有children
关系。 此节点包含名字对象,例如:<personMoniker name="/f817b728-e920-458e-bb99-98edc469d78f/Elizabeth I" />
了解名字对象
名字对象用于表示模型和关系图文件的不同部分之间的交叉引用。 它们在文件中也用于 .diagram
引用模型文件中的节点。 有两种形式的名字对象:
Id 名字对象引用目标元素的 GUID。 例如:
<personShapeMoniker Id="f79734c0-3da1-4d72-9514-848fa9e75157" />
限定键名字对象通过名为“名字对象键”的指定域属性的值来标识目标元素。 目标元素的名字对象以嵌入关系树中其父元素的名字对象为前缀。
以下示例取自 DSL,其中有一个名为 Album 的域类,该类与名为 Song 的域类具有嵌入关系:
<albumMoniker title="/My Favorites/Jazz after Teatime" /> <songMoniker title="/My Favorites/Jazz after Teatime/Hot tea" />
如果目标类具有一个域属性,则使用限定的键名字对象,该属性在 Xml 序列化行为中将选项设置为“名字对象密钥
true
”。 在示例中,这个选项是为域类“Album”和“Song”中名为“Title”的域属性设置的。
限定键名字对象比 ID 名字对象更易于读取。 如果希望模型文件的 XML 可读,请考虑使用合格的密钥名字对象。 但是,用户可以将多个元素设置为具有相同的名字对象键。 重复的键可能会导致文件无法正确地重新加载。 因此,如果定义使用限定键名字对象引用的域类,应考虑阻止用户保存具有重复名字对象的文件的方法。
设置要由 ID 名字对象引用的域类
请确保类及其基类中每个域属性的“Is Moniker Key”设置为
false
。在 DSL 资源管理器中,展开“Xml 序列化行为\类数据\<域类>\元素数据”。
验证每个域属性的“Is Moniker Key”是否设置为
false
。如果域类具有基类,则在该类中重复这一过程。
设置域类的序列化 Id =
true
。可以在“Xml 序列化行为”下找到此属性。
设置要由限定键名字对象引用的域类
为现有域类的域属性设置“Is Moniker Key”。 属性的类型必须为
string
。在 DSL 资源管理器中,展开“Xml 序列化行为\类数据\<域类>\元素数据”,然后选择域属性。
在“属性”窗口中,将“Is Moniker Key”设置为
true
。
- 或 -
使用“命名域类”工具创建新域类。
此工具创建一个新类,该类具有名为“Name”的域属性。 此域属性的“Is Element Name”和“Is Moniker Key”属性将初始化为
true
。- 或 -
创建从域类到具有名字对象键属性的另一个类的继承关系。
避免重复的名字对象
如果使用限定的键名字对象,则用户模型中的两个元素可能在键属性中具有相同的值。 例如,如果 DSL 具有包含属性 Name 的 Person 类,则用户可以将两个元素的 Name 设置为相同。 尽管模型可以保存到文件中,但它不会正确重新加载。
有几种方法可帮助避免这种情况:
为键域属性设置 Is Element Name =
true
。 在 DSL 定义关系图中选择域属性,然后在“属性”窗口中设置值。当用户创建类的新实例时,此值会导致自动为域属性分配其他值。 默认行为是在类名末尾添加一个数字。 这不会阻止用户将名称更改为重复名称,但在保存模型之前用户未设置值的情况会有所帮助。
为 DSL 启用验证。 在 DSL 资源管理器中,选择“编辑器\验证”,并将“Uses...”属性设置为
true
。有一种自动生成的验证方法,用于检查歧义。 该方法位于
Load
验证类别中。 这可确保警告用户可能无法重新打开文件。有关详细信息,请参阅域特定语言中的验证。
名字对象路径和限定符
限定键名字对象以名字对象键结尾,在嵌入树中以其父级的名字对象作为前缀。 例如,如果一张专辑的名字对象为:
<albumMoniker title="/My Favorites/Jazz after Teatime" />
那么,该专辑中的一首歌曲可以是:
<songMoniker title="/My Favorites/Jazz after Teatime/Hot tea" />
但是,如果按 ID 引用专辑,则名字对象将如下所示:
<albumMoniker Id="77472c3a-9bf9-4085-976a-d97a4745237c" />
<songMoniker title="/77472c3a-9bf9-4085-976a-d97a4745237c/Hot tea" />
请注意,由于 GUID 是唯一的,因此它永远不会以其父对象的名字对象为前缀。
如果你知道某个特定域属性将始终在模型内具有唯一值,则可以将该属性的“Is Moniker Qualifier”设置为 true
。 这会导致它用作限定符,而不使用父级的名字对象。 例如,如果同时 为 Album 类的 Title 域属性设置了 Is Moniker 限定符 和 Is Moniker Key ,则对于 Album 及其嵌入子项,模型的名称或标识符不会在名字对象中使用:
<albumMoniker name="Jazz after Teatime" />
<songMoniker title="/Jazz after Teatime/Hot tea" />
自定义 XML 的结构
若要进行以下自定义,请在 DSL 资源管理器中展开“Xml 序列化行为”节点。 在域类下,展开“元素数据”节点以查看源自此类的属性和关系的列表。 选择关系,并在“属性”窗口中调整其选项。
将“Omit Element”设置为 true 可省略源角色节点,只留下目标元素列表。 如果源类和目标类之间存在多个关系,则不应设置此选项。
<familyTreeModel ...> <!-- The following node is omitted by using Omit Element: --> <!-- <people> --> <person name="Henry VIII" .../> <person name="Elizabeth I" .../> <!-- </people> --> </familyTreeModel>
设置“Use Full Form”,将目标节点嵌入表示关系实例的节点中。 将域属性添加到域关系时,会自动设置此选项。
<familyTreeModel ...> <people> <!-- The following node is inserted by using Use Full Form: --> <familyTreeModelHasPeople myRelationshipProperty="x1"> <person name="Henry VIII" .../> </familyTreeModelHasPeople> <familyTreeModelHasPeople myRelationshipProperty="x2"> <person name="Elizabeth I" .../> </familyTreeModelHasPeople> </people> </familyTreeModel>
设置 Representation = Element,以将域属性另存为元素而不是特性值。
<person name="Elizabeth I" birthYear="1533"> <deathYear>1603</deathYear> </person>
若要更改特性和关系的序列化顺序,请右键单击“元素数据”下的某个项,然后使用“上移”或“下移”菜单命令。
使用程序代码的主要自定义项
可以替换部分或所有序列化算法。
建议你研究一下 Dsl\Generated Code\Serializer.cs 和 SerializationHelper.cs 中的代码。
自定义特定类的序列化
在“Xml 序列化行为”下该类的节点中设置“Is Custom”。
转换所有模板,生成解决方案,并调查生成的编译错误。 每个错误附近的注释说明了必须提供的代码。
为整个模型提供你自己的序列化
- 替代 Dsl\GeneratedCode\SerializationHelper.cs 中的方法
注意
从 Visual Studio 2022 17.13 开始,由于 BinaryFormatter 的安全风险,默认序列化实现不再支持使用 BinaryFormatter 序列化或反序列化自定义数据类型。
如果将自定义数据类型用于任何域属性,则需要重写类中的 SerializationHelper
序列化方法,或实现 TypeConverter
能够将每个自定义数据类型转换为字符串和从字符串转换的方法。
虽然不建议出于安全原因使用 BinaryFormatter
,但如果必须保持与使用 BinaryFormatter
序列化的较旧模型的向后兼容性,则可以实现 TypeConverter
反序列化二进制数据。 以下代码片段用作实现此兼容性的模板:
class MyCustomDataTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string text)
{
// First, try to parse the string as if it were returned by MyCustomDataType.ToString().
if (MyCustomDataType.TryParse(text, out var custom))
return custom;
// Fall back to trying to deserialize the old BinaryFormatter serialization format.
var decoded = Convert.FromBase64String(text);
using (var memory = new MemoryStream(decoded, false))
{
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(memory) as MyCustomDataType;
}
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string) && value is MyCustomDataType custom)
return custom.ToString();
return base.ConvertTo(context, culture, value, destinationType);
}
}
// ...
[TypeConverter(MyCustomDataTypeConverter)]
class MyCustomDataType
{
// ...
}
Xml 序列化行为中的选项
在 DSL 资源管理器中,Xml 序列化行为节点包含每个域类、关系、形状、连接线和关系图类的子节点。 每个节点下是源自该元素的属性和关系的列表。 关系在其自己的右侧和源类下显示。
下表总结了可以在 DSL 定义的这一部分中设置的选项。 在每种情况下,在 DSL 资源管理器中选择一个元素,并在“属性”窗口中设置选项。
Xml 类数据
这些元素位于 DSL 资源管理器中的“Xml 序列化行为\类数据”下。
属性 | 说明 |
---|---|
Has Custom Element Schema | 如果为 True,则指示域类具有自定义元素架构 |
Is Custom | 如果要为此域类编写自己的序列化和反序列化代码,请将该值设置为 True 。 生成解决方案并调查错误以发现详细说明。 |
域类 | 此类数据节点应用到的域类。 只读。 |
元素名称 | 此类的元素的 Xml 节点名称。 默认值是域类名称的小写版本。 |
Moniker Attribute Name | 名字对象元素中用于包含引用的特性的名称。 如果为空,则使用键属性的名称或 ID。 在此示例中,它是“name”: <personMoniker name="/Mike Nash"/> |
Moniker Element Name | XML 元素的名称,用于引用该类的元素的名字对象。 默认值是带有“Moniker”后缀的类名的小写版本。 例如, personMoniker 。 |
Moniker Type Name | 为此类的元素的名字对象所生成 xsd 类型的名称。 XSD 位于 Dsl\Generated Code\*Schema.xsd 中 |
Serialize Id | 如果为 True,则元素 GUID 包含在文件中。 如果没有标记为“名字对象键”的属性,并且 DSL 定义对此类的引用关系,则必须将该值设置为 True。 |
类型名称 | 由指定域类在 xsd 中生成的 XML 类型的名称。 |
备注 | 与此元素相关联的非正式注释 |
Xml Property Data
Xml 属性节点位于类节点下。
属性 | 说明 |
---|---|
Domain Property | XML 序列化配置数据应用到的属性。 只读。 |
Is Moniker Key | 如果该值设置为 True,则属性将用作创建引用此域类实例的名字对象的键。 |
Is Moniker Qualifier | 如果该值设置为 True,则属性用于在名字对象中创建限定符。 如果为 false,并且此域类的 SerializeId 不为 true,则名字对象由嵌入树中父元素的名字对象限定。 |
表示形式 | 如果该值设置为 Attribute,则该属性将序列化为 xml 属性;如果该值设置为 Element,则将其序列化为元素;如果该值设置为 Ignore,则不会序列化该值。 |
Xml Name | 用于代表属性的 XML 特性或元素的名称。 默认情况下,该值是域名的小写版本。 |
备注 | 与此元素相关联的非正式注释 |
Xml Role data
角色数据节点位于源类节点下。
属性 | 说明 |
---|---|
Has Custom Moniker | 如果要提供自己的代码来生成和解析遍历此关系的名字对象,请设置为 true。 有关详细说明,请生成解决方案,然后双击错误消息。 |
域关系 | 指定这些选项应用于的关系。 只读。 |
Omit Element | 如果为 True,则从架构中省略对应于源角色的 XML 节点。 如果源类和目标类之间存在多个关系,则此角色节点区分属于这两个关系的链接。 因此,我们建议在这种情况下不要设置此选项。 |
Role Element Name | 指定从源角色派生的 XML 元素的名称。 默认值为角色属性名称。 |
Use Full Form | 如果为 true,则每个目标元素或名字对象都包含在表示关系的 XML 节点中。 如果关系具有其自己的域属性,则应将此属性设置为 true。 |