65.9K
CodeProject 正在变化。 阅读更多。
Home

DataGridView 列中的 BLOB(带命令按钮列、XML 和分层)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2011年8月17日

CPOL

29分钟阅读

viewsIcon

39154

downloadIcon

2312

这是一个在 Windows 窗体的 DataGridView 列上使用 C++/CLI 实现 BLOB 的工作示例,其中数据来自 XML 文档。

Blob column in use

目标

本文的目的是提供一个在 Windows 窗体的 DataGridView 上使用 C++/CLI 实现 BLOB 列的工作示例。它还演示了一些相关和不相关的功能

  • DataGridView 上的命令按钮
  • Enum 标志的实际应用
  • 在同一个解决方案中创建两个 Visual Studio 项目
  • 不引用 schema 解析 XML(使用递归)
  • 不引用 schema 创建和更新 XML 节点
  • 管理 XML 属性
  • 使用预处理器指令作为交互式调试的替代方案
  • 分层方法的好处
  • 比较和对比 XML 数据处理器与 MySQL 数据处理器

背景

DataGridView 上的枚举类列一样,使用 C++/CLI 在网上找到 DataGridView 上的 BLOB 示例也很少,但在这种情况下,有一些不错的 C# 和 VB 示例可用。大多数示例使用硬编码数据或关系数据库,但这个示例是少数从 XML 获取数据的示例之一。

示例起源

该示例来自我正在开发的一个系统,该系统依赖于存储的图像来识别客户。

一个可疑的设计决策

在深入探讨之前,有必要提请注意经验丰富的用户会认为将 DataGridView 与使用文档对象模型 (DOM) 访问的 XML 结合起来是一个疯狂的决定。为什么?因为整个数据集在内存中存储了两次!更糟糕的是,我们使用的是内存占用大的 BLOB(Binary Large Objects,二进制大对象)。如果您正在实现一个带有 DataGridView 的类似窗体,并且选择 XML 作为数据存储,那么请考虑使用 XML 流 XmlReader/XmlWriter

然而,这是一个教程示例,我想模拟我的应用程序在即时更新和删除方面的 MySQL 数据库行为,为此,DOM 更适合。同样,如果您在日常工作中需要操作 XML,我相信 DOM 提供了更大的灵活性;这也影响了我在此处使用它的决定。

风格说明

我的 .h 文件中除了定义之外,不喜欢包含太多内容,因此当 IDE 将 Windows 窗体函数或事件添加到 .h 文件时,我的做法是在 .cpp 文件中调用用户定义的函数,并传递参数。这是一个开销,因为我无法强制 IDE 将 Windows 窗体函数直接插入到 .cpp 文件中,而且当我将它们移动到那里时,IDE 会感到困惑——但我更喜欢它所提供的顺序。好的,现在进入正题。

管理 BLOB

使用命令按钮加载 BLOB

在查看 DataGridView 上的 BLOB 之前,我们首先向网格添加一个命令按钮。我们将使用此按钮开始加载 BLOB。通过编辑 DataGridView 的属性来添加按钮,然后选择“编辑列”。将其中一列的类型设置为 DataGridViewButtonColumn

再次使用“属性”面板,在 DataGridView 上添加一个 CellContentClick 事件。这是我的代码

if (e->ColumnIndex != dgLoadImage->Index)
    return; // Hardcoded to only accept the event on the button column
else
{
    OpenFileDialog^ openfiledialog = gcnew OpenFileDialog();
    openfiledialog->DefaultExt = L"bmp|jpg|gif";
    if (openfiledialog->ShowDialog() ==  System::Windows::Forms::DialogResult::OK)
    {
        m_FileName = openfiledialog->FileName;

        String ^MessageString = "Button Click";
        MessageBox::Show(m_FileName,MessageString);
        pictureBox1->Image= Drawing::Image::FromFile(m_FileName);
        gridPerson->Rows[e->RowIndex]->Cells[dgBlob->Index]->Value=
                   Drawing::Image::FromFile(m_FileName);
        m_fs = gcnew FileStream(m_FileName, FileMode::Open, FileAccess::Read);
        m_ElementList = m_ElementList | m_FlagBits::BLOB;
    }
}

首先,如果所选列不是按钮列(本例中为 dgLoadImage),则点击事件将退出。现在,由于网格上只有一个按钮列,此语句是冗余的,但将来如果需要,它仍然存在。

else 块中的第一个操作是提供按典型文档类型筛选的标准文件打开对话框。但是您会注意到筛选中有一个错误——所有内容都返回了。由于它与任何内容无关,我允许它通过。

行:

pictureBox1->Image= Drawing::Image::FromFile(m_FileName);

将图像拉入网格下方的 PictureBox

这引出了本文的第一行关键代码

gridPerson->Rows[e->RowIndex]->Cells[dgBlob->Index]->Value=
                    Drawing::Image::FromFile(m_FileName);

DataGrid (gridPerson) 当前行上的 BLOB 列 dgBlob 现在存储了图像。最后,我将 BLOB 流式传输到 FileStream^ 变量 m_fs 中,并设置一个枚举标志以指示 dgBlob 列的值已发生更改,两者都用于后续使用。

从行中提取 BLOB

我正在使用 DataGridView 上的 RowEntered 事件来演示从 DataGridView 行中提取 BLOB。这是所涉及的代码

try
{
    array<Byte> ^byteBLOBData = gcnew array<Byte>(0);
    byteBLOBData = safe_cast<array<Byte>^>(
     gridPerson->Rows[e->RowIndex]->Cells[dgBlob->Index]->Value);
    MemoryStream^ stream = gcnew MemoryStream(byteBLOBData);

    pictureBox1->Image = Image::FromStream(stream);
}
catch(...)
{
    pictureBox1->Image = nullptr;
}

首先,我声明一个新的字节数组 byteBLOBData,并将 BLOB 单元格强制转换为其中。然后我将其转换为内存流,最后将该流传递给 pictureBox1 图像。

