假设您正在创建 Windows 窗体应用程序,并且已将 DataGridView 控件绑定到标准 List(Of Customer) 数据结构。您希望能够使网格中的项目与基础数据源中的值保持同步。也就是说,如果其他代码或其他窗体更改了 List 中用户的数据,您希望网格随之更新并显示修改的数据。
通常情况下,使用 Windows 窗体可以实现此目的。您可以进行更新,但这种方法很受限制。例如,在正常情况下,您可以立即在网格中看到更新,但是如果有人向数据源中添加新行,则要向网格中添加新行可就没那么容易了。Windows Presentation Foundation (WPF) 在 Microsoft .NET Framework 中添加了一些功能,所以您实际上可以可靠地使绑定控件与其数据源保持一致。我将在本文中演示如何使用 WPF 提供的 ObservableCollection 类。
利用 ObservableCollection 类,WPF 应用程序可以使绑定控件与基础数据源保持同步,但它还提供了更有用的信息,尤其是 ObservableCollection 类还可以在您添加、删除、移动、刷新或替换集合中的项目时引发 CollectionChanged 事件。此功能还可以在您的窗口以外的代码修改基础数据时做出反应。在本月的示例应用程序中,您将了解到如何使用此信息,这正是接下来我要介绍的内容。
ObservableCollection 类简介
System.Collections.ObjectModel.ObservableCollection(Of T) 类从 Collection(Of T)(泛型集合的基类)继承而来,可实现 INotifyCollectionChanged 和 INotifyPropertyChanged 两种接口。INotifyCollectionChanged 接口增加了集合的趣味性,同时也是允许绑定对象(和代码)确定集合是否已发生更改的接口。
值得注意的是,虽然 ObservableCollection 类会广播有关对其元素所做的更改的信息,但它并不了解也不关心对其元素的属性所做的更改。也就是说,它并不关注有关其集合中项目的属性更改通知。
如果您需要了解是否有人更改了集合中某个项目的属性,则您将需要确保集合中的项目可以实现 INotifyPropertyChanged 接口,并需要手动附加这些对象的属性更改事件处理程序。无论您如何更改此集合中的对象属性,都不会触发该集合的 PropertyChanged 事件。事实上,ObservableCollection 的 PropertyChanged 事件处理程序已受到保护 — 除非您从此类中继承并亲自将其公开,否则您甚至无法对其做出反应。在示例应用程序中,我采用的方法比较简单,让客户端应用程序处理单个项目的更改事件,当然,您也可以在继承的集合中处理该集合内每个项目的 PropertyChanged 事件。
如果您忽略了继承的受保护成员(假设您已经熟悉从其中派生 ObservableCollection 类的所有成员的 Collection 基类),则剩下的有趣成员仅有 Move 方法(允许您将某个成员移动到集合中的新位置)和 CollectionChanged 事件(广播有关对集合内容所做的更改的信息)。继续阅读之前,您可能需要下载并安装演示这些功能的示例 WPF 应用程序。
查看示例
示例解决方案 ObservableCollectionTest 包含从 ObservableCollection 继承而来的 CustomerList 类(请参见图 1)。如您所料,CustomerList 类会公开包含 Customer 对象的 ObservableCollection 实例。但是,如果您检查一下代码,便会发现该类仅公开一个列表,所以该类的多个使用者分别检索对同一集合的引用。(这是此次特定演示的关键,但对其他应用程序来说不是必要的。)此类提供了一个私有构造函数,因此,检索此类实例的唯一方法是调用共享的 GetList 方法,该方法用于分发现有集合实例:
Private Shared list As New CustomerListPublic Shared Function GetList() As CustomerListReturn list
End Function

图 1 CustomerList
System.Collections.ObjectModel
Imports System.ComponentModelPublic Class CustomerListInherits ObservableCollection(Of Customer)Private Shared list As New CustomerListPublic Shared Function GetList() As CustomerListReturn listEnd FunctionPrivate Sub New()' Make the constructor private, enforcing the "factory" concept ' the only way to create an instance of this class is by calling' the GetList method.AddItems()End SubPublic Shared Sub Reset()list.ClearItems()list.AddItems()End SubPrivate Sub AddItems()Add(New Customer("Maria Anders"))Add(New Customer("Ana Trujillo"))Add(New Customer("Antonio Moreno"))End Sub
End Class

