我为什么热爱 Silverlight 数据绑定






4.21/5 (15投票s)
用 ViewModel 解决常见的 UI 问题。
引言
MVVM 设计模式(又名 ViewModel)被广泛讨论。这不是一个好兆头。成熟、已建立的设计模式不会被过度讨论。它们被简单地学习和使用。
如果有什么的话,大量的讨论反映了需要更好地理解。许多文章和博客文章在概念层面讨论 MVVM。相当多的博主认为需要额外的框架,并专注于框架而不是设计模式本身。一些博主提供了一个 ViewModel 的实际示例,但所解决的问题可以不使用 ViewModel 轻松实现。很少有人真正描述(例如 这篇)一个非平凡的问题,然后用 ViewModel 优雅地解决。
本文描述了一个常见的 UI 问题,并展示了如何使用 MVVM 来解决它。
免责声明:与其他模式不同,MVVM 没有被清楚地描述(例如,在维基百科上,它很好地定义了其他模式),因此有可能这实际上并不是 MVVM 的一个例子。尽管如此,问题和解决方案仍然很有趣。
问题所在
在用户界面中,屏幕上的几个项目经常需要同步。一个简单的例子是“保存”按钮,它指示是否有内容需要保存;禁用该按钮可向用户提供操作已成功完成的反馈。另一个例子是更新 DataGrid
中显示的记录,以及 DataForm
。DataGrid
显示原始值,而 DataForm
显示更新的、但尚未保存的值。这是本文讨论的例子。
我们的要求是
DataForm
显示DataGrid
中选定的项目。DataGrid
是只读的,显示已保存的值;DataForm
显示当前(可能已更改、未保存)的值。DataGrid
和DataForm
都使用特殊背景色指示项目是否仍需要保存。- 有一个摘要行,指示有多少条记录未保存。
- 有一个“保存”按钮,用于保存(单个)选定的记录。
- 有一个“重置”按钮,用于重置(单个)选定的记录。
- 当且仅当选定的记录尚未保存时,“保存”和“重置”按钮才会被启用。
- 显示的内容(人员记录)可以通过性别和记录的更改状态进行筛选。
请注意,数据的状态以各种需要保持同步的方式反映出来。用户可以并行编辑单个记录,并可以撤销或保存单个更改。应用程序随时清晰地反馈数据状态。我们可以通过大量事件处理来实现这一点。但 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
都有 *两个* 属性。它们分别用于绑定到 DataForm
和 DataGrid
(**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**,因为 DataForm
的 Background
属性和 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**,还剩五个要求。元素绑定也用于同步 DataGrid
和 DataForm
(**Req. 1**);DataForm
的定义包含
CurrentItem="{Binding SelectedItem, ElementName=TheGrid, Mode=OneWay}"
哇。元素绑定是我的朋友!摘要行位于 DataGrid
正下方(**Req. 4**),是一个简单的 TextBlock
,它绑定到(或观察)一个 SummaryText
属性。现在,这个属性不是 ViewModel.Person
的属性,而是 **应用程序缓存** 的属性。
缓存
为什么要使用缓存?嗯,为什么不呢?在过去,使用 HTML 和 JavaScript(还记得吗?),我从未想过客户端缓存的可能性,可能是因为数据和标记是不可分割地混合在一起的。但是,现在有了 Silverlight 技术,显然我们希望在客户端缓存数据,以最大限度地减少服务器负载并优化应用程序性能。因此,我们将主页的 DataContext
设置为应用程序缓存(即使只有一个页面,也是一个单例)。缓存有一个 People
属性,DataGrid
、Pager
和 DataForm
都绑定到此属性
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** 了。我们可以使用 ComboBox
es 的 SelectionChanged
事件来实现(上面图片中未显示)。但是,既然我们有了一个新锤子,我们也可以看看是否可以用它来解决这个要求。是的,我们可以!我们可以使用 TwoWay 绑定将每个 ComboBox
的 CurrentItem
属性绑定到应用程序缓存的一个属性。这样,我们就可以检测到过滤器选择已更改,并设置 People
(PagedCollectionView
)的过滤器。不过,这有点牵强。
关注点
你注意到
- 用于同步 UI 元素的唯一事件是
PropertyChanged
。 - 数据仅检索一次,并且仅在需要时检索。富 Internet 应用程序可以最大限度地减少服务器负载(因此可能更 高效)。
- 除了 MVVM,该解决方案还使用了 观察者 和 单例 设计模式。
你可能还注意到验证没有被解决,并且(相当不一致地)我们为 DataGrid
使用了 AutoGenerateColumns="false"
,为 DataForm
使用了 AutoGenerateFields="true"
,所以我们需要抑制显示访问 Model.Person
的属性 - [Display(AutoGenerateField = false)]
。
在运行示例应用程序之前
- 将 ViewModelDemo.Web 设置为启动项目。
- 将 ViewModelDemoTestPage.aspx 设置为起始页。
- 在 Silverlight 项目的服务引用文件夹中添加服务引用。发现并调用命名空间
DemoServiceReference
。 - 构建。
历史
- 2009 年 12 月 2 日 - 第一个版本。