注意 catch(...) 的使用。这很像使用 goto,不建议但在有限情况下有用。不建议使用它是因为您忽略了错误情况,而没有检查或以其他方式处理它。但是,我只在一种情况下使用它,即我乐于为元素分配一个空值,无论导致错误发生的原因是什么。

保存(持久化)BLOB

现在,我们已经在 DataGridView 上有了 BLOB,但是如果窗体一关闭我们就丢失了它,那它的用处就不大了。我们需要以某种方式保存它。为此,我正在使用 RowValidating 事件将 BLOB 单元格捕获到一个定义为 array<Byte>^ 的变量中。

Person 类有一个 BLOB 字段 m_Blob,定义为 arrayBlobXMLGrid<Byte>^p_Blob 是管理此字段数据传输的属性。我正在使用二进制读取器 (br) 从之前保存的文件流 (m_fs) 中提取图像数据。m_fs 上的 Length 属性决定了读取到 Person 类上的 BLOB 中的字节数。

BinaryReader ^br = gcnew BinaryReader(m_fs);
TransactionPerson->p_Blob = br->ReadBytes(safe_cast<int>(m_fs->Length));
TransactionPerson->p_BlobLen = m_fs->Length;
TransactionPerson->p_BlobName= m_fs->Name;
br->Close();
m_fs->Close();

关闭二进制读取器和文件流变量。完成保存需要一些特定的操作,具体取决于您使用的存储库的性质。

保存到 XML

String^ modifiedBlob;
modifiedBlob = Convert::ToBase64String(arg_PersonRow->p_Blob);

BLOB 节点的 value 元素将是字符串类型,因此在将其传递给临时 String^ 变量之前,我对我们的字节数组应用了 Base64 转换。完成此操作后,它可以像任何其他节点一样保存。

使用 SQL 保存到关系数据库

cmd->Parameters->Add("@Photo", MySqlDbType::LongBlob, 
     arg_PersonRow->p_Photo->Length)->Value = arg_PersonRow->p_Photo;

@photo 是一个命令参数,CommandText 字符串中令牌前的 @ 符号将该令牌标记为参数,这里的代码将该参数加载到我们的 BLOB 中,将其标识为 MySQL 数据库中的 MySqlDbType::LongBlob。之后,Insert 或 Update 语句将完成保存过程。

将保存的 BLOB 加载到 DataGridView 中

完成 BLOB 管理所需的第三个方面是将我们保存的 BLOB 信息加载回 DataGridView 的代码。在这种情况下,我的方法取决于图像的存储格式。

从 XML

当我在解析过程中检测到包含 BLOB 数据的 XML 节点时,我通过从节点值中提取 Base64String 信息来填充一个临时字节数组

tmpBlob = Convert::FromBase64String(ArgValue);

从 SQL

从 SQL 提取结果中的 BLOB 也涉及填充临时字节数组

tmp_Photo = gcnew array<Byte>(reader->GetInt32(11));
reader->GetBytes(3,0,tmp_Photo,0,reader->GetInt32(11));

此示例中字节数组的大小由字段 11 确定(图像保存时图像长度存储在此处)。然后我们使用 MySQLDataReaderGetBytes 方法提取 BLOB。这些是参数

  • 3 - 提取行中 BLOB 的元素编号。
  • 0 - 偏移量,由于我们没有使用,图像将从元素 3 的位置 0 找到。
  • tmp_Photo - 将接收 BLOB 的字节数组的名称。
  • reader->GetInt32(11) - 存储在提取行元素 11 中的 BLOB 长度。

完成加载

我的 fetch 模块将返回一个 Person 列表,用于填充到 DataGridView。此列表的格式无论是来自 XML 还是 SQL 都相同。无需进一步的 BLOB 特定操作即可将其加载到 DataGridView。为了完整性,这是代码

array<Object^>^ itemRec = gcnew array<Object^> {candidate->p_PersonID,
                candidate->p_Blob,
                candidate->p_Surname,
                candidate->p_Forename
                };

gridPerson->Rows->Add(itemRec);

这就是管理 DataGridView 上的 BLOB 所涉及的所有内容,但用于实现此目的的代码中还有更多值得一看的地方。

DataGridView 上的命令按钮

在此示例中,我在设计时使用 IDE 将命令按钮添加到 DataGridView。类型为 DataGridViewButtonColumn 的列将为您创建按钮。为了使用该按钮,我已将 CellContentClick 事件添加到 DataGridView。乍一看,此事件的代码看起来有点奇怪,因为它以一个 if 语句开头,当单击事件发生在除按钮之外的任何列上时,该语句会强制退出。

if (e->ColumnIndex != dgLoadImage->Index)
    return; // Hardcoded to only accept the event on the button column
else
{
   :
}

这只是我格外小心。CellContentClick 事件只会针对按钮列触发,因此在此示例中,我无需担心;但是,如果我的窗体上有多个按钮,那么我需要确定是哪个列触发了单击事件并采取相应的行动。

Enum 标志的实际应用

在我最近关于 DataGridView 上枚举列的示例中,代码还包括枚举标志,但在该示例中没有使用它们。在这里,您将了解如何定义和使用枚举标志。枚举标志背后的技术大量借鉴了传统计算中的位操作,但您无需特别关注这一点。因此,首先,这是此示例中的枚举标志定义

[Flags] enum class m_FlagBits
{ 
    PERSONID = 1, 
    SURNAME = 2, 
    FORENAME = 4,
    BLOB = 8, 
};

m_FlagBits m_ElementList;

首先要注意的是 [Flags] 属性。这指示编译器将随后的枚举类定义视为一组位标志。下一个重要方面是与每个标志关联的编号。它必须从 1 开始,并且每个新标志的编号加倍。不遵循此做法不会产生任何编译器错误,但会在运行时导致意外结果。最后,我们定义了一个变量,在本例中为 m_ElementList,其类型是我们的新枚举标志类。

此示例中的标志对应于我们的 DataGridView 中的列,我们将使用它们来记录哪些列已更改了值。

