文章目录
- 一、引言
- 二、ObservableCollection
- 三、结语
一、引言
在GUI编程中经常会用到条目控件,常见的如ComboBox(下拉列表框),它内部往往有多个项。
在使用一些图形框架(Qt、WinForm)上进行原始开发时,往往在界面初始化阶段直接访问UI控件,往其中添加列表项;若后期有改动,通过事件触发处理程序来再往其中增删项。
这种方式有一定局限性,你想要更新列表中的数据并使之呈现在UI上就得直接访问控件。如果只是ComboBox这种简单的条目控件,那直接 comboBox.AddItem就好;但如果是在更复杂的控件中(如grid网格),要想添加新项,代码就没那么容易写了。
WPF中的ObservableCollection可以将控件与数据源绑定,使得控件与数据源保持同步。简单讲,在控件与数据源绑定后,你只需要往数据源中增删数据,而不需要管UI变更逻辑,且实现了前后端分离。
二、ObservableCollection
微软官方文档中描述,ObservableCollection是一个动态数据集合,当集合中的项增加、移除、更新,或整个列表刷新时,它会提供通知(通知绑定的控件做出改变)。
下面是我写的demo,左边是一个ComboBox,它的ItemSource绑定了集合对象;右边是一些按钮,按钮单击的命令会修改集合对象,
下面是界面主要的XAML代码:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding Items}" Height="30" x:Name="comboBox"/>
<StackPanel Grid.Column="1">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="8"/>
</Style>
</StackPanel.Resources>
<Button Content="添加一项" Command="{Binding AddItemCommand}"/>
<Button Content="移除一项" Command="{Binding RemoveItemCommand}"/>
<Button Content="更新项" Command="{Binding UpdateItemCommand}"/>
<Button Content="替换整个列表" Command="{Binding ReplaceListCommand}"/>
<Button Content="查看ItemSource绑定对象" Command="{Binding InspectObjectCommand}" CommandParameter="{Binding ElementName=comboBox}"/>
</StackPanel>
</Grid>
下面是后台ViewModel中的代码,主要是命令:
public MainWindowViewModel()
{
_items = new ObservableCollection<string>()
{
"初始项1",
"初始项2",
"初始项3"
};
}
private ObservableCollection<string> _items;
public ObservableCollection<string> Items
{
get => _items;
}
private RelayCommand _addItemCommand;
public RelayCommand AddItemCommand
{
get => _addItemCommand?? (_addItemCommand = new RelayCommand(() =>
{
_items.Add("新加项");
}));
}
private RelayCommand _removeItemCommand = null;
public RelayCommand RemoveItemCommand
{
get => _removeItemCommand ?? (_removeItemCommand = new RelayCommand(() =>
{
_items.Remove(_items.Last());
}));
}
private RelayCommand _updateItemCommand;
public RelayCommand UpdateItemCommand
{
get => _updateItemCommand ?? (_updateItemCommand = new RelayCommand(() =>
{
_items[0] = "更新项";
}));
}
private RelayCommand _replaceListCommand;
public RelayCommand ReplaceListCommand
{
get => _replaceListCommand ?? (_replaceListCommand = new RelayCommand(() =>
{
_items = new ObservableCollection<string>()
{
"替换后的表"
};
MessageBox.Show("替换完毕");
}));
}
private RelayCommand<object> _inspectObjectCommand;
public RelayCommand<object> InspectObjectCommand
{
get => _inspectObjectCommand ?? (_inspectObjectCommand = new RelayCommand<object>((o) =>
{
ComboBox cb = o as ComboBox;
MessageBox.Show(string.Format("HashCode:\nItemSource={0}\n_items={1}\n是否相等?{2}",
cb.ItemsSource.GetHashCode(), _items.GetHashCode(),cb.ItemsSource.Equals(_items)));
}));
}
其中增、删、改这三个命令都能即时反映到UI。而替换列表命令并不能起作用。单击 查看绑定对象 按钮,可以看到,ItemSource和后台_items的hashCode是相等的,并且用equals比较它们也是相等的;
接着,单击 替换整个列表 按钮,再查看绑定对象,这次ItemSource的hashCode并未发生变化,且equals也反映出与后台_items不等。
可见,对绑定的数据源做替换并不能修改ItemSource(访问控件的ItemSource做修改是可以的,虽然这是废话)。
引用类型与基本类型
这里的本质问题其实是引用类型与基本类型的赋值问题。
int a = 1; int b = a;
基本类型(如 数字、字串、布尔,通常是较简单的)的赋值,是把值拷贝过去。
声明基本类型时,会在栈内存中开辟一块空间,存放变量的值。当a的值赋给b时,a的值会被复制到b的空间中。它们两值的存放是独立的,在不同空间中,如下图。
而引用类型数据的变量名存放在栈内存中,值存放在堆内存中,栈内存会提供一个引用地址用于指向堆内存中的值。非常类似于C语言中的指针,
所以这里的情况就是,
而_items = new ObservableCollection(){ "替换后的表"}
其实就是在堆空间中再开辟一块内存,然后将_items变量的栈内存控件里的地址指向它。因此ItemSource并不会改变。
因此想替换整个列表,应该直接替换ItemSource的值。
三、结语
ObservableCollection基本使用如上述代码示例所示。其中要注意的是,替换ViewModel中的绑定对象并不能真实替换ItemSource。还有ObservableCollection不是线程安全的,ItemSource绑定其后,不能跨线程(UI线程外)修改ObservableCollection,关于这点会另辟文进行介绍。