写点什么

论道 WP(五):通过附加属性和行为扩展控件

  • 2012-09-12
  • 本文字数:4584 字

    阅读完需:约 15 分钟

最近和朋友合作一个应用,开发的时候遇到两个问题,第一个是 ListBox 控件的多选问题,第二个是 PhotoChooserTask 选择器和 Image 控件的配合问题。巧合的是,近日有读者在我的博客里提到他也遇到第二个问题,因此,我想在这篇文章里分享一下如何使用附加属性和Expression Blend 行为解决这两个问题。

如何绑定ListBox 控件的SelectedItems 属性?

当我们把SelectionMode 属性的值设为Multiple 时,ListBox 控件就能支持多选了,如图1 所示,此时,我们可以通过ListBox 控件的SelectedItems 属性获取选中的项。接下来,我很自然就会想到把ListBox 控件的ItemsSource 和SelectedItems 两个属性分别绑到视图模型的对应属性,但是,SelectedItems 属性不是依赖属性,无法进行数据绑定,怎么办?

图 1

既然自带的SelectedItems 属性不支持数据绑定,我们就自己创建一个支持数据绑定的吧。在Silverlight 里,我们可以通过附加属性扩展依赖对象,比如说,我们可以在ListBox 控件上设置Grid.Row 附加属性,如代码1 所示。

代码 1

Grid.Row 附加属性不是 ListBox 控件的属性,却被用来附加额外的数据。在 ListBox 控件上设置 Grid.Row 附加属性本身不会产生什么效果,但是,当我们把这个 ListBox 控件放在一个 Grid 里时,这个 Grid 将会根据 Grid.Row 附加属性的值安排 ListBox 控件的位置,这是附加属性的常见用途。

回到我们的问题,我希望创建一个 SelectedItems 附加属性,作为自带的 SelectedItems 属性和视图模型对应的属性之间的桥梁,如代码 2 所示。当我们把 SelectedItems 附加属性绑到视图模型的对应属性时,前者会把后者的数据添加到自带的 SelectedItems 属性。当用户在用户界面上更改选中的项时,SelectedItems 附加属性会把数据更新回视图模型的对应属性。

代码 2

接下来,我们一起看看这个 SelectedItems 附加属性是如何实现的。创建一个类,然后在里面创建一个 SelectedItems 附加属性,如代码 3 所示。在 Visual Studio 里,你可以通过 propa 这个代码段快速创建附加属性。值得提醒的是,你必须同时提供GetSelectedItems 和SetSelectedItems 两个方法,否则无法在XAML 里读或写这个附加属性的值。

代码 3

虽然现在已经可以在ListBox 控件上把SelectedItems 附加属性绑到视图模型的对应属性,但是仅仅这样无法实现我们想要的效果,因为ListBox 控件并不认识这个外来的异物。于是,把绑定的数据添加到自带的SelectedItems 属性的责任就落到SelectedItems 附加属性的身上了。我希望SelectedItems 附加属性能在其值发生改变时自动刷新自带的SelectedItems 属性的值,因此,在注册SelectedItems 附加属性的时候通过PropertyMetadata 指定负责刷新的方法。

在刷新之前,我们必须确保目标控件是ListBox 控件,并且处于多选模式,如代码4 所示,否则就多此一举了。刷新的代码非常简单,只需把自带的SelectedItems 属性清空,然后把绑定的数据添加进来就行了。值得提醒的是,listBox.SelectedItems.Clear(); 必须放在if (collection != null) 的外面,否则,当绑定的数据变成null 时,ListBox 控件仍会显示之前选中的项,这显然是不对的。

代码 4

当用户在用户界面上更改选中的项时,会触发ListBox 控件的SelectionChanged 事件,我们可以趁此机会把数据更新回数据源,如代码5 所示。值得提醒的是,因为SelectedItems 附加属性的值有可能从一个有效值变成null,所以我们必须在改变之后的值不为null 时才订阅事件,否则,试图更新的时候将会引发NullReferenceException 异常。此外,因为向自带的SelectedItems 属性添加数据会触发ListBox 控件的SelectionChanged 事件,所以每次刷新之前需要取消订阅事件(首次除外),否则,相同的数据最终会在数据源出现两次。