每次用户输入新行时,我都会使用 DataGridView 上的 RowEntered 事件将所有标志设置为 false。这是代码

m_ElementList = m_ElementList & ~ m_FlagBits::PERSONID;
m_ElementList = m_ElementList & ~ m_FlagBits::SURNAME;
m_ElementList = m_ElementList & ~ m_FlagBits::FORENAME;
m_ElementList = m_ElementList & ~ m_FlagBits::BLOB;

例如,考虑 PERSONID。上面的第一个语句告诉我们 m_ElementList 等于它自己,并且 PERSONID 为 false。这对于每个标志都重复。接下来,当列值发生更改时,我使用此行打开该列的标志

m_ElementList = m_ElementList | m_FlagBits::PERSONID;

如果您使用调试器观察 m_ElementList,您会看到它为每个设置为 true 的标志添加了类定义中的值,并减去了任何设置为 false 的标志值。因此,在内部,m_ElementList 的值为 5 将告诉系统 PERSONIDFORENAME 为 true,所有其他标志为 false。但我们不必担心查询此值。我们可以按名称检查每个标志,以查看是否需要采取任何操作

if (static_cast<int>(m_ElementList) & static_cast<int>(m_FlagBits::PERSONID))
{
    TransactionPerson->p_PersonID = System::Convert::ToInt16(lblPersonID->Text);
}

注意将值强制转换为整数。这是我学到的方法,它工作可靠。可能有一个更简洁的方法。用英语来说,我们说的是,“如果 m_ElementList 包含 PERSONID 为 true,那么执行某些操作”。考虑解释这个的旧式位操作方法。就我们而言,PERSONIDFORENAME 为 true,所有其他都为 false。我们可以将 m_ElementList 表示为“1010”,其中 1 代表 PERSONIDFORENAMEm_FlagBits::PERSONID 表示为 1000。对它们应用按位与操作会产生二进制加法,结果为 1000,或者 PERSONID 为 true,从而允许执行某些操作。

然而,如果一个标志设置为 false,例如 m_FlagBits::SURNAME(表示为 0100),当与包含“1010”的 m_ElementList 进行 AND 运算时,它将返回 false 值,因此 Surname 不会发生任何操作。

在大型应用程序中,数据存储(无论是表还是 XML)都会有很长的行。仅更新那些发生更改的列可以显著提高性能,而枚举标志是管理这种控制程度的有用方法。

在同一个解决方案中创建两个 Visual Studio 项目

通常,我的项目和解决方案之间是 1 对 1 的关系,这更多是习惯使然。但是,如果我确实重新开始编写自己的商业代码,那么我的一些模块将在多个系统中发挥作用。这个示例中有两个模块,为了方便选择下载它的成员加载,我选择了尝试单解决方案方法。我从单解决方案中看到的主要好处是整个应用程序可以一次编译。当然,IDE 上的解决方案资源管理器也更有意义,并且不需要告诉 IDE 在哪里找到程序集,因为它将主 exe 和任何 DLL 放在解决方案级别的同一个文件夹中。

当您从菜单中单击“文件 -> 新建 -> 项目”(或使用快捷键 Ctrl+Shift+N)时,默认的解决方案设置是“创建新解决方案”。将其更改为“添加到解决方案”,如下图所示

Setting the Solution attribute

不引用 Schema 使用递归解析 XML

本示例中用于解析 XML 和提取相关数据的功能集基于我用于复杂海关和消费税消息的功能集。虽然它们对于这个非常简单的 XML 来说是过度设计,但如果您需要在某个阶段查询大型 XML 文档,它们将非常有用。我从 Stephen Fraser 的书“Pro Visual C++/CLI and the .NET 3.5 Platform”(Apress 2009)中提供的示例中获取了它们。此方法将 XML 用作 DOM 或文档对象模型。它由一个 Navigate 函数驱动,该函数需要文档中的第一个节点作为种子,从中开始处理。这是您获取第一个节点的方法

XmlNode ^node = doc->FirstChild;

导航

Navigate 是递归实际应用的一个极好的例子。如果您将 XML 文档可视化为一棵树,Navigate 会沿着最左侧可用的路径向下遍历,同时调用自身处理当前节点的第一个子节点和当前节点的下一个兄弟节点。每次调用都会被存储以便稍后重新访问。当它遇到空节点时,它会停止调用并“掸去”最近存储的 NextSibling,然后再次开始调用和存储序列,直到遇到空节点。这个过程会重复进行,直到没有更多的节点需要处理。

但是 Navigate 不仅仅是调用自己。一旦它确定当前节点不为空,它就会为文本类型节点调用 Process_Node 函数,或者在节点具有属性时调用 Process_Attribute

如前所述,Navigate 将节点作为参数,但您还会看到另一个参数 depth。此参数不是必需的,但我保留它,因为它有时在调试期间很有用,尤其是在文档中不同级别具有相同子树的复杂 XML 上。

Process_Node

Process_Node 将当前节点的父节点和当前节点的值作为参数。那么为什么是父节点而不是节点本身呢?这是 DOM 的一个怪癖。例如,考虑我们的一个节点 PERSON_ID,它的节点类型是“Element”,其值(实际的 Person ID)存储在类型为 Text 的子节点中。因此,当我们遇到文本节点时,我们将该文本节点的值及其父节点传递给 Process_Node,以便我们知道正在处理哪个元素。在 Process_Node 中,我们按名称检查每个节点并根据该节点采取行动。因为此示例正在填充网格,所以我选择保留信息,直到我获取到当前行上的所有兄弟节点。

Process_Attribute

Process_Attribute 接受当前节点、当前属性和当前属性的值作为参数。我们可以将其简化为仅当前属性,并且仍然提供相同的功能。在此示例中,当前节点的唯一目的是简化调试过程的信息。在大型文档中,如果不同节点上有相同名称的属性,它会提高代码的可读性,但没有其他贡献。类似地,将值作为单独的参数传递只会增加可读性。

通常,您会检查属性的名称和节点的名称,然后执行您选择的操作。

