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

我为什么热爱 Silverlight 数据绑定

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.21/5 (15投票s)

2009年12月3日

CPOL

7分钟阅读

viewsIcon

56151

downloadIcon

708

用 ViewModel 解决常见的 UI 问题。

引言

MVVM 设计模式(又名 ViewModel)被广泛讨论。这不是一个好兆头。成熟、已建立的设计模式不会被过度讨论。它们被简单地学习和使用。

如果有什么的话,大量的讨论反映了需要更好地理解。许多文章和博客文章在概念层面讨论 MVVM。相当多的博主认为需要额外的框架,并专注于框架而不是设计模式本身。一些博主提供了一个 ViewModel 的实际示例,但所解决的问题可以不使用 ViewModel 轻松实现。很少有人真正描述(例如 这篇)一个非平凡的问题,然后用 ViewModel 优雅地解决。

本文描述了一个常见的 UI 问题,并展示了如何使用 MVVM 来解决它。

免责声明:与其他模式不同,MVVM 没有被清楚地描述(例如,在维基百科上,它很好地定义了其他模式),因此有可能这实际上并不是 MVVM 的一个例子。尽管如此,问题和解决方案仍然很有趣。

问题所在

在用户界面中,屏幕上的几个项目经常需要同步。一个简单的例子是“保存”按钮,它指示是否有内容需要保存;禁用该按钮可向用户提供操作已成功完成的反馈。另一个例子是更新 DataGrid 中显示的记录,以及 DataFormDataGrid 显示原始值,而 DataForm 显示更新的、但尚未保存的值。这是本文讨论的例子。

我们的要求是

  1. DataForm 显示 DataGrid 中选定的项目。
  2. DataGrid 是只读的,显示已保存的值;DataForm 显示当前(可能已更改、未保存)的值。
  3. DataGridDataForm 都使用特殊背景色指示项目是否仍需要保存。
  4. 有一个摘要行,指示有多少条记录未保存。
  5. 有一个“保存”按钮,用于保存(单个)选定的记录。
  6. 有一个“重置”按钮,用于重置(单个)选定的记录。
  7. 当且仅当选定的记录尚未保存时,“保存”和“重置”按钮才会被启用。
  8. 显示的内容(人员记录)可以通过性别和记录的更改状态进行筛选。

请注意,数据的状态以各种需要保持同步的方式反映出来。用户可以并行编辑单个记录,并可以撤销或保存单个更改。应用程序随时清晰地反馈数据状态。我们可以通过大量事件处理来实现这一点。但 Silverlight 数据绑定支持一种更优雅的解决方案:ViewModel 解决方案。

Visual Studio 解决方案

在 Visual Studio 中,我们有一个标准的 Silverlight WCF 服务,它在收到请求时将 Person 对象列表发送到客户端。Person 类定义在 ASP.NET Web 应用程序项目中的Model/Person.cs 文件中。创建服务引用后,Visual Studio 会在 ViewModelDemo.DemoServiceReference 命名空间中创建等效的 Person 对象。我们重命名此命名空间

using Model = ViewModelDemo.DemoServiceReference;

以便客户端上的 Person 类可以称为 Model.Person。在客户端,我们还有一个 ViewModel.Person 类,它包装了 Model.Person

public class Person : INotifyPropertyChanged
{
    private Model.Person _modelPerson = null;