私有构造函数调用 AddItems 方法;公开共享的 Reset 方法清除列表,然后调用 AddItems 方法。无论采用哪种方法,结果都是显示集合中的三个使用方:
Private Sub AddItems()Add(New Customer("Maria Anders"))Add(New Customer("Ana Trujillo"))Add(New Customer("Antonio Moreno"))
End Sub

在本示例中,Customer 类特别简单(简化到仅够演示必要的功能)。图 2 中显示的类仅包含 Name 属性,要不是该类可以实现 INotifyPropertyChanged 接口,以便属性值发生更改时会通知该类实例(包括数据绑定控件)的使用者,它根本不值得一提。
图 2 具有 PropertyChanged 事件的 Customer 类
Imports System.ComponentModelPublic Class CustomerImplements INotifyPropertyChangedPublic Event PropertyChanged( _ByVal sender As Object, _ByVal e As PropertyChangedEventArgs) _Implements INotifyPropertyChanged.PropertyChangedProtected Overridable Sub OnPropertyChanged( _ByVal PropertyName As String)' Raise the event, and make this procedure' overridable, should someone want to inherit from' this class and override this behavior:RaiseEvent PropertyChanged( _Me, New PropertyChangedEventArgs(PropertyName))End SubPublic Sub New(ByVal Name As String)' Set the backing field so that you don't raise the ' PropertyChanged event when you first create the Customer._name = NameEnd SubPrivate _name As StringPublic Property Name() As StringGetReturn _nameEnd GetSet(ByVal value As String)If _name <> value Then_name = valueOnPropertyChanged("Name")End IfEnd SetEnd Property
End Class

某个类实现 INotifyPropertyChanged 接口时,它必须提供 PropertyChanged 事件:
Public Event PropertyChanged( _ByVal sender As Object, _ByVal e As PropertyChangedEventArgs) _Implements INotifyPropertyChanged.PropertyChanged

为了引发使用标准 .NET 设计模式的事件,Customer 类包含受保护且可覆盖的 OnPropertyChanged 过程,该过程引发以下事件:
Protected Overridable Sub OnPropertyChanged( _ByVal PropertyName As String)' Raise the event, and make this procedure' overridable, should someone want to inherit from' this class and override this behavior:RaiseEvent PropertyChanged( _Me, New PropertyChangedEventArgs(PropertyName))
End Sub

然后,在 Name 属性的定义范围内,属性 setter 会在新值与属性的当前值不同时调用 OnPropertyChanged 方法:
   Private _name As StringPublic Property Name() As StringGetReturn _nameEnd GetSet(ByVal value As String)If _name <> value Then_name = valueOnPropertyChanged("Name")End IfEnd SetEnd Property

如果该类引发了 PropertyChanged 事件,则使用该类或该类的实例集合的代码会对 PropertyChanged 事件做出反应,然后基于属性更改采取相应的措施。(请注意,PropertyChangedEventArgs 类仅将 PropertyName 属性添加到标准事件参数,并不提供有关该属性的旧值或新值的任何信息。稍后您会看到,示例应用程序突破了这一限制,至少可以确定已更改属性的新值。)
此示例还包含一个名为 MainWindow 的 WPF 窗口,如图 3 所示。此窗口标记中唯一重要的细节在于 ListBox 控件的定义,其中包括该控件的 ItemsSource 属性的声明式数据绑定。绑定指示该控件应该从 MainWindow 类的 Data 属性中获取它的数据,并且应该显示 Data 属性中每个项目的 Name 属性:
<ListBox DisplayMemberPath="Name"ItemsSource="{Binding ElementName=MainWindow, Path=Data}" Grid.Column="3" Grid.RowSpan="3" Name="ItemListBox" Margin="5" />

图 3 WPF 窗口示例(单击图像可查看大图)

MainWindow 的代码隐藏类包括以下声明:
Public WithEvents Data As CustomerList = CustomerList.GetList()