不引用 Schema 再次创建和更新 XML 节点

当设置了对节点进行操作的标志时,我搜索该节点,如果未找到并且我有要存储的值,那么是时候创建一个节点了,否则现有节点将通过更新节点的功能进行更新或删除。

创建节点

我创建 XML 节点的方法并不是特别吸引人,假以时日我会替换它,然而它确实有效,“如果它没坏……”等等。我所做的是从当前节点开始构建一个元素列表,一直添加到根节点。所有节点通用的元素都作为 Create_Node 函数的一部分添加。因为我们的示例使用一个非常浅的树,所以我们有多个这种奇怪逻辑的实例

node_list->Clear();
node_list->AddLast("Person_ID");
Create_Node(doc, arg_PersonRow->p_PersonID.ToString(), node_list, nullptr);

然而,在一个更复杂的例子中,它更有意义

node_list->Clear();
node_list->AddLast("GoodsShipment");
node_list->AddLast("CustomsGoodsItem[" + GI_Index.ToString() +"]");
node_list->AddLast("Consignee");
node_list->AddLast("Address");
node_list->AddLast("CityName");
Create_Node(doc, GoodsItemConsigneeCity->Text, node_list, nullptr);

创建节点

Create_Node 接受 XML 文档、要插入的节点值、路径列表以及(如果相关)属性列表作为参数。在 Create_Node 中执行的第一个操作是将任何公共节点添加到列表的开头。

为新节点创建文档元素

new_node = ArgDoc->CreateElement(ArgList->Last->Value->ToString());

接下来,为与新节点关联的值创建一个文本元素

node_text = ArgDoc->CreateTextNode(ArgValue);

然后将文本元素设为新节点的子元素

new_node->AppendChild(node_text);

我们在此处添加任何属性,但我将它们作为单独的主题处理。因此,继续,刚刚添加的节点将从列表中删除,并使用文档、刚刚创建的新节点和列表的剩余部分调用 Create_Parent 节点。Create_Parent 将决定新节点在文档中的位置。

创建父级

此函数递归调用自身,从 Create_Node 传递的新节点的父节点开始,从节点列表中获取该节点的父节点,在文档中检查它,如果父节点不存在,则创建它并不断重复此过程,直到找到一个确实存在的父节点。

查看该函数,如果它发现传入的节点列表为空,它会设置文档根,这就是它需要做的全部事情。

if (ArgList->Last == nullptr)
{
    XmlNode ^tmp;
    XmlAttribute ^att;
    tmp = ArgDoc->CreateXmlDeclaration("1.0","UTF-8","");
    ArgDoc->AppendChild(tmp);
    tmp = ArgDoc->CreateComment(
     "Test XML created by Ger Hayden's BlobGridXml example");
    ArgDoc->AppendChild(tmp);

    att = ArgDoc->CreateAttribute("xmlns:xsi");
    att->Value =  "http://www.w3.org/2001/XMLSchema-instance";
    ArgChild->Attributes->Append(att);

    ArgDoc->AppendChild(ArgChild);
    ret    urn; //if there is no parent left in the list, this is the root
}

然而,那是一个罕见的事件,唯一满足该条件的情况是将第一个节点添加到空白文档中。继续,现在事情开始变得笨拙,但就像我说的,它有效。需要将父级列表转换为一个字符串,该字符串可用于 XPath 搜索,以确定其末尾的父节点是否存在。这是构建该字符串的代码

TmpStr = "";
current = ArgList->First;
if (current != nullptr)
// We are only going to do this if the list has something
{
    TmpStr ="//";

    while (current != nullptr)
    {
        TmpStr += current->Value->ToString();
        current = current->Next;
        if (current != nullptr)
        {
            TmpStr +="/";
        }
    }
}

生成的字符串用于如下搜索文档

TmpNode = ArgDoc->SelectSingleNode(TmpStr);

找到节点后,传入的作为参数的新节点可以使用 Insert_Sibling 函数插入到其兄弟节点之间,我们的工作又完成了,无需进一步调用。

但是,您可能会发现父节点不存在,在这种情况下必须创建它

new_parent = ArgDoc->CreateElement(ArgList->Last->Value->ToString());

请注意,来自列表的一些节点名称在 [] 中带有索引。这会导致 CreateElement 失败,因此会检查这些名称,如果找到,则在 catch 块中将其删除。如果精简后的字符串无法用于创建父节点,则会发生故障,并且不会采取进一步的操作。成功创建父节点后,作为参数传入的节点将附加到其新的父节点,父节点将从父节点列表的末尾移除,最后,再次递归调用 Create_Node,并传入文档、新的父节点以及节点列表的剩余部分

new_parent->AppendChild(ArgChild);
ArgList->RemoveLast();  
Create_Parent(ArgDoc, new_parent, ArgList);

插入兄弟节点

Insert_Sibling 接受文档、父节点和子节点作为参数,然后确定子节点应该放置在其兄弟节点中的哪个位置。这是严重硬编码的,并且在文档格式更改时必须重新设计。此函数说明了使用模式来决定元素在文档中的位置的重要性。

它查看传入节点的名称,并为每个节点提名一个 XPath 格式的唯一父字符串,然后检查子节点的名称,并为每个子节点构建一个必须在其之前的兄弟节点列表。例如:

if (ArgNode->Name == "row")
{
    Parent = "//row[" + (Row_Index +1) + "]/";
    // Siblings Person_ID,Surname,Forename, Blob
    sibling_list->Clear();
 
    if (ArgChild->Name == "Person_ID")
    {
        sibling_list->Clear();
    }

    if (ArgChild->Name == "Surname")
    {
        sibling_list->Clear();
        sibling_list->AddLast("Person_ID");
    }

    if (ArgChild->Name == "Forename")
    {
        sibling_list->Clear();
        sibling_list->AddLast("Person_ID");
        sibling_list->AddLast("Surname");
    }

    if (ArgChild->Name == "Blob")
    {
        sibling_list->Clear();
        sibling_list->AddLast("Person_ID");
        sibling_list->AddLast("Surname");
        sibling_list->AddLast("Forename");
    }
}