代码 5

在SelectionChanged 事件的事件处理程序里,我们可以通过SelectionChangedEventArgs 对象的AddedItems 属性获取用户选中的项,并添加到数据源;通过SelectionChangedEventArgs 对象的RemovedItems 属性获取用户取消选中的项,并从数据源移除,如代码6 所示。

代码 6

这里的实现只在SelectedItems 附加属性的值发生改变时才刷新自带的SelectedItems 属性的值,如果你的数据源是ObservableCollection 对象,而你又希望数据源的更改可以实时反映到自带的SelectedItems 属性上,你可以订阅ObservableCollection 对象的CollectionChanged 事件。

如何选取Image 控件的图片?

在Windows Phone 里,如果你想为一个联系人选取一个头像,可以在新建/ 编辑联系人页面上单击Image 控件打开PhotoChooserTask 选择器,然后选取一张图片。我想在我的应用里使用这种模式,如图2 所示,但我不想每次做一个新的应用都要重复实现一次。起初我想通过继承扩展 Image 控件,无奈它是密封的,不能继承,只好把目光放在 Expression Blend 行为上。

图 2

我所期望的效果是这样的,假设我们有一个 ChoosePhotoBehavior 行为,把它从 Assets 面板拖到一个 Image 控件上,然后在 Properties 面板上把 PhotoUri 属性绑到视图模型的对应属性,并且把它设为双向绑定,如图 3 所示,这样,当用户单击 Image 控件时,就会打开 PhotoChooserTask 选择器,在用户选好图片之后,ChoosePhotoBehavior 行为就会更新 Image 控件,并把 PhotoUri 属性的值设为图片的路径,由于数据绑定是双向的,视图模型的对应属性也会随之更新。

图 3

接下来,我们一起看看这个 ChoosePhotoBehavior 行为是如何实现的。创建一个 ChoosePhotoBehavior 类,并使之继承 Behavior类,如代码 7 所示。Behavior的泛型参数用来指定这个行为适用于什么对象,这个对象的类型必须是 DependencyObject 类或其子类。如果你希望一个行为可以用在一个继承体系上,你可以泛型参数设为这个继承体系的基类。

代码 7

接着,创建一个 PhotoUri 依赖属性,如代码 8 所示。在 Visual Studio 里,你可以通过 propdp 这个代码段快速创建依赖属性。我希望 PhotoUri 依赖属性能在其值发生改变时自动刷新 Image 控件,因此,在注册 PhotoUri 依赖属性的时候通过 PropertyMetadata 指定负责刷新的方法。

代码 8

HandlePhotoUriPropertyChanged 方法会把改变之后的 PhotoUri 依赖属性的值传给 SetPhotoSource 方法,由 SetPhotoSource 方法负责具体的刷新工作,如代码 9 所示。在 ChoosePhotoBehavior 行为里,我们可以通过从 Behavior类继承过来的 AssociatedObject 属性访问 Image 控件。

代码 9

LoadPhoto 方法会读取指定位置的图片,然后返回 BitmapImage 对象,如代码 10 所示。由于图片的存放位置可能是应用的安装文件夹或者独立存储区,为了区分这两种路径,这里规定指向独立存储区的 URI 必须带有“isostore:”前缀,如“isostore:/photo1.png”,而指向安装文件夹的 URI 则和平时的表示方式保持一致,如“/photo1.png”。

代码 10

ChoosePhotoBehavior 行为的主要用途是在用户单击 Image 控件时打开 PhotoChooserTask 选择器,并把用户选取的图片显示在 Image 控件上。为此,我们需要订阅 Image 控件的 Tap 事件,而订阅该事件的最佳时机是在 OnAttached 方法里,如代码 11 所示。OnAttached 方法会在 Expression Blend SDK 把 ChoosePhotoBehavior 行为附加到 Image 控件的时候调用,因此,我们可以趁此机会初始化 Image 控件。此外,我们可以在 OnDetaching 方法里取消订阅 Image 控件的 Tap 事件,OnDetaching 方法会在 ChoosePhotoBehavior 行为和 Image 控件分离的时候调用。