    public Person(Model.Person modelPerson)
    {
        _modelPerson = modelPerson;
        // editable properties:
        _firstName = _modelPerson.FirstName;
        _lastName = _modelPerson.LastName;
        _dob = _modelPerson.DateOfBirth;
        _gender = _modelPerson.Gender;
    }

我们可以说 Model.Person 对象保存在 ViewModel.Person 对象中,以记住其属性的原始值。这样,我们可以轻松重置值并检查它们是否已更改。成功保存操作后,相应的 Person 对象将再次具有相同的属性值。对于 Model.Person 的每个属性,ViewModel.Person 都有 *两个* 属性。它们分别用于绑定到 DataFormDataGrid(**Req. 2**)。

private string _firstName;
[Display(Name="First Name")]
public string FirstName
{
     get
     {
         return _firstName;
     }
     set
     {
         if (_firstName != value)
         {
             _firstName = value;     // validation: out of scope
             NotifyPropertyChanged("FirstName");
             IsDirty = (_firstName != _modelPerson.FirstName);
          }
     }
}
        
[Display(AutoGenerateField = false)]
public string FirstNameModel
{
    get
    {
        return _modelPerson.FirstName;
    }
    private set
    {
        if (_modelPerson.FirstName != value)
        {
            _modelPerson.FirstName = value;
            NotifyPropertyChanged("FirstNameModel");
        }
    }
}

ViewModel.Person 有一个 IsDirty 属性,有助于实现 **Req. 7**。

private bool _isDirty;
[Display(AutoGenerateField = false)]
public bool IsDirty
{
    get
    {
        return _isDirty;
    }
    private set
    {
        _isDirty = value;
        NotifyPropertyChanged("IsDirty");
        NotifyPropertyChanged("BackgroundColorString");
    }
}

在我们完成 Req. 7 之前,让我们看一下属性 BackgroundColorString

[Display(AutoGenerateField=false)]
public string BackgroundColorString
{
    get
    {
        if (_isDirty)
        {
            return "#FFCE5B";  // Colors.Orange.ToString();
        }
        return null;
    }
}

事实上,这涵盖了 **Req. 3**,因为 DataFormBackground 属性和 DataGrid 行(每个单元格中的 Panel)会观察此 BackgroundColorString 属性 - 每当它更改时,它们都会更新其背景。回到 Req. 7,两个按钮的定义如下所示

<Button x:Name="SaveButton" 
       IsEnabled="{Binding SelectedItem.IsDirty, ElementName=TheGrid}" 
       VerticalAlignment="Top" Margin="5">
    <StackPanel Orientation="Horizontal" Margin="5">
        <TextBlock Text="Save" VerticalAlignment="Center" Margin="5,0" />
        <Image Source="arrow-forward_32.png" />
    </StackPanel>
</Button>

我们使用元素绑定将按钮同步到 DataGrid 中选定的项目,并将按钮的 IsEnabled 属性绑定到当前选定 Person 记录的 IsDirty 属性。这就完成了 **Req. 7**,还剩五个要求。元素绑定也用于同步 DataGridDataForm(**Req. 1**);DataForm 的定义包含

CurrentItem="{Binding SelectedItem, ElementName=TheGrid, Mode=OneWay}"

哇。元素绑定是我的朋友!摘要行位于 DataGrid 正下方(**Req. 4**),是一个简单的 TextBlock,它绑定到(或观察)一个 SummaryText 属性。现在,这个属性不是 ViewModel.Person 的属性,而是 **应用程序缓存** 的属性。

缓存

为什么要使用缓存?嗯,为什么不呢?在过去,使用 HTML 和 JavaScript(还记得吗?),我从未想过客户端缓存的可能性,可能是因为数据和标记是不可分割地混合在一起的。但是,现在有了 Silverlight 技术,显然我们希望在客户端缓存数据,以最大限度地减少服务器负载并优化应用程序性能。因此,我们将主页的 DataContext 设置为应用程序缓存(即使只有一个页面,也是一个单例)。缓存有一个 People 属性,DataGridPagerDataForm 都绑定到此属性

private PagedCollectionView _people;
public PagedCollectionView People
{
    get
    {
        if (_people == null)
        {
            _people = new PagedCollectionView(new Collection<viewmodel.person>());
            // to avoid calling service more than once
            // (if there is more than one control bound to People)

            DemoServiceClient proxy = new DemoServiceClient();
            proxy.GetPeopleCompleted += 
              new EventHandler<getpeoplecompletedeventargs>(proxy_GetPeopleCompleted);
            proxy.GetPeopleAsync();
        }
        return _people;
    }
    set
    {
        _people = value;
        NotifyPropertyChanged("People");
    }
}
        
private void proxy_GetPeopleCompleted(object sender, GetPeopleCompletedEventArgs e)
{
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message);  // bare bones
        return;
    }

    Collection<viewmodel.person> people = new Collection<viewmodel.person>();
    foreach (Model.Person modelPerson in e.Result)
    {
        ViewModel.Person person = new ViewModel.Person(modelPerson);
        people.Add(person);
        person.PropertyChanged += new PropertyChangedEventHandler(person_PropertyChanged);
    }
    People = new PagedCollectionView(people);
    ApplyFilters();
}
        
private void person_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "IsDirty":
            NotifyPropertyChanged("ChangeSummary");
            break;
    }
}