此代码在窗口中公开 CustomerList 实例的内容,如图 3 所示。
查看 codebehind 类中代码的其余部分之前,您应该在此处先停下来,体验一下应用程序。因为窗口中的 ListBox 已绑定到从 ObservableCollection 继承的类,所以您希望列表框始终显示最新的集合内容,而演示窗口证实了这一点。
此外,本示例显示两个单独的主窗口实例,因为两个窗口上的 ListBox 控件都已绑定到同一 ObservableCollection 实例,所以在其中一个窗口中所做的更改会同时显示在两个窗口中。为了打开窗口中的两个实例,Application.xaml 文件包含了以下标记,指出应用程序应该首先运行 Application_Startup 过程中的代码:
<Application x:Class="Application"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Startup="Application_Startup"><Application.Resources></Application.Resources>
</Application>

Application.xaml.vb 代码隐藏文件包含以下启动代码,用于创建两个 MainWindow.xaml 实例,每个实例都有自己的标题:
Private Sub Application_Startup( _ByVal sender As System.Object, _ByVal e As System.Windows.StartupEventArgs)Dim window As New MainWindowwindow.Title = "Observable Collection 1"window.Show()window = New MainWindowwindow.Title = "Observable Collection 2"window.Show()
End Sub

按照下列步骤测验示例应用程序。
  1. 在 Visual Studio 2008 中,加载并运行示例应用程序。您会在同一窗口中看到两个实例。
  2. 单击以打开窗口左侧的组合框。请注意,控件包含 0、1 和 2 这三个数字,分别对应当前的三个用户。选择 1,会选中 Ana Trujillo 并将她的名字复制到文本框。
  3. 在一个窗口中,单击“删除”。Ana Trujillo 会从两个窗口中消失,因为两个 ListBox 控件都已绑定到同一 ObservableCollection 实例,而绑定使得更新会立即显示出来。再次打开组合框,请注意,现在仅会显示两个用户。在每个窗口中都尝试一下此操作,验证两个实例是否都是最新的。
  4. 再两次单击“删除”,删除所有用户。单击“重置数据”重新填充两个窗口中的列表。
  5. 在“添加新项”按钮旁边的文本框中,输入您自己的名字,然后单击“添加新项”。新名字即会显示在两个 ListBox 控件中。单击以打开组合框,并验证组合框现在是否包含 0 到 3 四个数字(每个数字分别对应一个用户)。验证两个窗口中的组合框都已更改,很明显,两个窗口的类都收到了指示集合已更改的事件。
  6. 在一个窗口的 ListBox 中,选择一个名字。在左侧较低的文本框中,修改名字并单击“更改”。首先,会出现一则警报,指示您已更改了属性,将其关闭后,您会立即看到两个窗口中的名字都已更改(请参见图 4)。

图 4 捕获集合中的数据更改事件(单击图像可查看大图)

除了在您更改用户名称和 ComboBox 控件(其各个项目都根据集合中的用户数量进行相应的更改)时出现的警报之外,示例窗口中的所有代码都与此窗口的用户界面有关。换言之,为保持 ListBox 控件与 ObservableCollection 实例同步而执行的所有操作都可“自主”进行,并且由 WPF 管理。
当您添加新项目时,ListBox 将自动显示完整列表。当您更改项目时,ListBox 将自动显示修改后的列表。当您删除项目时,ListBox 将与基础集合保持完全一致。换言之,对于将 ObservableCollection 类与 WPF 中的控件绑定这一任务来说,您可以很轻松地说“一切运行正常”。
实际上,这种情况背后确实有一些“魔法”。将 ListBox 与 ObservableCollection 绑定后,实际上,WPF 实际上会创建一个 CollectionView 实例,以显示处理分组、排序和筛选等操作的数据的视图。您可以在 ListBox 控件中看到集合的默认视图。您可以根据同一集合创建多个 CollectionView 实例,在其中一个 ListBox 控件中以不同的方式(如排序)显示数据。虽然这个问题超出了我们的讨论范围,但是,如果您需要以多种视图显示同一集合,研究一下 CollectionView 类还是有必要的。有关详细信息,请参阅 MSDN 上的 CollectionView 类信息。
查看代码
由于 MainWindow 类定义 CustomerList 实例时使用的是 WithEvents 关键字,因此代码可以处理 ObservableCollection 列表的事件,而无需用于手动添加处理程序的代码:
Public WithEvents Data As CustomerList = CustomerList.GetList()