代码 11

值得提醒的是,如果 PhotoUri 依赖属性的值是通过数据绑定获得的,那么 HandlePhotoUriPropertyChanged 方法会在 OnAttached 方法之后调用,因为 PhotoUri 依赖属性的值在创建 ChoosePhotoBehavior 对象的时候无法确定下来了,必须等到 ChoosePhotoBehavior 行为附加到 Image 控件之后才能确定。如果 PhotoUri 依赖属性的值是一个常量,那么 HandlePhotoUriPropertyChanged 方法会在 OnAttached 方法之前调用,因为 PhotoUri 依赖属性的值在创建 ChoosePhotoBehavior 对象的时候就能确定下来了。但是,由于此时 ChoosePhotoBehavior 行为还没附加到 Image 控件,AssociatedObject 属性的值是 null,这正是为什么 SetPhotoSource 方法(参见代码 9)要在刷新 Image 控件之前确保 AssociatedObject 属性的值不为 null。

当用户单击 Image 控件时,会设置并显示 PhotoChooserTask 选择器,如代码 12 所示。在用户选好图片之后,会把图片复制到独立存储区,刷新 Image 控件,修改 PhotoUri 依赖属性的值,如代码 13 所示。

代码 12

代码 13

由于修改 PhotoUri 依赖属性会导致 HandlePhotoUriPropertyChanged 和 SetPhotoSource 两个方法依次被调用,为了避免重复加载图片刷新 Image 控件,这里通过一个 _isChoosingPhoto 字段来表示是否处于选取图片的过程,如代码 14 所示。这样,当 PhotoUri 依赖属性的值是因为用户选取图片而改变时,SetPhotoSource 方法将会跳过刷新 Image 控件的代码。

代码 14

最后,为了配合 ChoosePhotoBehavior 行为产生的带有“isostore:”前缀的 URI,我特意创建了一个 UriToPhotoConverter 转换器,如代码 15 所示,以便图 1 的 Image 控件可以正确显示对应的图片。

代码 15

值得提醒的是,当我们把 Image 控件的 Source 属性绑到视图模型的某个字符串属性时,Silverlight 会帮我们完成从 String 到 ImageSource 的类型转换,但是,这仅限于指向安装文件夹的 URI,对于指向独立存储区的 URI,你要么自己读取图片并创建 BitmapImage 对象,要么通过转换器处理类型转换。

何时,何者?

附加属性和 Expression Blend 行为看似两种不同的扩展方式,实质上它们都是基于 Silverlight 的依赖属性系统,如果你查看图 3 生成的 XAML 代码,你会发现 ChoosePhotoBehavior 行为和 Image 控件之间隔着一个 Interaction.Behaviors 附加属性,如代码 16 所示。附加属性并不仅仅适用于 SelectedItems 附加属性这种简单情景,如果你细心观察 Silverlight for Windows Phone Toolkit 的 ContextMenu 组件,你会发现它也是通过 ContextMenuService.ContextMenu 附加属性实现的。

代码 16

自定义的附加属性只能通过手动编辑 XAML 来使用,因为 Expression Blend 并不认识它们,因此不会在属性面板上显示。Expression Blend 行为则不同,它是专为 Expression Blend 而设的,因此可以通过拖放的方式使用,此外,它的属性也能在属性面板上进行设置,尤其适合使用 Expression Blend 的前端设计师。

创建附加属性或者 Expression Blend 行为的一条重要原则是让它们用起来尽可能简单,但这不意味着它们本身也是简单的,它们承担着本该由用户自行处理的复杂性,换句话说,这些复杂性从它们的用户转移到它们的创建者。事物之所以看起来简单是因为与之相关的复杂性已经在内部被处理掉了,无论你开发一个组件还是一个产品,如果你没有做好心理准备迎接那些复杂性,那么这个组件或者产品的发展将会受到限制,因为它们无法承担用户希望摆脱的复杂性。

