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

用于 Synapse 控制关系的对象树

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.39/5 (13投票s)

2008 年 2 月 11 日

CPOL

8分钟阅读

viewsIcon

30912

downloadIcon

333

构建一棵树并允许树的每个分支与任何对象关联的概念。

results

引言

我最近一直在关注许多新推出的多点触控概念屏幕,例如 Microsoft Surface。 在这种类型的界面中,您的屏幕上可能会有很多不同类型的对象,例如一些图片、一个视频、网站快捷方式以及您随意写下的包含信息的文档。那么,我现在面临的问题是,如果像这样的产品有一天能超越新奇的范畴,它将需要一种好的方法来存储用户的信息,以便能够清除并稍后重新调用。目前传统的做法是在硬盘上存储一系列文件夹和文件,但在我看来,这种设置存在缺陷。当可视化屏幕上的对象时,图片显示为图片,视频显示为视频,等等。您必须做出一个巨大的范式转变,才能理解您看到的图片对象实际上是由零和一组成的文件,并且应该这样组织。我开始思考这样一个事实:我们以文件的形式存储图片等信息,并将其显示为照片,因为我们的大脑理解渲染后的照片,而不是文件。那么,为什么我们还要强迫用户在分类或组织照片时将其视为文件呢?

我提出了一个构建树的概念,并允许树的每个分支与任何对象关联。用户可以通过将一个项目拖放到另一个项目上来构建树,或者操作系统可以根据用户所做的某些事情来构建树。例如,如果用户将他的摄像手机拖放到屏幕上,然后从相机中取出一些照片,然后将照片从屏幕上移走,拿起他的相机走开,操作系统就可以创建一个树,其中他的相机是一个对象,所有照片都是子对象。那么,下次当他将他的摄像手机放在屏幕上时,操作系统就可以回忆起与该相机相关的所有对象,并建议他查看它们。更令人兴奋的概念是第二部分,允许用户通过将对象拖放到彼此上来将它们关联起来。

这使得用户可以更像我们的大脑组织记忆那样存储他们的对象。例如,也许照片中的某个人总是让您想起第一次遇到那个人时听过的某首歌曲,也许您有一份包含那首歌歌词的文档,也许您还有一份是同一位送您歌词的人发送给您的视频,而这个人也有一个个人网站。在过去,关系在很大程度上是由某些任意规则决定的;因为这个原因,这个可以关联到那个。我的概念是打破计算机理解事物为何相关的需求;它们之所以相关,仅仅是因为用户说它们相关。在我上面的例子中,如果用户将音乐对象拖到照片上,歌词拖到歌曲上,视频拖到歌词上,歌词拖到网站上,那么下次任何一个对象被调用时,操作系统都可以提醒您与其相关的对象。所以,例如,如果用户打开了他朋友的照片,他可能会被提醒起他也拥有的那首歌,或者如果他打开了视频,他可能会被提醒起他朋友的网站。

项目

为了完成这种存储信息方法的概念验证,我创建了一个演示项目,其中包括一个通用的对象树类、一组任意对象、一个用于创建和关联对象的 Web UI,以及一个用于存储和检索通用对象树(完全按照显示的方式)的序列化类。

该项目是用 VB.NET 在 Visual Studio 2008 中编写的,需要 .NET 3.5 框架。请注意,如果您正在寻找一个用于构建递归导航或家谱的库,我建议您搜索“generic tree”,尽管从演示中看这可能正是您需要的;但这个演示是关于在树中存储不相关对象的示例,而不是在树中存储所有相同类型的对象。

为了使此树正常工作,我知道我需要继承自一个泛型集合对象,并且我也知道如果我们想要从树中检索特定项,我将遇到一个信号量问题。我选择继承自泛型 Dictionary 对象,它不仅为我提供了将对象存储在集合中的能力,还为我提供了为每个对象分配唯一键的能力,以及能够非常快速有效地通过键查找对象的能力。