在代码的“更改事件处理程序”区域中,您将看到 CollectionChanged 事件处理程序,它可以验证您在集合中是添加了还是删除了项目。如果确实执行了这些操作,代码会设置组合框的数据源,并在窗口中启用相应的按钮,如图 5 所示。
图 5 检查更改的集合
Private Sub Data_CollectionChanged( _ByVal sender As Object, _ByVal e As NotifyCollectionChangedEventArgs) _Handles Data.CollectionChanged' Because the collection raises this event, you can modify your user ' interface on any window that displays controls bound to the data. On ' both windows, if you add or remove an item, all the controls update ' to indicate the new collection!' Did you add or remove an item in the collection?If e.Action = NotifyCollectionChangedAction.Add Or _e.Action = NotifyCollectionChangedAction.Remove Then' Set the list of integers in the combo box:SetComboDataSource()' Enable buttons as necessary:EnableButtons()End If
End Sub

这段简单代码的重点在于 NotifyCollectionChangedEventArgs 参数。此参数提供集合中发生更改的内容的相关信息。此外,还提供了五个值得注意的属性,如图 6 所示。
图 6 NotifyCollectionChangedEventArgs 参数
参数 说明
Action 检索引发事件的操作的相关信息。此属性包含 NotifyCollectionChangedAction 值,该值可以是 Add、Remove、Replace、Move 或 Reset。
NewItems 检索更改集合时引入的新项目的列表。
NewStartingIndex 检索发生更改的集合的索引。
OldItems 检索受“替换”、“删除”或“移动”操作影响的旧项目列表。
OldStartingIndex 检索执行了“替换”、“删除”或“移动”操作的集合的索引。

获得所有这些信息之后,您的事件处理程序便可以准确确定集合中执行的哪些操作触发了该事件。如果集合的大小发生更改,示例代码将仅使用 Action 属性更新 ComboBox 控件中的整数列表:
If e.Action = NotifyCollectionChangedAction.Add Or _e.Action = NotifyCollectionChangedAction.Remove Then

虽然这与 ObservableCollection 类的讨论无关,但是,了解代码如何填充 ComboBox 控件的索引列表也很有趣:
Private Sub SetComboDataSource()' Set the list of integers shown in the ' combo box:ItemComboBox.ItemsSource = _Enumerable.Range(0, Data.Count)
End Sub

此代码不是通过执行某种循环来生成包含 0 至集合中编号最高的索引之间的整数的列表,而是直接调用 Enumerable.Range 方法来检索从 0 开始且包含 Data.Count 值的整数集合。只要代码将 ComboBox 控件的 ItemsSource 属性设置为返回的集合即可 — 就是这么简单!(如果想了解有关 Enumerable 类的详细信息,请阅读我编写的前两期“高级基础知识”专栏:LINQ Enumerable 类,第 1 部分和 LINQ Enumerable 类,第 2 部分。)
为了在您更改集合中某个项目的属性后通知示例应用程序,您必须再编写一些代码。每次某些代码更改此类中的每个属性值时,都会引发 PropertyChanged 事件。(当然,这要由具体类的作者来确定更改属性时会引发 PropertyChanged 事件,因为该事件不会自动发生。如您所知,Customer 类的 Name 属性可以实现此目的。)
在 MainWindow 类中,您可以看到 HookupChangeEventHandler 过程,该过程可挂接单独 Customer 对象的 PropertyChanged 事件:
Private Sub HookupChangeEventHandler(ByVal cust As Customer)' Add a PropertyChanged event handler for ' the specified Customer instance:AddHandler cust.PropertyChanged, _AddressOf HandlePropertyChanged
End Sub

HookupChangeEventHandlers 过程可挂接用户的 ObservableCollection 类中每个 Customer 对象的事件处理程序,如下所示:
Private Sub HookupChangeEventHandlers()For Each cust As Customer In DataHookupChangeEventHandler(cust)Next
End Sub

当窗口加载或您单击“重置”按钮时,代码将调用 HookupChangeEventHandlers 过程。如果单击“删除”,会同时从窗口中删除集合中的项目和事件处理程序:
' From DeleteItemButton_ClickDim index As Integer = ItemComboBox.SelectedIndex
If index >= 0 ThenRemoveHandler Data.Item(index).PropertyChanged, _AddressOf HandlePropertyChangedData.RemoveAt(index)