最后,不得不提的一点是,本文介绍的两个扩展组件可在 http://wputils.codeplex.com/ 下载,代码采用 MIT 开源协议。


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-09-12 00:003212

评论

发布
暂无评论
发现更多内容

Cisco ISR 1000 IOS XE 17.15.1a 发布下载,新增功能概览

sysin

Cisco 路由器 思科 ISR IOS XE

通过 Python 轻松获取 QQ 游戏排行榜数据:实时查看热门游戏排名

幂简集成

游戏 API

陶建辉演讲干货分享,AI 时代下的数据预测和数据处理挑战

TDengine

tdengine

面试官:如何实现线程池任务编排?

不在线第一只蜗牛

面试 线程池

行云管家应邀参加2024腾讯全球数字生态大会, 共创数字生态美好未来

行云管家

云计算 腾讯数字生态大会 数字生态

KubeCon China 回顾|快手的 100% 资源利用率提升:从裸机迁移大规模 Redis 到 Kubernetes

小猿姐

数据库 redis Kubernetes 云原生

KubeCon China 回顾|在没有专用 Operator 的情况下管理数据库集群

小猿姐

数据库 Kubernetes 云原生

Teams电话 中国语音解决方案

cts喜友科技

通信 通讯 云通讯 通信通讯

携手浙商证券、华锐技术,共话交易技术的创新与应用

非凸科技

淘宝API大全:淘宝商品详情数据接口

tbapi

淘宝商品详情数据接口 淘宝API接口

专访北极星资本:比特币矿业基金背后的努力

TechubNews

广西北海市等保测评机构有哪些?在哪里?

行云管家

网络安全 北海

KubeBlocks 如何降低管理多种数据库的学习门槛

小猿姐

数据库 云原生 operator kubernete

中国CRM的出路:先做好该做的

ToB行业头条

企业智能之旅(3): 构建智能企业的文化、组织、人才与能力

亚马逊云科技 (Amazon Web Services)

企业智能之旅(4): 智能企业为生成式 AI 做好数据准备

亚马逊云科技 (Amazon Web Services)

软件测试学习笔记丨Charles 安装及证书配置

测试人

软件测试

昇思多维混合分布式并行技术,助力伙伴快速复现Llama 3.1-405B大模型

极客天地

Nexpose 6.6.268 发布下载,新增功能概览

sysin

漏洞扫描 安全审计 漏洞管理 Nexpose

数据库运维实操优质文章文档分享(含Oracle、MySQL等) | 2024年8月刊

墨天轮

MySQL 数据库 oracle postgresql 国产数据库

Acrobat Pro DC 2021 (Win&Mac) 中文特别版

你的猪会飞吗

mac软件下载 Acrobat Pro DC mac mac破解软件下载

百度MEG数据开发治理平台-TDS

百度Geek说

数据治理 数据开发 任务调度 #大数据

深入浅出智能体工作流(Agentic workflow)

灵快科技

大模型 AIGC 多智能体 企业 AI 应用 AI 智能体

如何在扫码填写信息后,将数据实时推送给指定成员?

草料二维码

企业出海网络方案,助力TikTok直播

Ogcloud

海外直播专线 tiktok运营 tiktok直播 tiktok直播专线 tiktok直播网络

云手机能否全面替代传统手机?深入探讨云手机的优缺点

Ogcloud

云手机 海外云手机 云手机海外版 云手机群控 手机群控

解锁精准电商营销新纪元:深度剖析京东商品详情API数据驱动的营销策略

代码忍者

api 网关 API 测试

软件测试学习笔记丨Postman实战练习

测试人

软件测试

ETL数据集成丨MySQL到MySQL的数据迁移实践

RestCloud

MySQL 数据库 数据同步 数据迁移 ETL

【HDFS】集群出现大量的Under Replicated Blocks

扬_帆_起_航

论道WP(五):通过附加属性和行为扩展控件_微软_allenlooplee_InfoQ精选文章