实现持久化

来源 目录

实现持久化

概述

本主题回顾了使用 .NET 实现的持久化技术。

译注:

本主题主要讲述了 .NET 环境下 ArcGIS 实现持久化的技术。

持久化是一个通用术语,用于表述一个过程:将“在内存中运行的对象(实例或数据)”写入“持久性存储介质”中,并在之后,能够将在“持久性存储介质”中的数据重建回对象(实例或数据)。例如:打开mxd文件能够看到关闭前的地图显示范围。

与序列化相比,持久化更注重数据在存储介质之间来回变换。

序列化,描述一个对象(实例或数据)变为“流”的过程。之后,若将“流”存储至磁盘、数据库等,则称为之“持久化”;若将“流”用于传输,则与“持久化”无关。读取文件(得到文件流 FileStream)或直接接收到流后,将流转化为对象(实例或数据),这个过程称之为“反序列化”。

序列化产生的流,既可用于数据的存储,也可用于数据传输。

二者都可以实现重建对象的功能。

关于持久化

持久化是一个通用术语,是指“将【对象当前状态的信息】写入持久性存储介质”的过程。(例如磁盘上的文件)。

ArcGIS 中使用持久化,来保存文档和模板的当前状态。

通过与 ArcGIS 用户界面 (UI) 交互,您可以更改属于地图文档的许多对象的属性,例如渲染器(renderer)。当地图文档被保存并关闭时,渲染器类的实例被终止。当文档重新打开时,您可以看到渲染器对象的状态已被保留(恢复到关闭之前的状态)。

结构化存储、复合文件、文档和流

地图文档及其内容使用一种称为结构化存储的技术进行保存。结构化存储是持久化的一种实现方式,由许多标准 COM 接口来定义。在结构化存储技术出现之前,人们通过单个文件指针来访问文件。而在结构化存储中,使用了复合文件模型(每个文件都包含【存储对象】和【流对象】)。【存储对象】提供结构(例如,操作系统上的文件夹)并且可以包含其他存储对象和流对象。【流对象】提供存储(例如,传统文件)并且可以包含任何类型的数据(使用任何内部结构)。当流被重新打开时,可以初始化一个新对象,并根据流中的信息设置对象的状态,恢复到前一个对象的状态。

这样,单个复合文件就可以充当迷你文件系统;它可以被许多文件指针访问。结构化存储的好处:增量文件读/写、文件结构标准化,尽管也导致产生更大的文件大小。

ArcGIS 使用结构化存储,来持久化应用程序使用的所有对象的当前状态,但也使用其他持久化技术。

结构化存储仅用于非 GIS 数据

ArcGIS 中的持久化

在整个 ArcGIS 框架中,“结构化存储接口”被广泛实现。

在 ArcGIS 框架中,何时使用持久化,了解了之后,可以帮助您“在所创建的类中”实现正确的持久化。以下章节,解释了何时实现持久化,以及实现哪些接口,并回顾了在持久化对象时,可能遇到的哪些问题。

在整个 ArcGIS 框架中,尽管都使用了持久化,但它并不是无处不在。并非每个对象都有机会持久化自己。

复合文档结构

ArcGIS 应用程序使用复合文档结构来存储文档,例如地图文档、globe 文档、地图和globe 模板、普通模板等。保存文档时,当前在文档或模板中运行的所有对象都将被持久化保存到复合文件中。

例如,在地图文档中,当用户在 ArcMap 中单击保存时,MxApplication 会根据需要创建流,将它们(即创建的流)与现有的 .mxd 文件相关联(如果该文档之前已保存),然后请求【文档将自身持久化到这些流】中去。如果对普通模板或地图模板进行了更改,则会对相应的 .mxt 文件重复此过程。当重新打开文件时,就可以重新创建文档的当前状态。

例如,ArcMap 会持久化许多项目。

  • 地图集合 - 每张地图都保留其图层、符号系统、图形、当前范围、空间参考等。这可以包括自定义图层、渲染器、符号、元素或其他地图项。
  • 页面布局、地图框、地图周边、项目布局等 - 这可以包括自定义地图周边或框架。
  • 可见目录 (TOC) 视图及其状态 - 这可以包括自定义 TOC 视图。
  • 当前可见的工具栏、其成员和位置(如果浮动),包括标准和自定义工具栏和命令,以及 UIControls。
  • 已注册的扩展及其状态 - 这可以包括自定义扩展。
  • 当前 DataWindow、它们的类型、位置和内容 - 这可以包括自定义 DataWindow。
  • 通过 StyleGallery 引用的当前样式的列表;列表中项目(items)通过使用持久化来存储样式 - 这可以包括自定义 StyleGalleryItem 或 StyleGalleryClass。