如果单击“添加新项”,代码会创建新的用户,并挂接其 PropertyChanged 事件:
' From NewItemButton_Clickcust = New Customer(NewItemTextBox.Text)
HookupChangeEventHandler(cust)
Data.Add(cust)

当然,由于窗口将 ListBox 控件绑定到 ObservableCollection 实例,因此,所有这些更改会自动显示在窗口的两个实例中,而无需借助任何代码支持。实际上,只有在以编程方式在列表中添加或删除用户,然后挂接并响应单个用户中发生的更改时才需要使用代码支持。
如果您确实更改了 Customer 类中某个属性的值,客户端应用程序会通过 PropertyChanged 事件处理程序接收通知。请注意,HandlePropertyChanged 过程(如图 7 所示)包含应用程序中最复杂的代码。由于更改通知只提供更改属性的名称,因此请务必记住,需要依靠代码来检索此属性的当前值(如果您需要此值)。
图 7 使用 Reflection 的 HandlePropertyChanged
Private Sub HandlePropertyChanged( _
ByVal sender As Object, _
ByVal e As PropertyChangedEventArgs)' In this particular application, you only want to bother with this ' code for the first window, although both will run the code. In this ' case, if the event was raised by the window whose title is ' "Observable Collection 1" then process the event:If Me.Title.EndsWith("1") ThenDim propName As String = e.PropertyNameDim myCustomer As Customer = CType(sender, Customer)' Unfortunately, no one hands you the old property value, or the new ' property value. You can use Reflection to retrieve the new property ' value, given the object that raised the event and the name of the ' property:Dim propInfo As System.Reflection.PropertyInfo = _GetType(Customer).GetProperty(propName)Dim value As Object = _propInfo.GetValue(myCustomer, Nothing)MessageBox.Show(String.Format( _"You changed the property '{0}' to '{1}'", _propName, value))End If
End Sub

此过程首先确保代码只运行一次 — 因为您打开了窗口的两个实例,否则代码会分别针对每个实例运行一次,但没有必要显示两次警报。此代码仅检查标题的最后一个字符(假设您没有更改窗口的 Title 属性),并限制仅在一个窗口中进行操作:
If Me.Title.EndsWith("1") Then'Code removed here…
End If

此代码检索并存储发生更改的属性的名称以及对引发事件的对象(即当前用户)的引用:
Dim propName As String = e.PropertyName
Dim myCustomer As Customer = CType(sender, Customer)

然后,获得属性的名称和类型之后,代码将使用 Reflection 检索 System.Reflection.PropertyInfo 实例:
Dim propInfo As System.Reflection.PropertyInfo = _GetType(Customer).GetProperty(propName)

获得 PropertyInfo 对象和特定的 Customer 实例之后,代码随后就会检索属性的当前值:
Dim value As Object = _propInfo.GetValue(myCustomer, Nothing)

应用程序中的其余代码维护用户界面,包括启用/禁用按钮以及使组合框和列表框保持同步等等。
虽然此应用程序利用了 ObservableCollection 类提供的绑定支持,并响应 CollectionChanged 事件来更新用户界面,但是您不必按照这种方法使用此类。因为它会在其内容发生更改时通知侦听程序,所以您可以替换与 ObservableCollection 实例一起使用的任何 List 或 Collection 实例(即使您创建的不是 WPF 应用程序),然后挂接事件处理程序以通知客户端,集合的内容已发生更改。
正如示例窗口在集合大小发生更改时更新与集合索引对应的整数列表一样,您可以使用任一必要的方法来响应客户端类的集合中发生的更改。但请记住,集合本身不会告诉您其子元素的属性是否发生了更改。您必须挂接客户端中的事件处理程序,以便客户端在集合中的子元素的属性发生更改时收到通知。
另请记住,您在示例应用程序中看到的丰富数据绑定支持仅适用于 WPF 应用程序。如果您创建的是 Windows 窗体应用程序,那么当集合发生更改时,您仍然需要手动刷新绑定到 ObservableCollection 实例的所有控件的绑定。另一方面,由于您会在集合发生更改时收到通知,因此现在至少可以实现此操作。

转载于:https://www.cnblogs.com/llkey/archive/2013/05/30/3108608.html