''' <summary>
''' GenericObjectTree
''' </summary>
''' <remarks>
''' Generic Object Tree
''' Used to relate objects of any type to eachother in a tree object.
''' </remarks>
<Serializable()> _
Public Class GenericObjectTree
    Inherits Generic.Dictionary(Of String, GenericObjectTree)
    Implements ISerializable

    ''' <summary>
    ''' Data
    ''' </summary>
    ''' <remarks>
    ''' The actual data store of an object of your choice
    ''' </remarks>
    Private m_Data As Object
    Public Property Data() As Object
        Get
            Return m_Data
        End Get
        Set(ByVal value As Object)
            m_Data = value
        End Set
    End Property

    ''' <summary>
    ''' New
    ''' </summary>
    ''' <remarks>
    ''' Default Constructor.
    ''' </remarks>
    Public Sub New()
        MyBase.New()
    End Sub

    ''' <summary>
    ''' New
    ''' </summary>
    ''' <param name="treeData"></param>
    ''' <remarks>
    ''' Create Guid As Unique Node Identifier.
    ''' Assign First Key/Value Pair of current collection to Data Object.
    ''' </remarks>
    Public Sub New(ByVal treeData As Object)
        MyBase.New()
        Dim newID As String = Guid.NewGuid().ToString()
        Me.Add(newID, New GenericObjectTree())
        Me(newID).Data = treeData
    End Sub

    ''' <summary>
    ''' New
    ''' </summary>
    ''' <param name="childData"></param>
    ''' <param name="skipemptyroot"></param>
    ''' <remarks>
    ''' Private Constructor, called from AddChild
    ''' </remarks>
    Private Sub New(ByVal childData As Object, ByVal skipemptyroot As Boolean)
        MyBase.New()
        Me.Data = childData
    End Sub

    ''' <summary>
    ''' AddChild
    ''' </summary>
    ''' <param name="childData"></param>
    ''' <returns>Unique ID</returns>
    ''' <remarks>
    ''' Create Guid As Unique Node Identifier.
    ''' Add Child Key/Value Pair with Data Onject.
    ''' </remarks>
    Public Overridable Function AddChild(ByVal childData As Object) As String
        Dim newID As String = Guid.NewGuid().ToString()
        Me.Add(newID, New GenericObjectTree(childData, True))
        Return newID
    End Function

End Class

现在,您首先注意到的是该对象继承自一个自身的 Generic.Dictionary。 我意识到一个对象继承自身在逻辑上是完全不合乎情理的,但是一个对象继承自一个它自身的集合有意义吗?当然,为什么不呢,继承声明该对象继承了泛型字典和它自身的所有属性,同时允许您创建同类型的子对象。您会看到每个树节点都有一个类型为 object 的附加属性,用于存储任意对象。

另外要注意的一点是私有/公共构造函数的存在。这是因为一个简单的鸡生蛋还是蛋生鸡的问题;要访问每个节点中存储的数据,您需要一个键;由于对象的初始实例不是任何其他字典的子对象,因此无法为其分配键。为了解决这个问题,我们总是需要创建一个不存储任何数据但只引用其子对象的初始实例。这个操作在公共构造函数中完成。一旦创建了树,就不再需要这个空节点,并且私有构造函数通过 AddChild 调用。

递归树

如果您对递归有很好的理解,那么递归树相当简单。它只是对当前树节点执行操作,检查该树节点是否有子节点,如果有,则对子节点执行相同操作,然后检查该节点是否有兄弟节点,并对每个兄弟节点执行相同操作。这会一直持续下去,直到您遍历了整个树,或者直到您找到了您正在寻找的东西。以下示例是项目中包含的递归方法之一