从 ArcGIS 9.1 开始,您可以保存地图文档,以便在以前版本的 ArcGIS 中打开和使用它们。有关在自定义组件中处理这种持久性的更多信息,请参阅本主题中的 版本兼容性版本兼容性一致性部分

如果地图文档引用的某一对象不支持持久化,则可能会向用户抛出错误,并且可能会阻止保存的完成,从而导致文档不可用;因此,您应该始终清楚您的类是否需要实现持久化,并在需要时实现正确的持久化行为。

实现持久化的类

当一个对象被请求持久化时,它会将其成员变量的当前值写入“流”中。如果其中一个成员引用了另一个对象并且该对象也是可持久化的,则最有可能通过请求成员对象持久化自身来委派持久化工作。这种级联效应,确保了所有被引用的对象都有机会持久化。这可以包括您的自定义对象,如果它们被可持久化的对象引用的话。

当每个对象请求其成员依次持久化自己时,持久化事件会在文档中级联。

请参见下图:

持续事件的插图。

正如前面看到的那样,每个类决定 哪些成员定义了它自己的状态,并且只持久化这些“状态数据”(在大多数情况下,是它的私有成员变量的值)。

如果由于某种原因,您决定您的自定义类不需要将有关其状态的任何信息保存到流中,但希望支持持久化,那么您仍然必须实现持久化,尽管您不一定需要将任何数据写入流中。

对于您创建的大多数自定义类,对象会被持久化到框架创建的流(之一)中,您不太可能需要创建一个新的存储对象或流对象。

例如, ArcGIS Desktop 的 ArcMap 、 ArcGIS Engine 中的 MapDocument 类【译注:这二者都是框架创建的,您创建的类的对象,如果进行持久化,通常持久化到它们那里】。

加载的扩展

在保存过程中,应用程序检查所有当前加载的扩展,以验证它们是否实现了持久化。如果是这样,则要求每个扩展都持久化自身。因此,扩展不一定必须支持持久化。如果不支持,也不会引发错误。这取决于扩展是否需要在文档关闭时保持状态。扩展按照它们被引用的顺序进行持久化,即它们的类标识符 (CLSID) 的顺序。

应用程序实例(application object)为每个扩展创建一个单独的流,并且新的流与其他的流存储在相同的复合文件中。

应用程序实例还为扩展创建了一个单独的 ObjectStream。有关 对象流 的更多信息,请参阅以下 ObjectStream 部分。

对象流(ObjectStream)

对象的状态并不总是由值类型定义。

您已经了解了地图文档如何通过调用其他对象的持久化,来持久化自身。

通常,对同一个实例有多个引用,例如,地图中的同一图层可能由 IMap.LayerILegendItem.Layer 引用。如果每一个属性都被要求持久化自身,则会将 Layer 持久化为两个单独的副本。这不仅会增大文件大小,还会破坏对象引用的正确性。为了避免这个问题,ArcObjects 使用【对象流(ObjectStreams)】来持久化对象,保证在持久化时正确维护对象引用。

当 ArcObjects 对象启动持久化自身时,会为持久化创建一个流,同时创建一个 ObjectStream 并将其与流相关联;一个 ObjectStream 可以与一个或多个流相关联。ObjectStream 维护已持久化到该流的【对象列表】。

第一次遇到某个对象时,它会以通常的方式持久化这个对象。如果再次遇到,ObjectStream 会确保不会再次持久化这个对象,而是存储引用【对“现有已保存对象”的引用】。请参见下图:

显示 ObjectStream 的插图。

除了确保对象引用的完整性之外,这还有助于让文件保持尽可能的小。

只有支持 IUnknown 和 IPersist 的 COM 对象才能以这种方式存储。

public interface IPersist
{
    void GetClassID(out Guid pClassID);
}

实现持久化

要创建持久类,请实现 IPersist 和 IPersistStreamIPersistVariant。IPersistStream 和 IPersistVariant 指定了以下三个基本功能:

在 .NET 中实现 IPersistStream 是一种高级技术,本主题不讨论。相反,本主题讨论 IPersistVariant 的实现。在任何情况下,您都不需要同时实现这两个接口。

public interface IPersistVariant
{
    UID ID
    {
        get;
    }

     void Load(IVariantStream Stream);

     void Save(IVariantStream Stream);
}

public interface IPersistStream : IPersist
{
    new void GetClassID(out Guid pClassID);

    void IsDirty();

    void Load(IStream pstm);

    void Save(IStream pstm, int fClearDirty);

    void GetSizeMax(out _ULARGE_INTEGER pcbSize);
}

当一个文档被持久化时,客户端将类的标识写入流(使用 ClsID)。然后它调用 Save 方法将实际的类数据写入流中。加载文档时,首先读取类的标识,从而创建正确类的实例。然后将剩余的持久化数据加载到类的新实例中。