ObservableCollection 类 详解相关推荐

  1. OpenCV Mat类详解和用法(官网原文)

    参考文章:OpenCV Mat类详解和用法 我马克一下,日后更 官网原文链接:https://docs.opencv.org/3.2.0/d6/d6d/tutorial_mat_the_basic_i ...

  2. 转载:c+string类详解

    C++ string 类详解 </h1><div class="clear"></div><div class="postBod ...

  3. JDBC学习笔记02【ResultSet类详解、JDBC登录案例练习、PreparedStatement类详解】

    黑马程序员-JDBC文档(腾讯微云)JDBC笔记.pdf:https://share.weiyun.com/Kxy7LmRm JDBC学习笔记01[JDBC快速入门.JDBC各个类详解.JDBC之CR ...

  4. JDBC学习笔记01【JDBC快速入门、JDBC各个类详解、JDBC之CRUD练习】

    黑马程序员-JDBC文档(腾讯微云)JDBC笔记.pdf:https://share.weiyun.com/Kxy7LmRm JDBC学习笔记01[JDBC快速入门.JDBC各个类详解.JDBC之CR ...

  5. Android复习14【高级编程:推荐网址、抠图片上的某一角下来、Bitmap引起的OOM问题、三个绘图工具类详解、画线条、Canvas API详解(平移、旋转、缩放、倾斜)、矩阵详解】

    目   录 推荐网址 抠图片上的某一角下来 8.2.2 Bitmap引起的OOM问题 8.3.1 三个绘图工具类详解 画线条 8.3.16 Canvas API详解(Part 1) 1.transla ...

  6. Java中的Runtime类详解

    Java中的Runtime类详解 1.类注释 /**Every Java application has a single instance of class Runtime that allows ...

  7. [NewLife.XCode]实体类详解

    NewLife.XCode是一个有10多年历史的开源数据中间件,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode. 整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含 ...

  8. basicdatasourcefactory mysql_Java基础-DBCP连接池(BasicDataSource类)详解

    Java基础-DBCP连接池(BasicDataSource类)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 实际开发中"获得连接"或"释放资源 ...

  9. JAVA的StringBuffer类详解

    JAVA的StringBuffer类详解 StringBuffer类和String一样,也用来代表字符串,只是由于StringBuffer的内部实现方式和String不同,所以StringBuffer ...

最新文章

  1. 基于OpenCV的行人目标检测
  2. SQL SERVER 使用 OPENRORWSET(BULK)函数将txt文件中的数据批量插入表中(2)
  3. 并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法
  4. 为选择合适的ERP供应商,是否该发布需求建议书(RFP)?
  5. 【剑指offer】——求出一个正整数的质数因子(Python)
  6. 一文梳理JavaScript中常见的七大继承方案
  7. Manacher入门
  8. Java NIO教程
  9. MS CRM 2011 如何创建基于SQL的自定义报表,并使用数据预筛选(Pre-Filtering)
  10. Atitit 使用js nodejs进行图像处理ocr的解决方案attilax总结
  11. 一文读懂: 什么是用户故事?What is User Stories?
  12. qmc0文件怎么转换mp3_怎么用手机把手机里的视频转换成mp3音乐?(手机,不是电脑)...
  13. html把字体设置为繁体,XP下怎样将繁体字设置成系统字体?XP下把系统字体改为繁体的方法...
  14. 手机屏幕物理点击器是什么原理_手机屏幕物理连点器
  15. 逆向教程-U3D游戏逆向分析(伊甸逆向分析)
  16. wd 文件服务器客服电话,wd 云服务器
  17. LFS : 制作分区和挂载分区
  18. bat批量安装软件,完成最后删除文件夹里所有安装包
  19. [导入]阿里妈妈广告牌生成器
  20. 解决QDialogButtonBox按钮的英文翻译问题

热门文章

  1. JUnit单元测试中多线程的坑
  2. 最小生成树(Kruskal和Prim算法)
  3. docker基本组成
  4. STM32开发 -- base64详解
  5. 日常生活小技巧 -- 玩转 PDF
  6. UNIX再学习 -- 环境变量
  7. 带你全面了解比特黄金(bitcoin gold)分叉
  8. 插件框架实现思路及原理
  9. 【问链-EOS公开课】第八课 EOS 数据库与持久化 API(一)
  10. Android设计模式MVVM之DataBinding简单使用