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

一步一步迈向 WPF 数据绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (105投票s)

2008 年 5 月 19 日

CPOL

6分钟阅读

viewsIcon

365006

downloadIcon

3803

逐步介绍WPF数据绑定世界。

引言

本文解释了WPF数据绑定的绝对基础知识。它展示了四种执行相同简单任务的不同方法。每一次迭代都更接近最简洁的纯XAML实现。本文面向没有WPF数据绑定经验的人士。

背景

WPF编程涉及大量数据绑定。WPF用户界面通常比大多数Windows Forms或ASP.NET用户界面使用更多的数据绑定。用户界面中的大多数(如果不是全部)数据移动都是通过数据绑定实现的。本文通过展示如何将纯代码解决方案转换为简洁的纯XAML解决方案,帮助WPF新手开始用WPF数据绑定的思路思考。

本文不怎么讨论绑定API。它只讨论与简单示例相关的部分。如果您想了解更多关于WPF数据绑定的技术细节,可以点击这里阅读我的文章。

演示应用程序

在本文中,我们将研究实现相同简单功能的几种方法。我们的目标是创建一个允许我们编辑一个人名和姓氏的WPF程序。该应用程序还应以<姓氏>, <名字>的格式显示该人的姓名。每当名字或姓氏更改时,格式化的姓名应立即更新。

用户界面应如下所示

screenshot.png

版本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);
}

在此版本中,我们不再直接将值分配给TextBoxTextBlockText属性。现在,我们将控件上的这些属性绑定到Person对象的属性。Binding类是WPF的一部分;事实上,它是所有WPF数据绑定的核心组件。设置Binding对象的Source属性表示绑定的数据源(即数据来自哪里)。设置Path属性表示如何从数据源获取绑定值。将UpdateSourceTrigger属性设置为‘PropertyChanged’告诉绑定在键入时更新,而不是等待TextBox失去焦点后再更新数据源。

这看起来都很好,但有一个问题。如果您现在运行程序,当您编辑名字或姓氏时,格式化的全名不会更新。在上一版本中,格式化的全名会更新,因为我们钩住了每个TextBoxTextChanged事件,并手动将新的FullName值推送到TextBlock。但现在,所有这些控件都已绑定,所以我们无法这样做。怎么回事?

WPF数据绑定系统并非神奇。它无法知道当FirstNameLastName属性被设置为新值时,我们的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的新实现不使用自动属性。由于我们需要在设置FirstNameLastName的新值时引发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开发人员通过方便的标记扩展语法使用数据绑定。在更复杂、动态的场景中,在代码中创建绑定可能会很有用。我希望本文能为您揭示一些关于该主题的见解,以便您能够就您想要如何完成工作做出明智的决定。

© . All rights reserved.