现在我们知道在我们子节点之前的兄弟节点,我们需要查看哪个最接近子节点的兄弟节点实际存在。这是 Find_Candidate 函数的工作。找到候选节点后,子节点将添加在其之后

ArgNode->InsertAfter(ArgChild, Candidate);

否则,它将作为第一个兄弟节点添加

ArgNode->PrependChild(ArgChild);

查找候选人

Find_Candidate 接受文档、XPath 格式的父节点和兄弟节点列表作为参数。它遍历兄弟节点列表,存储最近找到的节点,直到列表耗尽或搜索失败。如果找到任何节点,它将返回最近找到的节点,否则返回 null。这是代码

Candidate = nullptr;

while (curr_list_item != nullptr)
{
    TmpNode = ArgDoc->SelectSingleNode(ArgParent + 
                       curr_list_item->Value->ToString());
    if (TmpNode != nullptr)
        Candidate = TmpNode;
    curr_list_item = curr_list_item->Next;
}
return Candidate;

更新节点

我的更新过程要简单得多。如果存在新值,则节点值会更改,但如果新值为 null,则节点将被删除。这由 Update_Node 函数管理。

更新节点

Update_Node 接受当前节点、其新值以及可能存在的属性列表作为参数。如果新值为 null,它将调用 Delete_Node 并结束。但当有值需要更新时,我们需要循环遍历节点的子节点,直到找到其文本节点

ArgNode = ArgNode->FirstChild;
while ((ArgNode->NodeType.ToString()!= "Text") && (ArgNode != nullptr))
{
    ArgNode = ArgNode->NextSibling;
}

如果找到文本节点,更改值的代码很简单

ArgNode->Value = ArgValue;

更改值后,属性可能也需要处理,但这将单独处理。如果未找到文本节点,则会显示一条消息,通知用户该节点尚未更新。

删除节点

Delete_Node 接受要移除的节点作为参数。我不喜欢我的 XML 中有无子节点,所以我在此函数中做的第一件事就是检查当前节点是否有任何兄弟节点

if ((ArgNode->NextSibling != nullptr) || (ArgNode->PreviousSibling != nullptr))
{
    has_siblings = true; // so we do not remove the parent
}
else
{
    has_siblings = false;
}

如果它有兄弟节点,那么这个函数所需要做的就是删除传入的节点,但首先我们需要找到它的父节点

if (ArgNode->ParentNode != nullptr)
{
    Parent = ArgNode->ParentNode;
    has_parent = true;
}
else
{
    has_parent = false;
}

一旦我们确定这个节点有一个父节点,删除它就非常容易了

Parent->RemoveChild(ArgNode);

如果刚刚删除的节点没有兄弟节点,但有一个父节点,那么我们也会使用递归调用删除父节点

Delete_Node(Parent);

这将确保删除操作持续进行,直到它回溯到一个有兄弟节点的节点。这使得文档保持整洁,确保任何元素都有子节点或一个值。在极端情况下,它会删除整个文档。但要发生这种情况,我们需要一个文档只有一个具有值的元素和一长串祖先。

管理 XML 属性

我们在查看 Create_NodeUpdate_Node 时已经接触过属性。在此示例中,我使用的属性是 BLOB 的名称和大小。您可能遇到的其他典型属性包括自由文本字段上的语言,或表示货币值时的货币代码。

当一个节点(在此示例中为 BLOB)具有属性时,我设置一个标志并构建一个属性列表,其元素由名称和值对组成。

AttributeList = gcnew List<CAttributeDefinition^>();
AttributeList->Add(gcnew CAttributeDefinition("Name", arg_PersonRow->p_BlobName));
AttributeList->Add(gcnew CAttributeDefinition("size", arg_PersonRow->p_BlobLen.ToString()));

属性列表作为最后一个参数包含在 Create_NodeUpdate_Node 中。

创建节点

Create_Node 中,我们为每个要添加的属性调用 Add_Attribute

更新节点

Update_Node 中的情况稍微复杂一些。我们为每个候选属性检查节点中已有的属性。如果找到匹配项,则更新该属性,否则添加该属性。这是代码

for each (CAttributeDefinition^ Candidate in ArgAttrList)
{
    bool AttrUpdated = false;
    // spin through the attributes. If this one is found update it, else add it.
    for (int i = 0; i < ArgNode->Attributes->Count; i++)
    {
        if (ArgNode->Attributes[i]->Name == Candidate->m_Attribute_Name)
        {
            ArgNode->Attributes[i]->Value = Candidate->m_Attribute_Value;
            AttrUpdated = true;
        }
    }
    if (!AttrUpdated)
    {
        Add_Attribute(doc, ArgNode->ParentNode, Candidate);
    }
}

但这还不是全部——如果发现节点没有属性,那么为每个要添加的属性调用 Add_Attribute,就像在 Create_Node 中所做的那样,最后,如果节点上存在属性,但 Attributes 标志为 false(当更新的节点没有属性时会发生),则必须删除所有属性

ArgNode->Attributes->RemoveAll();

还有一种情况我没有包含代码——删除单个属性。

添加属性

Add_Attribute 不是一个复杂的函数。它将文档、节点和候选(记住,包括名称和值)作为参数。

它使用属性名称在文档上创建一个属性,然后将值添加到该属性,最后将该属性附加到其节点。这是代码

XmlAttribute ^attr;
attr = ArgDoc->CreateAttribute(Arg_NewAttr->m_Attribute_Name);
attr->Value = Arg_NewAttr->m_Attribute_Value;
ArgNode->Attributes->Append(attr);

使用预处理器指令作为交互式调试的替代方案

如果您已经查看了本文随附的示例代码,您会注意到一些用 # 标签括起来的灰色代码块。这些不是来自 Twitter 的杂散代码,而是来自 C 编程的昔日,那时通常没有交互式调试器。例如,来自 DB_PersonManager.cpp