如果要实现特定于版本的持久性代码,请参阅版本兼容性了解更多信息。

本主题中的所有代码部分均来自 示例:三角形图形元素

您需要保存的内容

当你实现一个持久化类时,由你来决定持久化类的哪些状态。选择将哪些数据写入流中,取决于您。

存储有关的公共属性和内部成员的数据,确保您的代码可以重新创建一个状态正确的实例。

您还可以决定不保留哪些状态项。例如,地图不会保留 IMap.SelectedLayer 属性。打开地图文档时,SelectedLayer 属性为空。

实现 IPersistVariant

IPersistVariant 接口供非 C++/VC++ 程序员使用。请参见以下代码示例:

[C#]

public sealed class TriangleElementClass: IPersistVariant

[VB.NET]

Public NotInheritable Class TriangleElementClass
Implements ..., IPersistVariant...

在 ID 属性中,创建 UID 并将对象设置为类的完全限定类名,或者使用(CLASSID/全局唯一标识符 [GUID])。请参见以下代码示例:

[C#]

public UID ID
{
    get
    {
        UID uid=new UIDClass();
        uid.Value="{" + TriangleElementClass.CLASSGUID + "}";
        return uid;
    }
}

[VB.NET]

Public ReadOnly Property ID() As UID Implements IPersistVariant.ID
Get
Dim uid As UID=New UIDClass()
uid.Value="{" & TriangleElementClass.CLASSGUID & "}"
Return uid
End Get
End Property

保存和加载的基本实现,如以下代码示例所示:

在示例中,持久化了一个双精度数、一个字符串和一个点(m_size、m_elementType、m_pointGeometry)。

[C#]

public void Save(IVariantStream Stream)
{
    Stream.Write(m_size);
    Stream.Write(m_elementType);
    Stream.Write(m_pointGeometry);
    ...
}

public void Load(IVariantStream Stream)
{
    m_size=(double)Stream.Read();

    m_elementType=(string)Stream.Read();
    m_pointGeometry=Stream.Read() as IPoint;
    ...
}

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(m_size)
    Stream.Write(m_elementType)
    Stream.Write(m_pointGeometry)
    ...
End Sub

Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    m_size=CDbl(Stream.Read())
    m_elementType=CStr(Stream.Read())
    m_pointGeometry=TryCast(Stream.Read(), IPoint)
    ...
End Sub

流是顺序的。Load 方法必须以与在 Save 方法中将数据写入流的顺序相同的顺序从流中读取数据。

确保您的数据以正确的顺序保存和加载,以便将正确的数据写到正确的成员上。如果您有一个大型复杂类,则对 Save 和 Load 方法进行编码可能要复杂得多。

传递给 IPersistVariant 接口的流是实现IVariantStream的专业流类。使用此接口,可以将任何值类型或 COM 对象写入流。此流类在 ArcObjects 内部。

IVariantStream 接口允许您使用相同的语义将 COM 对象和值数据类型写入流。

识别文档版本

如果希望您的对象可以保存到以前版本,则需要调整您的 IPersistVariant 实现,以识别文档版本。为不同版本,设置不同的持久化代码。

为确定文档的版本,在对 Save 和 Load 方法的调用中,您可以通过将“流对象”转换为 IDocumentVersion 接口,通过 IDocumentVersion 来确定文档的版本。

public interface IDocumentVersion
{
    esriArcGISVersion DocumentVersion
    {
        get;
        set;
    }
}

[C#]

public void Save(IVariantStream Stream)
{
    if (Stream is IDocumentVersion)
    {
        IDocumentVersion docVersion=(IDocumentVersion)Stream;
        if (docVersion.DocumentVersion == esriArcGISVersion.esriArcGISVersion83)
        {
            //Save object as 8.3 version of itself.
        }
        else
        {
            //Save object.
        }
    }
}

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    If TypeOf Stream Is IDocumentVersion Then
        Dim docVersion As IDocumentVersion
        docVersion=CType(Stream, IDocumentVersion)
        If docVersion.DocumentVersion=esriArcGISVersion.esriArcGISVersion83 Then
            'Load object as 8.3 version of itself.
        Else
            'Load object.
        End If
    End If

如果您的代码安装在安装了 9.1 版本之前的 ArcGIS 的计算机上,则无法保证传递的流支持 IDocumentVersion。如前所示,如果找不到此接口,请始终尝试为该接口进行强制转换并采取适当的措施。您可能希望提供您的函数来发现已安装的 ArcGIS 版本,或者您可能希望依赖于您的内部持久性版本号。有关更多信息,请参阅 在持久性中编码向后兼容性

持久化不同数据的技术

以下部分为 IPersistVariant 的实现者提供了有关将某些类型的数据持久保存到流中的建议。

持久化对象

如果您使用 IPersistVariant,则对“对象的持久化”进行编码,在语法上,与对“值类型的持久化”进行编码相同。当您像传递值类型一样传递对象引用时,流使用与流内部关联的 ObjectStream 来持久化对象。请参见以下代码示例:

[C#]

Stream.Write(m_pointGeometry);

[VB.NET]

Stream.Write(m_pointGeometry)

对象以类似的方式重新加载。请参见以下代码示例:

[C#]

m_pointGeometry=Stream.Read() as IPoint;

[VB.NET]

m_pointGeometry=TryCast(Stream.Read(), IPoint)

持久化数组

通常,类成员可能是具有可变成员数量的动态数组。在这种情况下,将成员的值直接写入整个流,因为它不是 COM 对象。

只要包含有关数组大小的额外信息,就可以将每个数组成员依次写入流,因为 Load 方法需要调整数组大小,并从流中读取正确数量的成员以分配给数组。

以下代码示例显示了如何使用此技术,其中 m_array 是该类的成员:

[C#]

int count=m_array.Count;
Stream.Write(count);
for (int i=0; i < count; i++)
{
    Stream.Write(m_array[i]);
}

[VB.NET]

Dim Count As Integer=m_array.Count
Stream.Write(Count)
Dim i As Integer
For i=0 To Count - 1
    Stream.Write(m_array(i))
Next

现在可以在 Load 方法中正确初始化数组。请参见以下代码示例:

[C#]

int count=(int)Stream.Read();
for (int i=0; i < count; i++)
{
    m_array.Add(Stream.Read());
}

[VB.NET]

Dim Count As Integer=CInt(Stream.Read())
Dim i As Integer
For i=0 To Count - 1
    m_array.Add(Stream.Read())
Next

您可以不使用标准动态数组,而是将对象引用存储在 Esri Array 类中,并以相同的方式持久化每个引用(Array 类不可持久化)。

持久化一个 PropertySet

您可以使用 PropertySet 类来持久化类的成员数据,因为此类是可持久化的。如果您已经使用 PropertySet 在内部存储类数据,则在保存期间可以获得最大效率。

文档版本和 ObjectStream

版本兼容性编码保存副本 中描述了如何处理对象在不同版本的 ArcGIS 中的持久化。在组件的持久化代码中,如果持久化对象引用,请考虑这些被持久化的对象也需要正确处理文档版本。

所有核心 ArcObjects 组件都正确处理文档版本持久化 - 它们不实现IDocumentVersionSupportGEN接口,而是在内部处理此问题。如果要将对象持久化到 ObjectStream,则可以依赖所有核心 ArcObjects 组件来正确持久化而不管版本如何,或者使用类似于IDocumentVersionSupportGEN.ConvertToSupportedObject方法的方法将自身转换为合适的替换对象。

加载时的错误处理

如果您在尝试读取流时遇到错误,则必须将错误抛出到客户端。因为流是连续的,所以您的代码不应尝试继续读取,因为这会导致流指针未正确定位,而无法正确读取下一个值。

因此,在编写和测试持久性代码时,您应该始终特别小心。查看以下 版本兼容性 部分。如果您正确地创建了 向后兼容 的组件,您可以避免持久性代码中的许多错误。

安全装载

在某些情况下,尽管代码中有错误,ArcGIS 仍可以通过使用安全加载技术继续加载文档。

错误的影响可能因组件类型而异。例如,如果 ArcGIS 尝试从文档加载图层但失败,ArcMap 将继续加载文档的其余部分。无论此功能如何,都对您的组件进行编码,如果在退出 Load 函数之前无法完成加载,则会向调用函数引发错误。

未注册的类

您有责任确保在机器上注册了您的组件,以让机器可以打开文档(包含了您的组件的持久化)。

版本兼容性

如果你开发一个可持久化组件的新版本,很可能你需要持久化额外的状态信息——这意味着您需要更改类的持久化签名。虽然因此更改了类的持久化签名,但是,您的组件仍然可以保持二进制兼容性,并具有相同的 ClassID。

从开发周期开始时,就应确保组件在持久化时与自身的其他版本兼容。这允许您在使用 COM 升级组件时,充分利用二进制兼容性,而无需重新编译组件的客户端。

ArcGIS 中的兼容性

使用以下 ArcGIS 版本兼容性模型,对自定义组件进行编码:

  • 向后兼容性——ArcGIS 文档文件的工作原理是向后兼容,这可能是最常见的持久性版本兼容性形式。这意味着 ArcGIS 客户端可以打开使用早期版本的 ArcGIS 创建的文档。
  • 前向兼容性——可以编写前向兼容的组件,例如,客户端可以使用比最初编译时更新的版本加载和保存组件。实现前向兼容性需要非常小心,因为它可能会产生又长又复杂的持久性代码。

尽管 ArcGIS 没有实现一般的向前兼容性(这通常不是您的组件的要求),但从 ArcGIS 9.1 开始,用户可以使用保存副本命令,将其文档保存为特定的先前 ArcGIS 版本。然后可以使用之前的 ArcGIS 版本打开保存的文档。在 ArcGIS 9.2 中,您可以保存到 ArcGIS 8.3 或 ArcGIS 9 和 9.1。ArcGIS 9.1地图文档直接兼容ArcGIS 9;因此,没有选项可以将它们专门保存到版本 9。请参见下图:

显示向后和向前兼容性的插图。

如果您的组件无需重新编译为当前和以前的 ArcGIS 版本,也可工作,则无需调整组件以确保“保存副本”功能。

但是,如果您的对象无法持久化保存到先前版本的 ArcGIS,请实施 IDocumentVersionSupportGEN。此接口允许您提供替代对象。有关详细信息,请参阅 编码保存副本功能

如果您的对象可以保存到以前版本的 ArcGIS,但您可能需要在持久性代码中考虑这一点,请调整 IPersistVariant 的实现以识别要持久化的版本并进行任何必要的更改。有关详细信息,请参阅 识别文档版本

实现持久化的向后兼容性

您现在将看一个通过创建类的两个不同版本来创建向后兼容类的示例。下面的代码示例是逐步构建的,展示了如何每次编写持久性方法。您将创建一个在地图上绘制等边三角形元素的自定义图形元素。有关详细信息,请参阅示例:三角形图形元素。

版本 1

对于三角形元素(triangle element)的第一个版本,存储成员变量如下:

[C#]

private double m_size=20.0;
private double m_scaleRef=0.0;
private esriAnchorPointEnum m_anchorPointType=esriAnchorPointEnum.esriCenterPoint;
private bool m_autoTrans=true;
private string m_elementType="TriangleElement";
private string m_elementName=string.Empty;
private ISpatialReference m_nativeSR=null;
private ISimpleFillSymbol m_fillSymbol=null;
private IPoint m_pointGeometry=null;
private IPolygon m_triangle=null;

[VB.NET]

Private m_size As Double=20.0
Private m_scaleRef As Double=0.0
Private m_anchorPointType As esriAnchorPointEnum=esriAnchorPointEnum.esriCenterPoint
Private m_autoTrans As Boolean=True
Private m_elementType As String="TriangleElement"
Private m_elementName As String=String.Empty
Private m_nativeSR As ISpatialReference=Nothing
Private m_fillSymbol As ISimpleFillSymbol=Nothing
Private m_pointGeometry As IPoint=Nothing
Private m_triangle As IPolygon=Nothing

三角形元素作为图形元素,必须是可持久化,必须实现 IPersistVariant 接口。

在进行持久化编码之前,添加 c_Version 私有常量。在整个持久化代码(生命周期)中,使用此常量来存储类的版本。将常量设置为 1,因为这是该类的第一个版本。

请参见以下代码示例:

[C#]

private const int c_Version=1;

[VB.NET]

Private Const c_Version As Integer=1

使用这个值是版本兼容的关键。根据此编号对保存和加载方法进行编码。在 Save 方法中写入流的第一件事是这个持久化版本值。

请参见以下代码示例:

[C#]

public void Save(IVariantStream Stream)
{
    Stream.Write(c_Version);
    //...
}

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(c_Version)

将类状态写入流。请参见以下代码示例:

[C#]

Stream.Write(m_size);
Stream.Write(m_scaleRef);
Stream.Write(m_anchorPointType);
Stream.Write(m_autoTrans);
Stream.Write(m_elementType);
Stream.Write(m_elementName);
Stream.Write(m_nativeSR);
Stream.Write(m_fillSymbol);
Stream.Write(m_pointGeometry);
Stream.Write(m_triangle);
}

[VB.NET]

Stream.Write(m_size)
Stream.Write(m_scaleRef)
Stream.Write(m_anchorPointType)
Stream.Write(m_autoTrans)
Stream.Write(m_elementType)
Stream.Write(m_elementName)
Stream.Write(m_nativeSR)
Stream.Write(m_fillSymbol)
Stream.Write(m_pointGeometry)
Stream.Write(m_triangle)
End Sub

在 Load 方法中,读取持久化类的版本号,并将该值存储在 ver 局部变量中。如果ver > c_Version,则意味着:被加载的文档是更高版本的类进行持久化产生,当前版本的类不知道如何正确加载持久化信息。

如果ver = 0,则某处存在错误,因为最小预期值为1。

两种情况都是错误并且可能导致流损坏。在这些情况下,将异常引发回调用对象。

请参见以下代码示例:

[C#]

public void Load(IVariantStream Stream)
{
    int ver=(int)Stream.Read();
    if (ver > c_Version || ver <= 0)
        throw new Exception("Wrong version!");
    //...
}

[VB.NET]

Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    Dim ver As Integer=CInt(Fix(Stream.Read()))
    If ver > c_Version OrElse ver <= 0 Then
        Throw New Exception("Wrong version!")
    End If

由于在实例化对象后的某个时间可能会调用 Load,因此请确保在 Load 开始时初始化类的默认值。请参见以下代码示例:

[C#]

InitMembers();

[VB.NET]

InitMembers()

在检查保存的版本信息是您期望的版本后,读取持久化类状态并设置当前对象的成员(此验证在以后生成组件的新版本时很有用)。请参见以下代码示例:

[C#]

if (ver == 1)
{
    m_size=(double)Stream.Read();
    m_scaleRef=(double)Stream.Read();
    m_anchorPointType=(esriAnchorPointEnum)Stream.Read();
    m_autoTrans=(bool)Stream.Read();
    m_elementType=(string)Stream.Read();
    m_elementName=(string)Stream.Read();
    m_nativeSR=Stream.Read() as ISpatialReference;
    m_fillSymbol=Stream.Read() as ISimpleFillSymbol;
    m_pointGeometry=Stream.Read() as IPoint;
    m_triangle=Stream.Read() as IPolygon;
}

[VB.NET]

If ver=1 Then
    m_size=CDbl(Stream.Read())
    m_scaleRef=CDbl(Stream.Read())
    m_anchorPointType=CType(Stream.Read(), esriAnchorPointEnum)
    m_autoTrans=CBool(Stream.Read())
    m_elementType=CStr(Stream.Read())
    m_elementName=CStr(Stream.Read())
    m_nativeSR=TryCast(Stream.Read(), ISpatialReference)
    m_fillSymbol=TryCast(Stream.Read(), ISimpleFillSymbol)
    m_pointGeometry=TryCast(Stream.Read(), IPoint)
    m_triangle=TryCast(Stream.Read(), IPolygon)
End If

现在您已经有了类的第一个版本,您可以编译和部署该组件。此时,用户可能拥有包含持久化 TriangleElement 图形元素的地图文档。

您可以使用项目中包含的 TriangleElementTool 将新的三角形元素添加到文档中。

版本 2

现在要求您添加功能以允许旋转三角形元素。要满足这些要求,须调整您的组件。添加一个类成员以保存元素的旋转角度,并在构建三角形时以及在 ITransform2D 接口的实现中将旋转应用于元素。

请参见以下代码示例:

[C#]

private double m_rotation=0.0;

[VB.NET]

Private m_rotation As Double=0.0

由于需要持久化的数据现在已更改,因此将类的持久化版本号加 1,设置为 2。

请参见以下代码示例:

[C#]

private const int c_Version=2;

[VB.NET]

Private Const c_Version As Integer=2

当您更改组件的持久化签名时,在 Load 方法中如果读到旧版本,新组件读取原始数据,仍应遵循第一个持久化版本的方式。

[C#]

public void Save(IVariantStream Stream)
{
    Stream.Write(c_Version);
    //...
    //New addition in version 2.
    Stream.Write(m_rotation);
}

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements IPersistVariant.Save
    Stream.Write(c_Version)
    ...
    'New addition in version 2.
    Stream.Write(m_rotation)
End Sub

调整 Load 方法,使之总能从流中读取“由新旧版本组件保存的”数据。

请参见以下代码示例:

[C#]

public void Load(IVariantStream Stream)
{
    int ver=(int)Stream.Read();
    if (ver > c_Version || ver <= 0)
        throw new Exception("Wrong version!");

    InitMembers();

    if (ver == 2)
    {
        m_rotation=(double)Stream.Read();
    }
}

[VB.NET]

Public Sub Load(ByVal Stream As IVariantStream) Implements IPersistVariant.Load
    Dim ver As Integer=CInt(Fix(Stream.Read()))
    If ver > c_Version OrElse ver <= 0 Then
        Throw New Exception("Wrong version!")
    End If
    
    InitMembers()
    ...
    
    If ver=2 Then
        m_rotation=CDbl(Stream.Read())
    End If
End Sub

在对象已被读取为版本 1 的情况下,方法 InitMembers() 设置默认旋转值。

如果您已经加载了持久性模式的第一个版本,请将第二个版本的成员变量设置为默认值。如果您已经加载了持久性模式的第二个版本,请阅读其他成员。

编译和部署 TriangleElement 类的版本 2。此时,如果新版本的组件遇到旧的持久化版本,它可以正确从持久化数据中加载。

当再次保存文档时(由版本 2 组件),持久版本将是版本 2。

过时的持久数据

在许多情况下,新的组件版本需要将数据添加到持久性模式中。有时,组件的新版本不再需要将某些数据保存到流中。

在这些情况下,如果您的新组件遇到旧版本持久化到流中,请读取过时的数据值;否则,流指针会留在错误的位置,并且无法正确读取下一个值。您可以在读取后丢弃过时的值,并在新的 Save 方法中仅保存所需的数据。请参见下图:

文档保存和文档加载的插图。

另一种可能性是创建一个类,如果由新版本的组件保存,该类不会更新到新的持久性模式。这使旧组件能够加载持久对象。

写在 Save 方法开头的持久性版本号应该说明使用的持久性模式。

为了使持久性版本的实现更直接,您可能需要考虑使用 PropertySet。您的组件的每个版本都可以根据需要添加更多或不同的属性,那么 Save 和 Load 事件只需要持久化当前的 PropertySet。如果选择此方法,请确保在 Load 事件开始时将所有类成员设置为其默认值,以防在当前 PropertySet 中找不到某些类成员的值。

编码保存副本功能

要允许您的组件在特定版本的 ArcGIS 中持久保存到文档中,例如,当用户在 ArcMap 中选择保存副本命令时,您可能需要实现 IDocumentVersionSupportGEN。

public interface IDocumentVersionSupportGEN
{
    bool IsSupportedAtVersion([In] esriArcGISVersion docVersion);
    object ConvertToSupportedObject([In] esriArcGISVersion docVersion);
}

此接口指定以下功能:

  • 允许组件指示它是否可以持久保存到特定版本的 ArcGIS 文档
  • 如果组件不能持久化到指定版本,则允许组件提供合适的替代对象而不是自身

如果 ArcGIS 无法将给定的可持久对象转换为 IDocumentVersionSupportGEN,则它假定该对象对于任何版本的 ArcGIS来说,其持久化保存不随版本变化而变化。

例如,在将自定义符号应用到 ArcGIS 9.2 文档中的图层,并且用户选择将文档的副本保存为 ArcGIS 8.3 文档的情况下,符号的持久化过程遵循以下一般步骤,来创建特定于版本的文档:

  1. 当 ArcMap 尝试持久化符号对象时,它会尝试将该符号对象转换为 IDocumentVersionSupportGEN,以确定是否可以将符号保存到 8.3 文档中。

  2. 如果转换失败,ArcMap 会照常调用该符号对象的持久化方法,假设符号可以持久化到任何版本的 ArcGIS。

  3. 如果转换成功,ArcMap 然后调用 IDocumentVersionSupportGEN.IsSupportedAtVersion 方法,传入一个指示所需 ArcGIS 版本的值。

    • 如果 IsSupportedAtVersion 返回 true,ArcGIS 将照常调用该符号对象的持久性方法。

    • 如果 IsSupportedAtVersion 返回 false,ArcGIS 将对符号调用 IDocumentVersionSupportGEN.ConvertToSupportedObject 方法。然后,该符号对象会创建一个合适的替代符号对象,该对象可以保留到指定的 ArcGIS 文档版本。然后从 ConvertToSupportedObject 返回此替代符号对象,ArcGIS 会将对原始符号的对象引用替换为对此替代符号的引用。下图显示了最后一种情况:
      保存副本的插图。

实施 IDocumentVersionSupportGEN

IsSupportedAtVersion 方法用于确定您的组件可以保留到哪些 ArcGIS 文档版本。根据传入此方法的参数指示的文档版本,从此方法返回 true 或 false。该参数是一个 esriArcGISVersion 枚举值。

例如,如果您的组件在所有版本的 ArcGIS 中都能很好地使用,您可以从 IsSupportedAtVersion 返回 true,尽管在这种情况下,您根本不需要实现该接口。

但是,如果您的组件依赖于仅从某个版本开始存在的核心对象或功能的存在,则返回 false 以指示该对象不能在以前版本的文档中使用(例如,前面解释的三角形元素示例)。您可以使用以下代码示例中所示的代码,来防止将元素按原样保存到 8.3 文档中:

[C#]

public bool IsSupportedAtVersion(esriArcGISVersion docVersion)
{
    //Support all versions except 8.3.
    if (esriArcGISVersion.esriArcGISVersion83 == docVersion)
        return false;
    else
        return true;
}

[VB.NET]

Public Function IsSupportedAtVersion(ByVal docVersion As esriArcGISVersion) As Boolean Implements IDocumentVersionSupportGEN.IsSupportedAtVersion
    'Support all versions except 8.3.
    If esriArcGISVersion.esriArcGISVersion83=docVersion Then
        Return False
    Else
        Return True
    End If
End Function

要允许用户将具有自定义三角形元素的文档副本保存为 8.3 文档,您可以使用以下示例中所示的代码,来创建一个基本标记元素,该元素使用三角形字符标记符号作为自定义的替代三角形元素,并从 ConvertToSupportedObject 返回。(这可能不是每个自定义图形元素的合适解决方案,但该原则可以应用于类似代码以创建适合您自己的自定义的对象)。

[C#]

public object ConvertToSupportedObject(esriArcGISVersion docVersion)
{
    //In the case of 8.3, create a character marker element and use a triangle marker.
    ICharacterMarkerSymbol charMarkerSymbol=new CharacterMarkerSymbolClass();
    charMarkerSymbol.Color=m_fillSymbol.Color;
    charMarkerSymbol.Angle=m_rotation;
    charMarkerSymbol.Size=m_size;
    charMarkerSymbol.Font=ESRI.ArcGIS.ADF.Converter.ToStdFont(new Font(
        "ESRI Default Marker", (float)m_size, FontStyle.Regular));
    charMarkerSymbol.CharacterIndex=184;

    IMarkerElement markerElement=new MarkerElementClass();
    markerElement.Symbol=(IMarkerSymbol)charMarkerSymbol;

    IPoint point=((IClone)m_pointGeometry).Clone()as IPoint;
    IElement element=(IElement)markerElement;
    element.Geometry=(IGeometry)point;

    return element;
}

[VB.NET]

Public Function ConvertToSupportedObject(ByVal docVersion As esriArcGISVersion) As Object Implements IDocumentVersionSupportGEN.ConvertToSupportedObject
    'In the case of 8.3, create a character marker element and use a triangle marker.
    Dim charMarkerSymbol As ICharacterMarkerSymbol=New CharacterMarkerSymbolClass()
    charMarkerSymbol.Color=m_fillSymbol.Color
    charMarkerSymbol.Angle=m_rotation
    charMarkerSymbol.Size=m_size
    charMarkerSymbol.Font=ESRI.ArcGIS.ADF.Converter.ToStdFont(New Font("ESRI Default Marker", CSng(m_size), FontStyle.Regular))
    charMarkerSymbol.CharacterIndex=184
    
    Dim markerElement As IMarkerElement=New MarkerElementClass()
    markerElement.Symbol=CType(charMarkerSymbol, IMarkerSymbol)
    
    Dim point As IPoint=TryCast((CType(m_pointGeometry, IClone)).Clone(), IPoint)
    Dim element As IElement=CType(markerElement, IElement)
    element.Geometry=CType(point, IGeometry)
    
    Return element
End Function

对于从 IsSupportedAtVersion 返回 false 的每个 esriArcGISVersion,请在 ConvertToSupportedObject 中实施合适的替代方案。如果您不这样做,您将阻止用户将文档的副本保存到该版本,并且用户可能不会收到有关他们尝试保存副本失败的原因的任何信息。也不要返回空引用,因为 ArcGIS 可能会尝试将此空引用应用于属性,这可能会导致保存的文档损坏。

实现持久化时的职责

在创建可持久化组件时,需要履行以下职责:

控制数据的持久化

请记住,您控制写入流的内容(决定持久化哪些数据、怎么持久化这些数据)。如果您的类需要保留对另一个自定义对象的引用,您有以下两种选择:

  • 在另一个自定义类上实现持久性并像对任何其他对象一样持久化该类。
  • 或者,将次要类的成员写入主要类的持久流。尽管此选项可能更简单,但请使用第一种方法,因为它更易于维护和扩展。

错误处理和文件完整性

如果您在流加载事件中引发错误,这可能会导致当前结构化存储(例如当前 .mxd 文件)不可读。编写持久性代码时要小心,以确保保持存储文件的完整性。

扩展中的 ObjectStream

如前所述,为每个扩展创建一个单独的 ObjectStream。这种情况可能会导致不考虑这种差异的组件出现问题。

如果您持久化一个已在文档其他地方持久化的对象引用,因此持久化到一个单独的 ObjectStream,这将导致两个单独的对象被持久化。重新加载文档时,您持久化的对象不再是在主文档 ObjectStream 中持久化的对象。您可能希望在扩展启动中而不是在持久性代码中初始化此类对象。

版本兼容性一致性

确保您的组件与 ArcGIS 使用的版本兼容性一致。至少确保您的组件向后兼容 - 客户端(例如,ArcMap)可以打开使用您的组件的早期版本创建的文档。

如果需要,请考虑实施 IDocumentVersionSupportGEN,以确保您的组件可以在保存为先前版本 ArcGIS 的文档中打开。对于从 IsSupportedAtVersion 返回 false 的每个 ArcGIS 版本,请确保从 ConvertToSupportedObject 返回有效的替代对象。

posted @ 2022-04-26 17:10  误会馋  阅读(95)  评论(0编辑  收藏  举报