《Programming WPF》翻译 第4章 1.不使用数据绑定
考虑一个非常简单的应用程序:遍及一个人的名字和年龄,正如图4-1所示:
图4-1
图4-1可以实现为一个简单的xaml如示例4-1。
示例4-1
<Window >
<Grid>
<TextBlock >Name:</TextBlock>
<TextBox x:Name="nameTextBox" />
<TextBlock >Age:</TextBlock>
<TextBox x:Name="ageTextBox" />
<Button x:Name="birthdayButton" >Birthday</Button>
</Grid>
</Window>
在这个简单应用程序中显示的数据,可以被一个简单的类表现,如示例4-2所示。
示例4-2
string name;
public string Name {
get { return this.name; }
set { this.name = value; }
}
int age;
public int Age {
get { return this.age; }
set { this.age = value; }
}
public Person( ) {}
public Person(string name, int age) {
this.name = name;
this.age = age;
}
}
通过这个类,可以自然的实现我们的应用程序行为,如示例4-3所示:
示例4-3
public class Person {}
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
this.birthdayButton.Click += birthdayButton_Click;
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age;
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
示例4-3的代码创建了一个Person对象,并且用Person对象的属性初始化了文本框。当Birthday按钮按下时,Person对象的Age属性值会增加,同时在一个消息框中显示更新后的Person数据,如图4-2所示。
图4-2
我们的简单应用程序实现,事实上,非常的简单。Person对象的Age属性在改变后,显示在消息框中,但是不会显示在主窗体中。一个保持应用程序UI是最新的办法是,编写代码使得无论一个Person对象何时更新,将会同时间手动更新UI,正如示例4-4所示。
示例4-4
++person.Age;
// Manually update the UI
this.ageTextBox.Text = person.Age.ToString( );
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
仅仅一行代码,我们就“修复”了这个应用程序。这是一个诱人而且流行的方法,然而不能随着应用程序变得复杂而伸缩,并且需要更多这样的“单行”代码。我们需要一个更好的方法,超越于最简单的应用程序之上。
4.1.1对象的改变
对于UI,一个更健壮的跟踪对象改变的方法是,当对象改变的时候为这个对象激发一个事件。从.NET2.0时开始,正确的方法是为这个对象实现INotifyPropertyChanged接口,正如示例4-5。
示例4-5
public class Person : INotifyPropertyChanged {
// INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propName) {
if( this.PropertyChanged != null ) {
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
string name;
public string Name {
get { return this.name; }
set {
this.name = value;
OnPropertyChanged("Name");
}
}
int age;
public int Age {
get { return this.age; }
set {
this.age = value;
OnPropertyChanged("Age");
}
}
public Person( ) {}
public Person(string name, int age) {
this.name = name;
this.age = age;
}
}
在示例4-5中,当Person对象的任意一个属性改变时(如由Birthday按钮激活引起的实现),就会激活该对象的PropertyChanged事件。我们可以使用这个事件保持UI同步于Person的属性值,正如示例4-6。
示例4-6
public class Person : INotifyPropertyChanged {}
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
// Watch for changes in Tom's properties
person.PropertyChanged += person_PropertyChanged;
this.birthdayButton.Click += birthdayButton_Click;
}
void person_PropertyChanged(
object sender,
PropertyChangedEventArgs e) {
switch( e.PropertyName ) {
case "Name":
this.nameTextBox.Text = person.Name;
break;
case "Age":
this.ageTextBox.Text = person.Age.ToString( );
break;
}
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age; // person_PropertyChanged will update ageTextBox
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
示例4-6显示了一个单独的Person示例,创建于主窗体第一次开始出现,使用Person值初始化了Name和Age的文本框,订阅了随属性改变的事件,用来保持当Person对象改变时文本框仍然是最新的。在这段代码的恰当位置,birthday按钮的click事件句柄不需要手动更新文本框,当Tom的年龄改变的时候;代替的,更新Age属性引起层叠式事件保持年龄的文本框是最新的,随着Person对象的改变,正如图4-3所示。
图4-3
步骤如下:
1.用户点击按钮,引起Click的事件被激活
2.Click句柄从Person对象获得年龄:9
3.Click句柄将Person对象的年龄为10
4.Person的Age属性设置器激发了PropertyChanged事件
5. PropertyChanged事件传递到UI代码的事件句柄
6.UI代码更新年龄的文本框,从9改为10
7.按钮的Click事件句柄显示一个消息框,显示新的年龄:10
在消息框显示Tom的新年龄之前,在表单中,年龄的文本框已经更新了,如图4-4所示
图4-4
随着处理了InotifyPropertyChanged的事件,当对象的数据改变时,UI将更新以反映这种改变。然而,这仅仅解决了问题的一半;我们仍然需要处理如何将UI中的改变反映到对象中。
4.1.2控件的改变
不考虑跟踪UI改变并将其反映到对象的特定方法,我们可以容易地以一个例子告终(想改变一个人的名字),显示对象(正如点击Birthday按钮时所发的),以及期望改变已经发生,仅仅对图4-5有所失望。
图4-5
注意到图4-5,表单中,名字是“Thomsen Federick”,而消息框中是“Tom”,这显示了UI的一部分已经改变,而底层的对象并未改变。为了修复这个问题,我们观察文本框对象的Text属性的改变,相应的更新Person对象,如示例4-7。
示例4-7
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Fill initial person fields
this.nameTextBox.Text = person.Name;
this.ageTextBox.Text = person.Age.ToString( );
// Watch for changes in Tom's properties
person.PropertyChanged += person_PropertyChanged;
// Watch for changes in the controls
this.nameTextBox.TextChanged += nameTextBox_TextChanged;
this.ageTextBox.TextChanged += ageTextBox_TextChanged;
this.birthdayButton.Click += birthdayButton_Click;
}
void nameTextBox_TextChanged(object sender, TextChangedEventArgs e) {
person.Name = nameTextBox.Text;
}
void ageTextBox_TextChanged(object sender, TextChangedEventArgs e) {
int age = 0;
if( int.TryParse(ageTextBox.Text, out age) ) {
person.Age = age;
}
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
++person.Age;
// nameTextBox_TextChanged and ageTextBox_TextChanged
// will make sure the Person object is up to date
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
现在,不论数据如何改变,Person对象和显示Person对象的UI会保持同步。图4-6显示了UI中名字的改变,正确传到了Person对象。
图4-6
尽管我们得到了想要的功能,仍需要写相当多的代码使之发生:
- Window1代码重构,设置控件的初始值
- Window1代码重构,使用PropertyChanged事件钩子,对Person对象的属性更改进行跟踪。
- PropertyChanged事件句柄从Person对象获取更新过的数据,将数据转换为适当的字符串
- Window1代码重构,使用TextBox对象的TextChanged事件钩子,跟踪UI的改变
- TextChanged事件句柄将更新过的TextBox数据传入Person对象,将数据适当的转换
这段代码允许我们安全的写自己的birthday按钮事件句柄,是所有的改变同步,当我们显示消息框的时候。然而,容易想象到,当对象的数量增加或者对象属性的数量增加时,这段代码很快就会失去控制。加上,这看起来就像一件相当普通的事情,以至于有人一定事先就提供了一种更简单的方法来做这件事。事实上,这种方法被称为“数据绑定”。