你看到了吗?应用程序缓存会观察 People 集合中的每个 ViewModel.Person,并且每当它们的 IsDirty 属性发生更改时,任何观察应用程序缓存的 ChangeSummary 属性的 UI 元素都会收到通知。你猜对了,摘要行就是观察应用程序缓存的这个属性

public string ChangeSummary
{
    get
    {
        int count = 0;
        foreach (ViewModel.Person person in _people.SourceCollection)
        {
            if (person.IsDirty)
            {
                count += 1;
            }
        }
        if (count == 1)
        {
            return String.Format("There is one person with unsaved changes.", count);
        }
        return String.Format("There are {0} persons with unsaved changes.", count);
    }
}

如果您在 People 属性的 getter 上设置断点来运行应用程序,您会发现它被击中了数次。第一次,会调用服务器,并且 _people 被赋予一个非 null 值。后者之所以这样做,是因为有几个 UI 元素绑定到 People 属性,而我们只想在调用一次服务器。一旦服务器返回 People 集合,就会调用 People 属性的 setter,从而触发所有这些 UI 元素再次调用 getter 来获取 PagedCollectionView。很棒。

**Req. 6** 在页面的代码隐藏中很容易实现(Quelle Horreur!)

private void ResetButton_Click(object sender, RoutedEventArgs e)
{
    ViewModel.Person currentPerson = TheGrid.SelectedItem as ViewModel.Person;
    if (currentPerson != null)
    {
        currentPerson.Reset();
    }
}

它调用 ViewModel.Person 的一个简单方法

public void Reset()
{
    FirstName = _modelPerson.FirstName;
    LastName = _modelPerson.LastName;
    DateOfBirth = _modelPerson.DateOfBirth;
    Gender = _modelPerson.Gender;
}

**Req. 5** 需要更多的代码,因为我们需要调用服务器。我们确保在实际更新 UI 之前,此调用是成功的。后者同样是隐式完成的,通过设置会引发 PropertyChanged 事件的属性

public void Save()
{
    DemoServiceClient proxy = new DemoServiceClient();
    Model.Person tmpPerson = new Model.Person()
    {
        PersonId = _modelPerson.PersonId,
        FirstName = _firstName,
        LastName = _lastName,
        DateOfBirth = _dateOfBirth,
        Gender = _gender
    };
    proxy.SavePersonCompleted += 
      new EventHandler<savepersoncompletedeventargs>(SavePersonCompleted);
    proxy.SavePersonAsync(tmpPerson);
}

private void SavePersonCompleted(object sender, SavePersonCompletedEventArgs e)
{
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message, 
          "Server Error", MessageBoxButton.OK);
        return;
    }
    Model.Person saved = e.Result;
    if (saved.PersonId == this.PersonId)
    {
        FirstNameModel = _firstName;
        LastNameModel = _lastName;
        DateOfBirthModel = _dateOfBirth;
        GenderModel = _gender;
        IsDirty = false;
    }
}

这样就剩下 **Req. 8** 了。我们可以使用 ComboBoxes 的 SelectionChanged 事件来实现(上面图片中未显示)。但是,既然我们有了一个新锤子,我们也可以看看是否可以用它来解决这个要求。是的,我们可以!我们可以使用 TwoWay 绑定将每个 ComboBoxCurrentItem 属性绑定到应用程序缓存的一个属性。这样,我们就可以检测到过滤器选择已更改,并设置 PeoplePagedCollectionView)的过滤器。不过,这有点牵强。

关注点

你注意到

  • 用于同步 UI 元素的唯一事件是 PropertyChanged
  • 数据仅检索一次,并且仅在需要时检索。富 Internet 应用程序可以最大限度地减少服务器负载(因此可能更 高效)。
  • 除了 MVVM,该解决方案还使用了 观察者单例 设计模式。

你可能还注意到验证没有被解决,并且(相当不一致地)我们为 DataGrid 使用了 AutoGenerateColumns="false",为 DataForm 使用了 AutoGenerateFields="true",所以我们需要抑制显示访问 Model.Person 的属性 - [Display(AutoGenerateField = false)]

在运行示例应用程序之前

  1. 将 ViewModelDemo.Web 设置为启动项目。
  2. ViewModelDemoTestPage.aspx 设置为起始页。
  3. 在 Silverlight 项目的服务引用文件夹中添加服务引用。发现并调用命名空间 DemoServiceReference
  4. 构建。

历史

  • 2009 年 12 月 2 日 - 第一个版本。
© . All rights reserved.