''' <summary>
''' FindNodeByUniqueIdentifier
''' </summary>
''' <param name="uniqueID">Unique ID</param>
''' <returns>GenericObjectTree</returns>
''' <remarks>
''' Return Your Tree Node By It's Assigned Unique Identifier.
''' This Method Will Recurse Itself.
''' </remarks>
Public Overridable Function FindNodeByUniqueIdentifier(ByVal _
                   uniqueID As String) As GenericObjectTree

    'check if key is a child currently.
    If Me.ContainsKey(uniqueID) Then
        'key found return tree node
        Return Me(uniqueID)
    End If

    'loop through collection to recurse branches
    For Each key As String In Me.Keys
        'recurse grandchild.
        Dim childtree As GenericObjectTree = _
            Me(key).FindNodeByUniqueIdentifier(uniqueID)
        'if found return tree node.
        If childtree IsNot Nothing Then Return childtree
    Next

    'if not found return nothing.
        Return Nothing
End Function

有一点我必须提到,可以争辩说上面的函数不是真正的递归,尽管它是递归性质的。如果您想调试代码并理解正在发生的事情,这一点很重要。当调用上述方法时,它确实调用了一个同名函数,并且遵循了标准递归函数的模式。但是,它实际调用的方法是它自身子实例中同名的方法,所以可以争辩说它并没有真正地调用它自己。

显示树数据

显示树数据只是递归地遍历树,决定当前树节点中存储的数据类型,并显示该特定对象的正确可视化表示。

Protected Sub DisplayCurrentFileTree(ByVal FileBranch As GenericObjectTree, _
                                     ByVal UniqueID As String, _
                                     Optional ByVal removeOther As Boolean = True)
...

    'Check type of data on this tree node 
    'and display appropriate box type.
    Select Case True
        Case TypeOf (FileBranch.Data) Is Files.MusicFile
            'Display a music box
            DisplayMusicBox(DirectCast(FileBranch.Data, _
                                       Files.MusicFile), UniqueID)

        Case TypeOf (FileBranch.Data) Is Files.Picture
            'display a picture box
            DisplayPictureBox(DirectCast(FileBranch.Data, _
                                         Files.Picture), UniqueID)

        Case TypeOf (FileBranch.Data) Is Files.Favorite
            'display a favortie box
            DisplayFavoriteBox(DirectCast(FileBranch.Data, _
                               Files.Favorite), UniqueID)

        Case TypeOf (FileBranch.Data) Is Files.Video
            'display a video box
            DisplayVideoBox(DirectCast(FileBranch.Data, _
                                       Files.Video), UniqueID)

        Case TypeOf (FileBranch.Data) Is Files.WordDocument
            'display a word document box
            DisplayWordDocumentBox(DirectCast(FileBranch.Data, _
                                   Files.WordDocument), UniqueID)

        Case Else
            'do nothing
    End Select

...

    'If sibling keys exists then loop through each key.
        For Each key As String In FileBranch.Keys

...

    'Recurse each found key.
    'Specify false do tree display is not truncated.
    DisplayCurrentFileTree(FileBranch(key), key, False)

...

Next

存储树

在这个系统中,我的目标是按照它显示的方式来存储对象,所以我选择将每个树序列化为二进制,并将每个对象也序列化为二进制。对象被存储和组织的方式与它们的显示方式完全相同。不需要将对象拆解和重构成扁平表。由于 Generic.Dictionary 默认无法正确序列化,我不得不首先实现 ISerializable,并在我的泛型字典对象中添加一个自定义构造函数和方法。存储 GenericObject 树时,我必须首先根据其 myBase 方法序列化对象。由于基方法没有任意对象数据的概念,并且每个对象都会以不同的方式序列化,因此我需要递归遍历树,并将每个对象存储为其自己的文件,以其键命名。检索对象时,首先反序列化树,然后递归加载每个任意对象。这个的好处是,每个任意对象都被存储为独立的,并且可以存在于许多树中。

''' <summary>
''' SaveObject
''' </summary>
''' <param name="obj"></param>
''' <param name="Filename"></param>
''' <param name="skipTree"></param>
''' <remarks>
''' Save a GenericObjectTree To Disk
''' </remarks>
Public Shared Sub SaveObject(ByVal obj As GenericObjectTree, _
              ByVal Filename As String, _
              Optional ByVal skipTree As Boolean = False)

    If Not skipTree Then
        'open a file strem
        Using s As Stream = File.Open(Filename, FileMode.Create, _
                                      FileAccess.ReadWrite)
            'serialize dictionary to disk in binary
            Dim b As New BinaryFormatter
            b.Serialize(s, obj)
        End Using
    End If

    'serialize all data objects to disk
    For Each key As String In obj.Keys

        Dim dataObj As Object = obj(key).Data

        If dataObj IsNot Nothing Then
            'write data object to disk, use unique key for filename.
            Using s As Stream = File.Open(Path.GetDirectoryName(Filename) &_
                                "\" & key & ".sf", _
                                FileMode.Create, FileAccess.ReadWrite)
                'serialize data to disk in binary
                Dim b As New BinaryFormatter
                b.Serialize(s, dataObj)
            End Using
        End If

        'recurse.
        SaveObject(obj(key), Filename, True)
    Next

End Sub

结论

尽管 Web UI 界面与多点触控显示屏相去甚远,但我相信它有效地证明了将任意对象彼此关联的可能性和有效性。正如您在示例中看到的,在构建、加载和保存非常大的树时,渲染效率非常高。

在下一版项目中,我希望实现一个界面,在该界面中,对象将在树本身之外创建,并存储在一个表面上,直到通过拖放关联。此外,我希望实现树内对象的移动、与树对象的解除关联、编辑对象数据以及删除对象。

© . All rights reserved.