自定义文件存储和 XML 序列化

当用户在 Visual Studio 中保存特定于域的语言(DSL)的实例或 模型时,将创建或更新 XML 文件。 可以重新加载该文件,以在应用商店中重新创建模型。

可以通过在 DSL 资源管理器中调整“Xml 序列化行为”下的设置来自定义序列化方案。 对于每个域类、属性和关系,“Xml 序列化行为”下都有对应节点。 关系位于其源类下。 还有对应于形状、连接线和关系图类的节点。

还可以编写程序代码,以便进行更高级的自定义。

说明

如果要以特定格式保存模型,但不需要从该窗体重新加载模型,请考虑使用文本模板从模型生成输出,而不是自定义序列化方案。 有关详细信息,请参阅从域特定语言生成代码

模型和关系图文件

每个模型保存在两个文件中:

  • 模型文件具有一个名称,如 Model1.mydsl。 它存储模型元素和关系及其属性。 文件扩展名(如 .mydsl)由 DSL 定义中 编辑器 节点的 FileExtension 属性确定。

  • 关系图文件有一个名称,如 Model1.mydsl.diagram。 它存储形状、连接线及其位置、颜色、线条粗细和其他图表外观的详细信息。 如果用户删除 .diagram 文件,模型中的基本信息不会丢失。 只是关系图的布局丢失了。 打开模型文件时,将创建一组默认的形状和连接线。

更改 DSL 的文件扩展名

  1. 打开 DSL 定义。 在 DSL 资源管理器中,单击“编辑器”节点。

  2. 在“属性”窗口中,编辑 FileExtension 属性。 不要包含文件扩展名的初始 .

  3. 在解决方案资源管理器中,更改 DslPackage\ProjectItemTemplates中的两个项模板文件的名称。 这些文件具有以下格式的名称:

    myDsl.diagram

    myDsl.myDsl

默认序列化方案

若要为本主题创建示例,请使用以下 DSL 定义。

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 节点的名称与域名相同,但首字母为小写。 例如,familyTreeModelperson

  • 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 序列化行为”中该属性的“Is Moniker Key”选项设置为 true,则使用限定键名字对象。 在此示例中,为域类“Album”和“Song”中名为“Title”的域属性设置此选项。

与 ID 标识符相比,限定的键标识符更易于理解。 如果您希望模型文件的 XML 易于人类阅读,请考虑使用限定的关键标识符。 但是,用户可以将多个元素设置为具有相同的标识符键。 重复键可能会导致文件无法正确重新加载。 因此,如果定义使用限定键名字对象引用的域类,应考虑阻止用户保存具有重复名字对象的文件的方法。

设置要由 ID 名字对象引用的域类

  1. 请确保类及其基类中每个域属性的“Is Moniker Key”设置为 false

    1. 在 DSL 资源管理器中,展开“Xml 序列化行为\类数据\<域类>\元素数据”

    2. 验证每个域属性的“Is Moniker Key”是否设置为 false

    3. 如果域类具有基类,请重复该类中的过程。

  2. 为域类设置 序列化 ID = true

    可以在 Xml 序列化行为下找到此属性。

设置要由限定键名字对象引用的域类

  • 为现有域类的域属性设置“Is Moniker Key”。 属性的类型必须是 string

    1. 在 DSL 资源管理器中,展开“Xml 序列化行为\类数据\<域类>\元素数据”,然后选择域属性。

    2. 在“属性”窗口中,将“Is Moniker Key”设置为 true

  • - 或 -

    使用 命名域类 工具创建新的域类。

    此工具创建一个名为 Name 的域属性的新类。 此域属性的“Is Element Name”和“Is Moniker Key”属性将初始化为 true

  • - 或 -

    创建从域类到具有别名键属性的另一个类的继承关系。

避免重复的名字对象

如果使用限定的键名字对象,则用户模型中的两个元素可能在键属性中具有相同的值。 例如,如果 DSL 具有具有属性名称的类 Person,则用户可以将两个元素的名称设置为相同。 尽管模型可以保存到文件中,但它无法正确重新加载。

有多种方法有助于避免这种情况:

  • 为关键域属性将 设置为元素名称 = 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 Qualifier”和“Is Moniker Key”,则不会在 Album 及其嵌入子级的名字对象中使用该模型的名称或标识符:

<albumMoniker name="Jazz after Teatime" />
<songMoniker title="/Jazz after Teatime/Hot tea" />

自定义 XML 的结构

若要进行以下自定义,请在 DSL 资源管理器中展开“Xml 序列化行为”节点。 在域类中,展开“元素数据”节点以查看该类的属性和关系列表。 选择关系并在“属性”窗口中调整其选项。

  • 省略元素 设置为 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.csSerializationHelper.cs中研究代码。

自定义特定类的序列化

  1. 在“Xml 序列化行为”下该类的节点中设置“Is Custom”

  2. 转换所有模板,生成解决方案,并调查生成的编译错误。 每个错误附近的注释说明了必须提供的代码。

为整个模型提供你自己的序列化

  1. 替代 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 序列化行为\类数据下。

财产 描述
具有自定义元素架构 如果为 True,则指示域类具有自定义元素架构
是自定义的 如果要为此域类编写自己的序列化和反序列化代码,请将该值设置为 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。 如果没有标记为“Is Moniker Key”的属性,并且 DSL 定义对此类的引用关系,则该值必须设置为 True。
类型名称 由指定域类在 xsd 中生成的 XML 类型的名称。
笔记 与此元素关联的非正式笔记

Xml 属性数据

Xml 属性节点位于类节点下。

财产 描述
Domain Property XML 序列化配置数据应用到的属性。 只读。
Is Moniker Key 如果该值设置为 True,则属性将用作创建引用此域类实例的名字对象的关键。
Is Moniker Qualifier 如果该值设置为 True,则该属性用于在名称标识符中创建限定符。 如果为 false,并且此域类的 SerializeId 不为 true,则名字对象由嵌入树中父元素的名字对象限定。
表示形式 如果该值设置为 Attribute,则该属性将序列化为 xml 属性;如果该值设置为 元素,则将其序列化为元素;如果值设置为 忽略,则不会序列化该值。
Xml Name 用于表示属性的 xml 属性或元素的名称。 默认情况下,该值是域名的小写版本。
笔记 与此元素关联的非正式笔记

Xml Role data

角色数据节点位于源类节点下。

财产 描述
Has Custom Moniker 如果要提供自己的代码来生成和解析穿越此关系的标识符,请将此项设为 true。

对于详细的操作说明,请先构建解决方案,然后双击错误消息。
域关系 指定这些选项适用的关系。 只读。
省略元素 如果为 true,则从架构中省略与源角色对应的 XML 节点。

如果源类和目标类之间存在多个关系,则此角色节点区分属于这两个关系的链接。 因此,我们建议在这种情况下不要设置此选项。
角色元素名称 指定从源角色派生的 XML 元素的名称。 默认值为角色属性名称。
使用完整表单 如果为 true,则每个目标元素或名字对象都包含在表示关系的 XML 节点中。 如果关系具有自己的域属性,则应将其设置为 true。