一步一步迈向 WPF 数据绑定






4.92/5 (105投票s)
逐步介绍WPF数据绑定世界。
引言
本文解释了WPF数据绑定的绝对基础知识。它展示了四种执行相同简单任务的不同方法。每一次迭代都更接近最简洁的纯XAML实现。本文面向没有WPF数据绑定经验的人士。
背景
WPF编程涉及大量数据绑定。WPF用户界面通常比大多数Windows Forms或ASP.NET用户界面使用更多的数据绑定。用户界面中的大多数(如果不是全部)数据移动都是通过数据绑定实现的。本文通过展示如何将纯代码解决方案转换为简洁的纯XAML解决方案,帮助WPF新手开始用WPF数据绑定的思路思考。
本文不怎么讨论绑定API。它只讨论与简单示例相关的部分。如果您想了解更多关于WPF数据绑定的技术细节,可以点击这里阅读我的文章。
演示应用程序
在本文中,我们将研究实现相同简单功能的几种方法。我们的目标是创建一个允许我们编辑一个人名和姓氏的WPF程序。该应用程序还应以<姓氏>, <名字>的格式显示该人的姓名。每当名字或姓氏更改时,格式化的姓名应立即更新。
用户界面应如下所示
版本1 - 手动移动数据
首先,我们将不使用数据绑定来实现此功能。让我们创建一个简单的类来保存人员姓名
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get
{
return String.Format("{0}, {1}",
this.LastName, this.FirstName);
}
}
}
接下来,我们在XAML中声明一个简单的用户界面。这些控件将显示我们Person
类的三个属性。它们存在于我们应用程序的主Window
中
<StackPanel>
<TextBox
x:Name="firstNameTextBox"
Width="200"
Margin="0,4"
/>
<TextBox
x:Name="lastNameTextBox"
Width="200"
Margin="0,4"
/>
<TextBlock
x:Name="fullNameTextBlock"
Background="LightBlue"
Margin="0,4"
/>
</StackPanel>
最后,我们可以在Window
的代码隐藏文件中编写一些代码来根据需要手动移动数据
Person _person;
// This method is invoked by the Window's constructor.
private void ManuallyMoveData()
{
_person = new Person
{
FirstName = "Josh",
LastName = "Smith"
};
this.firstNameTextBox.Text = _person.FirstName;
this.lastNameTextBox.Text = _person.LastName;
this.fullNameTextBlock.Text = _person.FullName;
this.firstNameTextBox.TextChanged += firstNameTextBox_TextChanged;
this.lastNameTextBox.TextChanged += lastNameTextBox_TextChanged;
}
void lastNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_person.LastName = this.lastNameTextBox.Text;
this.fullNameTextBlock.Text = _person.FullName;
}
void firstNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_person.FirstName = this.firstNameTextBox.Text;
this.fullNameTextBlock.Text = _person.FullName;
}
这类代码就像沼泽一样容易滋生bug。这种实现要求UI代码跟踪在特定属性值更改时需要更新哪些控件。这迫使我们在UI代码中重复问题的领域知识,这永远不是一件好事。如果我们处理的是更复杂的问题领域,这类代码会很快变得非常糟糕。一定有更好的办法……
版本2 - 代码绑定
使用我们Window
中完全相同的XAML,让我们重写代码隐藏,以便控件能够绑定到Person
对象。不再像以前那样让Window
的构造函数调用ManuallyMoveData
方法,现在它将调用此方法
private void BindInCode()
{
var person = new Person
{
FirstName = "Josh",
LastName = "Smith"
};
Binding b = new Binding();
b.Source = person;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
b.Path = new PropertyPath("FirstName");
this.firstNameTextBox.SetBinding(TextBox.TextProperty, b);
b = new Binding();
b.Source = person;
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
b.Path = new PropertyPath("LastName");
this.lastNameTextBox.SetBinding(TextBox.TextProperty, b);
b = new Binding();
b.Source = person;
b.Path = new PropertyPath("FullName");
this.fullNameTextBlock.SetBinding(TextBlock.TextProperty, b);
}
在此版本中,我们不再直接将值分配给TextBox
或TextBlock
的Text
属性。现在,我们将控件上的这些属性绑定到Person
对象的属性。Binding
类是WPF的一部分;事实上,它是所有WPF数据绑定的核心组件。设置Binding
对象的Source
属性表示绑定的数据源(即数据来自哪里)。设置Path
属性表示如何从数据源获取绑定值。将UpdateSourceTrigger
属性设置为‘PropertyChanged
’告诉绑定在键入时更新,而不是等待TextBox
失去焦点后再更新数据源。
这看起来都很好,但有一个问题。如果您现在运行程序,当您编辑名字或姓氏时,格式化的全名不会更新。在上一版本中,格式化的全名会更新,因为我们钩住了每个TextBox
的TextChanged
事件,并手动将新的FullName
值推送到TextBlock
。但现在,所有这些控件都已绑定,所以我们无法这样做。怎么回事?
WPF数据绑定系统并非神奇。它无法知道当FirstName
或LastName
属性被设置为新值时,我们的Person
对象的FullName
属性会发生变化。我们必须让绑定系统知道FullName
已更改。我们可以通过在Person
类上实现INotifyPropertyChanged
接口来实现,如下所示
public class Person : INotifyPropertyChanged
{
string _firstName;
string _lastName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
this.OnPropertyChanged("FirstName");
this.OnPropertyChanged("FullName");
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
this.OnPropertyChanged("LastName");
this.OnPropertyChanged("FullName");
}
}
public string FullName
{
get
{
return String.Format("{0}, {1}",
this.LastName, this.FirstName);
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(
this, new PropertyChangedEventArgs(propName));
}
#endregion
}
请注意,Person
的新实现不使用自动属性。由于我们需要在设置FirstName
或LastName
的新值时引发PropertyChanged
事件,因此我们必须改用普通属性和字段。
如果我们现在运行应用程序,当您编辑名字或姓氏时,格式化的全名文本会更新。这表明绑定系统正在监听Person
对象新的PropertyChanged
事件。此时,我们已经摆脱了上一版本中那些丑陋、容易出错的代码。我们的代码隐藏文件中没有确定何时更新哪个字段的逻辑。
我们仍然有很多代码。最好是将控件和数据之间的关系声明在XAML中。这样就可以将UI布局和配置与应用程序逻辑整齐地分开。如果您想使用设计工具,例如Microsoft Expression Blend,来创建您的用户界面,这一点尤其吸引人。
版本3 - XAML绑定(冗长)
现在,让我们注释掉第二个版本,看看如何将所有这些绑定代码移到XAML中。在代码隐藏中,我们将让Window
的构造函数调用此方法
private void BindInXaml()
{
base.DataContext = new Person
{
FirstName = "Josh",
LastName = "Smith"
};
}
其余的工作在XAML中完成。这是Window
的内容
<StackPanel>
<TextBox
x:Name="firstNameTextBox"
Width="200"
Margin="0,4"
>
<TextBox.Text>
<Binding
Path="FirstName"
UpdateSourceTrigger="PropertyChanged"
/>
</TextBox.Text>
</TextBox>
<TextBox
x:Name="lastNameTextBox"
Width="200"
Margin="0,4"
>
<TextBox.Text>
<Binding
Path="LastName"
UpdateSourceTrigger="PropertyChanged"
/>
</TextBox.Text>
</TextBox>
<TextBlock
x:Name="fullNameTextBlock"
Background="LightBlue"
Margin="0,4"
>
<TextBlock.Text>
<Binding Path="FullName" />
</TextBlock.Text>
</TextBlock>
</StackPanel>
该XAML使用属性元素语法为每个控件的Text
属性建立绑定。看起来我们正在将Text
属性设置为Binding
对象,但实际上我们没有。在底层,WPF XAML解析器将其解释为为Text
属性建立绑定的方式。每个Binding
对象的配置与上一个版本(全部在代码中)完全相同。
此时运行应用程序显示,基于XAML的绑定与之前看到的基于代码的绑定工作方式相同。两个示例都在创建相同类的实例,并将相同的属性设置为相同的值。然而,这似乎需要大量的XAML,特别是如果您是手动输入的。如果有一种不那么冗长的方式来创建相同的绑定就好了…
版本4 - XAML绑定
使用与上一个示例相同的代码隐藏方法,以及相同的Person
类,我们可以大大减少实现相同目标所需的XAML量。这里的关键是Binding
类实际上是一个标记扩展。标记扩展就像一个XAML魔术,允许我们以非常简洁的方式创建和配置对象。我们可以使用它们在XML属性的值内创建一个对象。此程序最终版本的XAML如下所示
<StackPanel>
<TextBox
x:Name="firstNameTextBox"
Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged}"
Width="200"
Margin="0,4"
/>
<TextBox
x:Name="lastNameTextBox"
Text="{Binding Path=LastName, UpdateSourceTrigger=PropertyChanged}"
Width="200"
Margin="0,4"
/>
<TextBlock
x:Name="fullNameTextBlock"
Text="{Binding FullName}"
Background="LightBlue"
Margin="0,4"
/>
</StackPanel>
该XAML几乎和原始版本一样短。然而,在这个例子中,没有将控件连接到数据源的任何连接代码。WPF中的大多数数据绑定场景都使用这种方法。使用Binding
标记扩展功能大大简化了您的XAML,并允许您花费时间处理更重要的事情。
结论
有许多方法可以将用户界面连接到数据。没有一种是错误的,而且在某些情况下它们都有用。大多数时候,WPF开发人员通过方便的标记扩展语法使用数据绑定。在更复杂、动态的场景中,在代码中创建绑定可能会很有用。我希望本文能为您揭示一些关于该主题的见解,以便您能够就您想要如何完成工作做出明智的决定。