#ifdef SHOW_DELETE_DISPLAYS
MessageString = "Found Sibling Name = "; 
if (ArgNode->NextSibling != nullptr)
    MessageString +=  ArgNode->NextSibling->Name;
else
    MessageString +=  ArgNode->PreviousSibling->Name;

MessageBox::Show(MessageString);
#endif

DB_PersonManager.h 中有一个注释掉的配套

//#define SHOW_DELETE_DISPLAYS

那么,我为什么还要费心在我的代码中包含这些“恐龙”呢?首先,它们提供了一种快速调试的好方法,当某些东西不起作用时,您需要检查一组“常见嫌疑人”的值。只需删除 #define 前的注释并重新编译。#ifdef 代码 then becomes black, and when you recompile, these values display.它们也是在没有 IDE 的机器上进行代码调试的好方法。例如,客户端的机器。只需部署一个 #define 未注释的 .exe,您就会看到显示。这是一种比在需要时键入然后删除显示更安全、更快速的方法。更快是因为只有两个字符需要插入或删除,更安全是因为没有误删一行关键代码的风险,或者忘记删除可能在某个尴尬位置(例如大循环中)的显示,而墨菲定律告诉我们,只有在您的客户的 IT 经理或公司总裁在场时,它才会出现。尝试它们,并考虑它们如何帮助您。

分层方法的好处

分层设计的目的是通过分层结构将构成系统的不同活动分开,这样您就可以更改其中任何一个,而对其他层的影响微乎其微甚至没有。典型的分层系统可能从菜单模块开始。这些模块又调用第二层的模块,其功能是显示窗体并允许用户管理数据和创建报告。这个交互层中的模块又调用输入/输出层中的模块。I/O 层的功能是管理数据的读取和写入。在一个 properly 分层的系统中,例如,应该可以更改 I/O 层模块用于使用 XML 管理数据的方法,而它们以前使用 SQL 管理数据,而无需更改菜单和交互层中的代码。当数据访问调用嵌入在交互模块中时,它们变得更难找到,并且通常与交互模块的逻辑根本性地交织在一起。分层的另一个好处是数据操作语句,例如,获取个人的年龄,可以重复使用。

此示例没有菜单层,但它具有通常可以位于交互层 (BlobXmlGrid2) 和 I/O 层 (DB_PersonManager) 的模块。

比较和对比 XML 数据处理器与 MySQL 数据处理器

在本节中,我们将查看此示例中的 DB_PersonManager 和我正在开发中的票务系统中的 DB_PersonMaster。我将使用它们来说明在 XML 和关系数据库之间交换 I/O 层中的模块时的异同。首先,它们通常不会有不同的名称,这是我在准备此示例时为防止意外编辑我的系统代码而采取的安全预防措施。

这些是常见的函数调用

DB_PersonManager
static List<DB_PersonManager::CPersonMaster^>^ Fetch_PersonMaster(int arg_call_type);
static int Update_PersonMaster(CPersonMaster^ arg_PersonRow, 
           int arg_UpdateList, int arg_Index);
static int Insert_PersonMaster(CPersonMaster^ arg_PersonRow, 
           int arg_InsertFlags, int arg_Index);
DB_PersonMaster
static List<DB_PersonMaster::CPersonMaster^>^ 
           Fetch_PersonMaster(MySqlConnection^ arg_Connection, int arg_call_type);
static int Update_PersonMaster(MySqlConnection^ arg_Connection, 
           CPersonMaster^ arg_PersonRow, int arg_UpdateList);
static int Insert_PersonMaster(MySqlConnection^ arg_Connection, 
           CPersonMaster^ arg_PersonRow, int arg_InsertList);

唯一的区别是我正在传入一个连接字符串,如果我的系统需要可互换的 XML/关系数据库 IO,那么这也会被参数化,并且删除函数将恢复为 SQL 版本。目前,我所有的 IO 模块都共享一个通用的动态删除语句。这些函数的内部结构非常不同,但它们产生相同的最终结果。XML 版本有以下一系列关系数据库版本不需要的附加内部函数

static void Navigate(XmlNode ^node, int depth);
static void Process_Attribute(XmlNode^ ArgNode, 
            XmlAttribute^ ArgAttr, String^ ArgValue);
static void Process_Node(XmlNode ^ArgNode, String^ ArgValue);
static void Add_the_Person();
static XmlElement^ Create_Node(XmlDocument ^ArgDoc, 
                      String ^ArgValue, 
                      LinkedList<String^>^ ArgList /*full path to node*/,
                      List<CAttributeDefinition^>^ ArgAttrList);
static void Delete_Node(XmlNode ^ArgNode);
static void Update_Node(XmlNode ^ArgNode, 
       String^ ArgValue,List<CAttributeDefinition^>^ ArgAttrList);
static void Create_Parent(XmlDocument ^ArgDoc, XmlElement ^ArgChild, 
       LinkedList<String^>^ ArgList /*full path to node*/);
static void Insert_Sibling(XmlDocument ^ArgDoc, XmlNode ^ArgNode, XmlElement ^ArgChild);
static XmlNode^ Find_Candidate(XmlDocument ^ArgDoc, String ^ArgParent, 
       LinkedList<String^>^ ArgList /*preceeding siblings*/);
static String^ ReformatListEntry(String ^ArgListEntry);
static void Add_Attribute(XmlDocument ^ArgDoc, XmlNode ^ArgNode, 
            CAttributeDefinition^ Arg_NewAttr);

逐步分析附带代码

附带的 BlobXmlGrid2 示例是一个完整的 Visual Studio 2008 项目,在压缩之前已清理。首先,再次说明我的风格。我的 .h 文件中除了定义之外,不喜欢包含太多内容,因此当 IDE 将 Windows 窗体函数添加到 .h 文件时,我的做法是在 .cpp 文件中调用用户定义的函数,并传递参数。这是一个开销,因为我无法强制 IDE 将 Windows 窗体函数直接插入到 .cpp 文件中,而且当我将它们移动到那里时,IDE 会感到困惑——但我更喜欢它所提供的顺序。

此示例是一个标准的 Windows 窗体应用程序,其中包含一个 DataGridView 和几个从工具箱中拖放的标签,如第二个屏幕截图所示。“有效期类型”列是一个组合框列,与“单元 ID”和“单元描述”一样,其他三列是文本框列。所有这些都是使用窗体设计器实现的。

在这里,我们将更深入地(从更高层次)查看 Form1.hBlobXmlGrid2.cppDB_PersonManager.hDB_PersonManager.cpp 模块。

Form1.h

这是我引用的所有程序集列表

using namespace System;
using namespace System::ComponentModel;
using namespace System::Collections;
using namespace System::Windows::Forms;
using namespace System::Data;
using namespace System::Drawing;
using namespace System::IO;
using namespace System::Collections::Generic;
using namespace DB_PersonManager;

请注意对 DB_PersonManager 的调用。这对于调用我们的程序集来管理 XML 至关重要。除了调用命名空间,我们还需要包含对程序集的引用。使用 Alt-F7 或菜单选项 Project -> BlobXMLGrid2 Properties 启动属性页。属性页如下所示

Adding an assembly reference

现在点击“添加新引用”,然后转到“浏览”选项卡。找到 DB_PersonManager.DLL 并选择它以包含。

除了 Launchform 函数调用之外,我在构造函数中还有一行代码。这是用于错误图标的

m_CellInError = gcnew Point(-2, -2);

#pragma endregion

我定义了我的内部变量和函数。这里包含了以 [Flags] 为前缀的枚举类。

定义了两个用于错误图标处理的变量,然后是 DataTables、BindingSources 和 DataSets 的定义。

.h 文件的最后一部分包含通过 IDE 中窗体设计器为 DataGridView 以及“提交”、“回滚”和“退出”按钮添加的函数定义。它们是

  • UserAddedRow
  • CellValueChanged
  • RowEnter
  • RowValidating
  • CellBeginEdit – 存储颜色 – 在 .CPP 中没有任何内容
  • CellEndEdit – 恢复颜色,然后调用 .CPP 函数
  • UserDeletingRow
  • btnCommit_Click
  • btnRollback_Click
  • Exit_Click – 关闭示例
  • FormClosing
  • List_Setup

BlobXmlGrid2.cpp

LaunchForm

在此函数中,从构造函数调用,展示了一些有趣的“花里胡哨”。

此代码自定义网格标题

DataGridViewCellStyle^ headerStyle = gcnew DataGridViewCellStyle;
headerStyle->Font = gcnew System::Drawing::Font("Times New Roman", 12,FontStyle::Bold);
gridPerson->ColumnHeadersDefaultCellStyle = headerStyle;

个性化选择颜色

gridPerson->DefaultCellStyle->SelectionBackColor=Color::FromArgb(255,255,128);
gridPerson->DefaultCellStyle->SelectionForeColor=Color::Black;

列标题上的工具提示文本

for each(DataGridViewColumn^ column in gridPerson->Columns)
    column->ToolTipText = L"Click to\nsort rows";

将颜色绑定应用于网格

gridPerson->AlternatingRowsDefaultCellStyle->BackColor = Color::LightGray;

调用 Load_People 来填充网格。随后的函数在很大程度上已经为某些功能在上面访问过了。

Load_People

Load_People 使用 DB_PersonManager 上的 fetch 函数填充网格。

Process_RowEntered

Process_RowEntered 初始化枚举标志并存储当前行的内容。

Process_CellEndEdit

Process_CellEndEdit 仅因其在显示错误图标方面的作用而包含在内。

Process_CellValueChanged

Process_CellValueChanged 在内容更改时更新标签并设置枚举标志值。

Process_RowValidating

Process_RowValidating 是错误处理的一部分。只有当此函数满足条件时,才会调用 Update_Row

更新行

Update_Row 负责 XML 上的插入和更新。

Process_UserDeletingRow

Process_UserDeletingRow 在从网格中删除行时调用。它在 XML 上复制删除操作。

Process_Commit

Process_Commit 使网格更改永久生效。

Process_Rollback

Process_Rollback 将网格恢复到最近提交的状态。

Process_Exit

Process_Exit 用于退出示例,并在退出时检查是否有未保存的更改。

Process_Click

Process_Click 处理网格上的按钮点击。

DB_PersonManager.h

DB_PersonManager 以注释掉的预处理器声明开始。

// DB_PersonManager.h
//#define SHOW_ATTR_TAG_DISPLAYS
//#define SHOW_TAG_DISPLAYS
//#define SHOW_NAVIGATION_VALIDATION_DISPLAYS
//#define SHOW_INSERT_DISPLAYS
//#define SHOW_DELETE_DISPLAYS

然后我们有必需的命名空间

using namespace System;
using namespace System::Windows::Forms;
using namespace System::Collections::Generic;
using namespace System::Drawing;
using namespace System::Xml;
//For the Base64 stuff
using namespace System::Text;

接下来,定义 Person 类本身

public ref class CPersonMaster
{
    // TODO: Add your methods for this class here.
public:
    // Constructor
    CPersonMaster(void)
    {
    }

    CPersonMaster(String^ arg_BlobName,
        Int64 arg_BlobLen,
        array<Byte>  ^arg_Blob,
        String^ arg_Forename,
        String^ arg_Surname,
        int arg_PersonID) :
        m_BlobName(arg_BlobName),
        m_BlobLen(arg_BlobLen),
        m_Blob(arg_Blob),
        m_Forename(arg_Forename),
        m_Surname(arg_Surname),
        m_PersonID(arg_PersonID){}

    CPersonMaster(String^ arg_Forename,
        String^ arg_Surname,
        int arg_PersonID) :
        m_Forename(arg_Forename),
        m_Surname(arg_Surname),
        m_PersonID(arg_PersonID){}

    property String^ p_BlobName
    {
        String^ get()
        {
            return m_BlobName;
        }
        void set(String^ arg_BlobName)
        {
            m_BlobName = arg_BlobName;
        }
    }
    property Int64 p_BlobLen
    {
        Int64 get()
        {
            return m_BlobLen;
        }
        void set(Int64 arg_BlobLen)
        {
            m_BlobLen = arg_BlobLen;
        }
    }
    property array<Byte> ^p_Blob
    {
        array<Byte>^ get()
        {
            return m_Blob;
        }
        void set(array<Byte>^ arg_Blob)
        {
            m_Blob = arg_Blob;
        }
    }
    property String^ p_Surname
    {
        String^ get()
        {
            return m_Surname;
        }
        void set(String^ arg_Surname)
        {
            m_Surname = arg_Surname;
        }
    }
    property String^ p_Forename
    {
        String^ get()
        {
            return m_Forename;
        }
        void set(String^ arg_Forename)
        {
            m_Forename = arg_Forename;
        }
    }
    property int p_PersonID
    {
        int get()
        {
            return m_PersonID;
        }
        void set(int arg_PersonID)
        {
            m_PersonID = arg_PersonID;
        }
    }
                
private:
String^ m_BlobName;
Int64 m_BlobLen;
array<Byte>^ m_Blob;
String^ m_Surname;
String^ m_Forename;
int m_PersonID;
    // TODO: Add your methods for this class here.
};

头文件以通信类的定义结束

public ref class CComs_PM
{    
private:
    static ref class CAttributeDefinition
    // Nested class for attribute names and values
    {
    public:
        CAttributeDefinition(String^ arg_Name, String^ arg_Value)
            :m_Attribute_Name(arg_Name), m_Attribute_Value(arg_Value)
        {}
        String^ m_Attribute_Name;
        String^ m_Attribute_Value;
    };
    static String^ tmpID, ^tmpForename, ^tmpSurname, ^tmpBlobName;
    static array<Byte>^ tmpBlob;
    static Int64 tmpBlobLen;
    static Image^ tmpImage;
    static int Row_Index;
    static XmlDocument ^doc;
    static LinkedList<String^>^ node_list;
    static void Navigate(XmlNode ^node, int depth);
    static void Process_Attribute(XmlNode^ ArgNode, 
                XmlAttribute^ ArgAttr, String^ ArgValue);
    static void Process_Node(XmlNode ^ArgNode, String^ ArgValue);
    static void Add_the_Person();
    static XmlElement^ Create_Node(XmlDocument ^ArgDoc, 
                                  String ^ArgValue, 
                                  LinkedList<String^>^ ArgList /*full path to node*/,
                                  List<CAttributeDefinition^>^ ArgAttrList);
    static void Delete_Node(XmlNode ^ArgNode);
    static void Update_Node(XmlNode ^ArgNode, String^ ArgValue, 
                List<CAttributeDefinition^>^ ArgAttrList);
    static void Create_Parent(XmlDocument ^ArgDoc, XmlElement ^ArgChild, 
                LinkedList<String^>^ ArgList /*full path to node*/);
    static void Insert_Sibling(XmlDocument ^ArgDoc, XmlNode ^ArgNode, XmlElement ^ArgChild);
    static XmlNode^ Find_Candidate(XmlDocument ^ArgDoc, String ^ArgParent, 
                    LinkedList<String^>^ ArgList /*preceeding siblings*/);
    static String^ ReformatListEntry(String ^ArgListEntry);
    static void Add_Attribute(XmlDocument ^ArgDoc, XmlNode ^ArgNode, 
                CAttributeDefinition^ Arg_NewAttr);

    static List<CAttributeDefinition^>^ AttributeList;

public:

    static void SaveDocument();
    static List<DB_PersonManager::CPersonMaster^>^ PersonList;

    static String^ m_Filename;
    [Flags] enum class m_FlagBits
    { 
        PERSONID = 1, 
        SURNAME = 2, 
        FORENAME = 4,
        IMAGE = 8,
    };

    static bool m_HasAttributes;

    static List<DB_PersonManager::CPersonMaster^>^ Fetch_PersonMaster(int arg_call_type);
    static void Delete_PersonMaster(int arg_Index);
    static int Update_PersonMaster(CPersonMaster^ arg_PersonRow, 
           int arg_UpdateList, int arg_Index);
    static int Insert_PersonMaster(CPersonMaster^ arg_PersonRow, 
               int arg_InsertFlags, int arg_Index);
    };
}

DB_PersonManager.cpp

首先我们有两个 include 语句

#include "stdafx.h"
#include "DB_PersonManager.h"

接下来是函数集

Fetch_PersonMaster

Fetch_PersonMaster 获取所有 XML 记录。

导航

Navigate 控制检索数据时 XML 的遍历方式。

Insert_PersonMaster

调用 Insert_PersonMaster 来管理向 XML 添加数据。

Update_PersonMaster

调用 Update_PersonMaster 来管理已存在于 XML 上的数据更新。

Delete_PersonMaster

Delete_PersonMaster 负责从 XML 中删除行。

Process_Attribute

Process_Attribute 从 XML 笔记中提取属性数据。

Process_Node

Process_Node 检查每个节点的数据内容。

Add_the_Person

Add_the_Person 将完全提取的个人行添加到列表中,以便返回给调用模块。

创建节点

Create_Node 在向 XML 添加数据时创建新节点。

创建父级

Create_Parent 在新节点被发现没有父节点时为其创建父节点。

插入兄弟节点

Insert_Sibling 决定新节点在兄弟节点列表中的位置。

查找候选人

Find_Candidate 搜索 Insert_Sibling 构建的列表,以确定新节点的精确位置。

ReformatListEntry

ReformatListEntry 从节点列表条目中删除元素编号(如果存在)。

SaveDocument

SaveDocument 保存 XML 文档。

更新节点

Update_Node 更新节点的值。

删除节点

Delete_Node 删除节点。

添加属性

Add_Attribute 向节点添加属性。

历史

  • 2011-08-16: V1.0 - 首次提交。
© . All rights reserved.