带有备用客户端的 Silverlight JukeBox






4.91/5 (10投票s)
一篇关于如何将 Silverlight 2 与 Web 服务连接以创建具有浏览、搜索和下载功能的音乐库应用程序的文章。

目录
贷方
首先,我要感谢 UncleRedz 使本文得以实现。是他最初创建了 Silver JukeBox,并好心地与我分享了代码。基本上,服务器上的所有代码都由他编写,我所做的就是更改为 Northwind 数据库,添加下载专辑封面的可能性,并对接口进行一些修改,使其更适合我的客户端。感谢 UncleRedz 的分享以及他作为该应用程序的原始创建者。
摘要
我想陈述三项关于我个人性格的真实情况:
- 我不会唱歌,也不会演奏任何乐器(连三角铁都不会),但我热爱音乐。
- 音乐会影响我的情绪。
- 如果可以,我希望无论身在何处都能访问音乐。
前两个事实很难改变(当然,我可以参加三角铁课程,但我认为有些事情应该由专业人士来做),但第三个事实的修复会非常酷。我喜爱并聆听的音乐都存储在我的家庭电脑上;我能否无论身在何处都能访问这些音乐?答案当然是“是”,本文将展示如何做到这一点。
引言
该应用程序由两部分组成:
- 一台运行在我家电脑上的服务器,基于 WPF 和 WCF 构建,负责跟踪我的音乐。
- 一个在 Silverlight 2 中开发的客户端,具有浏览、搜索和下载音乐文件的功能。
我需要强调的是,这不是另一个媒体播放器。客户端不负责实际“播放”音乐;它只是允许“访问”音乐。在我的电脑上,Windows Media Player(以下简称 WMP)是默认的音乐播放器。当在客户端中选择一首曲目或一张专辑时,WMP 会弹出并开始播放,即客户端负责访问音乐,WMP 负责播放它。
我还想指出 Internet Explorer(以下简称 IE)和 Firefox 之间行为的差异,因为它会影响应用程序的用户体验。在 IE 中选择一首曲目或一张专辑时,它会立即传递给 WMP,让它进行缓冲并开始播放,即使文件尚未下载。然而,Firefox 会在将文件传递给 WMP 之前下载完整文件,这可能需要一些时间,具体取决于互联网连接。
必备组件
如果您安装了 Visual Studio 2008,您就没问题了;否则,您需要安装以下组件:
- .NET 3.5 Service Pack 1
- Microsoft SQL Server 2005
客户端
我大部分的 UI 编程都使用 WPF,并且非常渴望能够动手使用这个名为 Silverlight 的新的 Web 支持框架。WPF 和 Silverlight 之间的区别当然有很好的文档记录,但当我开始编写 Silverlight 代码时,我首先想到的就是创建自己的类作为 UI 控件的 DataContext
的想法,因为在 Silverlight 中,无法使用 ElementName
方法将数据绑定到依赖属性。起初,每当我必须创建一个新控件时,都会让我头痛欲裂,因为我知道我必须同时创建一个 DataContext
类(是的,Visual Studio,我认为每当我向项目中添加新 UI 控件时,您都应该自动创建一个 DataContext
类)。令人惊讶的是,在客户端开发过程中,我适应了这个想法,并开始喜欢它。您将数据和 UI 分开这一事实,不仅仅是通过在 XAML 和代码隐藏中进行(就像在 WPF 中那样),而是将其完全放在不同的类中,这是我可能会在 WPF 中采用的一种方法;即使它并非必需,除非您使用 MVVM 或类似的模式。
背景
在接下来的章节中,我将讨论设计开发,以及该应用程序为何会发展成如今的模样。
版本 0.1
最初的版本在客户端启动时会立即向服务器请求所有专辑。想法是将所有数据存储在客户端,然后使用分页专辑的概念,从而实现非常快速的分页,因为所有数据都已存在于客户端。这个想法还旨在实现即时搜索,也就是说,当您在搜索字段中键入文本时,搜索结果几乎会实时更新,这本来会非常酷。
但不幸的是,该设计存在很多糟糕之处,我不知道从何说起。首先,加载时间将完全取决于音乐库的大小,当考虑到可伸缩性时,这不是一个好的设计。我的 4 年旧电脑大约需要 4-5 分钟才能完全加载数据,您应该注意到服务器和客户端运行在同一台机器上,这当然意味着在网络上会更慢。
版本 1.0
如果我无法将所有数据都放在客户端,那么即时搜索功能就无法实现,因为当时我对实现 AJAX 或类似技术不感兴趣。接受并非所有专辑都存在于客户端这一事实后,我走向了游戏的另一端:如果客户端非常愚蠢怎么办?如果所有 CPU 和耗时操作都在服务器上进行怎么办?仔细想想,服务器计算机很可能比客户端更强大。如果客户端上显示的数据是客户端上唯一可用的数据怎么办?这意味着不仅分页必须在服务器上进行,搜索也必须如此。这是一个引人入胜的想法,也满足了可伸缩性的要求。
此设计的结果是,当以下情况发生时会从服务器请求数据:
- 客户端启动时。
- 显示新页面时。
- 在搜索字段中按 Enter 键时。
这些项目会在第 Web 服务接口章中描述的 IJukeBoxServer
接口的 GetAlbums(...)
方法中得到体现。
调试客户端
如果将 debugPage.html 设置为起始页,则可以调试客户端。

服务器
首先,让我快速浏览一下服务器的用户界面。主窗口包含两个按钮。按下Library(库)按钮会打开一个对话框,您可以在其中配置扫描音乐的文件夹;而 Server(服务器)则允许您配置服务器设置。还有一个活动日志,显示客户端发起的网络请求、专辑封面搜索等。

库配置几乎不值得一提,您只需将包含音乐文件的文件夹添加到服务器扫描的列表中。

相反,服务器设置对话框更有趣。它提供了以下功能:

Web 服务接口
服务器公开两个接口:IFileServer
和 IJukeBoxServer
。顾名思义,IFileServer
用于传输文件,例如 Silverlight 客户端本身。IJukeBoxServer
只包含一个负责在服务器和客户端之间传输专辑的方法。GetPolicy()
方法仅存在,因为应该允许跨域调用。有关跨域的更多信息可以在 此处找到。
[ServiceContract]
public interface IFileServer
{
/// <summary>
/// Gets the Silverlight policy file.
/// </summary>
[OperationContract, WebGet(UriTemplate = "/crossdomain.xml")]
Stream GetPolicy();
/// <summary>
/// Gets the HTML page holding the Silverlight client.
/// </summary>
[OperationContract, WebGet(UriTemplate = "/JukeBox.html")]
Stream GetHtmlPage();
/// <summary>
/// Gets the Silverlight client.
/// </summary>
[OperationContract, WebGet(UriTemplate = "/SilverlightClient.xap")]
Stream GetClient();
/// <summary>
/// Gets a playlist of a specific album.
/// </summary>
[OperationContract, WebGet(UriTemplate =
"/albumplaylist.wax?id={albumId}&base={basePath}")]
Stream GetAlbumPlaylist(int albumId, string basePath);
/// <summary>
/// Gets a specific track.
/// </summary>
[OperationContract, WebGet(UriTemplate = "/song?id={trackId}")]
Stream GetTrack(int trackId);
}
IJukeBoxServer
中的单个方法允许客户端满足其向用户显示专辑的要求。第一个参数 searchText
指定用户正在搜索的艺术家、专辑或曲目。第二个参数 skipCount
允许客户端通过跳过搜索结果匹配来分页专辑,第三个参数 resultCount
是在客户端设置的一个常量值,表示要返回的专辑数量,即客户端中每页存在的专辑数量。
[ServiceContract]
public interface IJukeBoxServer
{
/// <summary>
/// Gets albums matching a specified search text. The caller has also the
/// option to specify the number of albums to skip and take.
/// </summary>
[OperationContract]
SearchResultDto GetAlbums(string searchText, int skipCount, int resultCount);
}
数据结构
服务器使用 LINQ to SQL 将媒体相关数据存储在 Northwind 数据库中。LINQ 已成为 .NET Framework 功能中我最喜欢的一个;它写起来如此简洁优雅!之所以使用 Northwind 数据库而不是 SQL Server Compact 数据库(您可以像在 Visual Studio 中添加任何其他内容一样轻松地将它添加到项目中),是因为 LINQ to SQL。Compact 版的当前版本不支持 LINQ to SQL,由于我是 LINQ 的忠实粉丝,Compact 版并不是一个真正的选择。我个人也认为 Northwind 比 Compact 更快,但请不要因此攻击我,因为这是我个人的看法,没有任何科学数据支持。不过,您随时可以证明我是错的。等一下,那是在抛出战帖吗?我想是的……

数据库包含四张表:
- Track(曲目)表代表磁盘上的媒体文件。此表中的一项代表磁盘上的一个文件。
- 一个曲目不包含艺术家;它通过一个名为
ArtistId
的外键关系引用 Artist(艺术家)表中的艺术家。通过这种关系,我们指示数据库根据表演者来管理曲目。 - 我们以与分离艺术家相同的方式分离专辑,方法是通过一个名为
AlbumId
的外键关系,该关系从曲目指向 Album(专辑)表中的一个条目。这样,数据库就可以管理特定专辑包含哪些曲目。 - AlbumArtist(专辑艺术家)表通过定义
AlbumId
和ArtistId
这两个外键关系来连接艺术家和专辑。由于这些关系,查询特定专辑的艺术家非常快速。
使用 Amazon Associates Web Service 获取专辑封面
在客户端显示专辑封面的功能至关重要。花费了大量精力来调查可靠且高命中率的搜索服务。互联网上的大部分建议都指向一个解决方案,即 Yahoo 搜索引擎是最佳选择,但我认为应该有更好的解决方案。最终,我决定 Amazon Associates Web Service(以前称为 Amazon E-Commerce Service 或 ECS)是最佳解决方案,原因在于它提供了一个非常成熟的 API,我实际上可以在其中指定我正在搜索图像。他们还有非常、非常好的 API 文档。我开始实现文档化的 URL 请求,直到我偶然发现了 示例代码和库 部分,在那里您可以下载一个实现 URL 请求的 .NET 库。您只需通过以下方式描述您要搜索的内容:
public static Uri SearchForAlbumArt(string artistName, string albumName)
{
// Create the query
AmazonECSQuery query = new AmazonECSQuery("MyAccessKeyId", null, AmazonECSLocale.US);
// Create the request
ItemSearchRequest request = new ItemSearchRequest
{
Artist = artistName,
Title = albumName,
SearchIndex = "Music",
ResponseGroup = new List<string> { "Images" }
};
try
{
// Perform search on Amazon
ItemSearchResponse response = query.ItemSearch(request);
// Validate the response
if (
response.Items.Count > 0 &&
response.Items[0].Item.Count > 0 &&
response.Items[0].Item[0].ImageSets.Count > 0 &&
response.Items[0].Item[0].ImageSets[0].ImageSet.Count > 0)
{
return new Uri(response.Items[0].Item[0].ImageSets[0].ImageSet[0].MediumImage.URL);
}
}
catch (AmazonECSException e)
{
// Do something about the exception
}
return null;
}
正如您所见,使用 Amazon Web Service 的唯一缺点是,您需要注册一个免费的 Associates Web Service 帐户才能获得一个Amazon Access Key Id,您在创建查询时会指定它。服务器将 ID 存储为应用程序设置,使其可以包含在可下载的二进制文件中。
另一件很酷的事情是,您可以在查询中指定要使用的区域设置,也就是说,根据您在世界上的位置,您可以通过选择离您较近的区域设置来优化您的请求。我住在瑞典,然后可以选择英国、德国(DE)和法国(FR)而不是例如美国来加快我的请求。区域设置也定义为应用程序设置,使其可以包含在可下载的二进制文件中。
加密通信
可以通过在服务器配置对话框中启用 HTTPS 来加密服务器和客户端之间的通信。BasicHttpBinding
和 WebHttpBinding
的创建方式支持加密通信,但这样做它们依赖于计算机上安装的证书。如何在 下一章 中描述创建和安装证书的说明。
private bool StartWebService()
{
serviceHost = new ServiceHost(typeof(Session), ServiceUri);
// Create and configure the bindings, RPC and web/file delivery
BasicHttpBinding basicBinding = CreateBasicHttpBinding();
WebHttpBinding webBinding = CreateWebHttpBinding();
serviceHost.AddServiceEndpoint(typeof(IJukeBoxServer), basicBinding,
"SilverJukeBoxServer");
serviceHost.AddServiceEndpoint(typeof(IFileServer),
webBinding, "").Behaviors.Add(new WebHttpBehavior());
EnableMetadata(serviceHost);
try
{
serviceHost.Open();
// Web service successfully started
return true;
}
catch (AddressAlreadyInUseException)
{
// Unable to start the web service
return false;
}
}
private BasicHttpBinding CreateBasicHttpBinding()
{
BasicHttpBinding basicBinding;
// If we use HTTPS we need to enabled transport security
if (IsHttpsEnabled)
{
basicBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
}
else
{
// If we don't use HTTPS we need to enabled credential security if
// authentication is required.
basicBinding = new BasicHttpBinding(IsRequiringAuthentication ?
BasicHttpSecurityMode.TransportCredentialOnly : BasicHttpSecurityMode.None);
}
// Use standard windows authentication if enabled (Ntlm works with FireFox 2.0,
// Windows doesn't)
basicBinding.Security.Transport.ClientCredentialType = IsRequiringAuthentication ?
HttpClientCredentialType.Ntlm : HttpClientCredentialType.None;
// This should be better configured to match realistic worst case values.
basicBinding.ReceiveTimeout = TimeSpan.FromHours(2);
basicBinding.ReaderQuotas.MaxArrayLength = int.MaxValue;
basicBinding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
basicBinding.ReaderQuotas.MaxDepth = int.MaxValue;
basicBinding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue;
basicBinding.ReaderQuotas.MaxStringContentLength = int.MaxValue;
return basicBinding;
}
private WebHttpBinding CreateWebHttpBinding()
{
WebHttpBinding webBinding;
// If we use HTTPS we need to enabled transport security
if (IsHttpsEnabled)
{
webBinding = new WebHttpBinding(WebHttpSecurityMode.Transport);
}
else
{
// If we don't use HTTPS we need to enabled credential security if authentication
// is required.
webBinding = new WebHttpBinding(IsRequiringAuthentication ?
WebHttpSecurityMode.TransportCredentialOnly : WebHttpSecurityMode.None);
}
// Use standard windows authentication if enabled
// (Ntlm works with FireFox 2.0, Windows doesn't)
webBinding.Security.Transport.ClientCredentialType = IsRequiringAuthentication ?
HttpClientCredentialType.Ntlm : HttpClientCredentialType.None;
return webBinding;
}
创建和安装证书
创建和安装证书可能非常麻烦,但我创建了一些文件来帮助您。这些文件位于可下载源的Thirdparty文件夹中,也就是说,不包含在二进制文件中。
- Generate_certificate.bat 包含如何创建和安装证书的说明(Win32 OpenSSL Light 是先决条件)。
- httpcfg_add.bat 在 Generate_certificate.bat 中被引用,并包含如何将特定证书映射到计算机上某个端口的说明。
- httpcfg_remove.bat 与 httpcfg_add.bat 相反,也就是说,它将从计算机的某个端口移除证书映射。
Generate_certificate.bat 中的以下说明将为您安装证书,但由于您自己创建并签名了证书,因此它不会被 IE、Firefox 或您使用的任何浏览器视为有效。只有当以下所有陈述都为真时,证书才有效:
- 证书未过期。
- 证书的通用名称与 URL 匹配。
- 证书由认证机构颁发,例如 VeriSign。
Generate_certificate.bat 创建的证书将满足前两个陈述(假设输入了正确的通用名称),但第三个陈述仍然会失败。这将表现为浏览器中一个大的红色警告,您当然可以忽略它,并仍然浏览到客户端的用户界面。

不幸的是,您在 WMP 中不会获得该选项,WMP 相反会显示一条模糊的消息,并阻止您播放从客户端获取的音乐。

您需要将证书导入 Windows 的“受信任的根证书颁发机构”存储。最简单的方法是使用 IE 浏览到客户端,忽略证书警告,然后通过单击地址字段旁边的红色盾牌图标来查看证书属性。

假设您是以管理员身份运行的,证书属性对话框中有一个按钮可以安装证书。请确保将证书放置在受信任的根证书颁发机构中,然后重新启动 IE。
身份验证
可以通过启用 NTLM 身份验证来限制对客户端的访问,即 Windows 使用的用户管理。我自己创建了一个名为“JukeBox”的新标准用户,并在访问客户端时使用它。但是,在使其正常工作之前,您可能会遇到一些问题。首先,您用来访问客户端的用户帐户必须有一个密码,没有密码的用户将被立即拒绝访问。此外,如果您使用的是未加入域的 Windows XP 计算机,则必须禁用 ForceGuest。您可能会想,“ForceGuest”是什么?嗯,微软的说法是最好的:
“如果 Microsoft Windows XP 系统上启用了简单文件共享,并且该系统未加入域,那么所有通过网络访问该系统的用户都将被强制使用 Guest 帐户。这是“网络访问:本地帐户的共享和安全模式”安全策略设置,也称为 ForceGuest。”
这意味着即使您创建了一个新用户来访问客户端,仍然会使用 Guest 帐户,这当然会失败。请按照以下步骤禁用简单文件共享功能:
- 双击桌面上的我的电脑图标,或单击开始 - 我的电脑。
- 选择工具 - 文件夹选项。
- 选择查看选项卡。
- 清除使用简单文件共享(推荐)复选框。
您可能需要考虑在计算机上创建新用户,例如,您可能不喜欢新用户显示在 Windows 登录屏幕上,但接下来的章节将为您解决这些问题。
修复:从登录屏幕删除用户帐户
Silver JukeBox 的用户帐户不应显示在 Windows 登录屏幕上。此外,如果您错误地使用该用户登录 Windows,您的计算机上将创建一堆新文件和文件夹,这真的非常烦人,因为该用户的目的从来都不是真正登录 Windows。有关如何从登录屏幕删除用户的指南可以在 这里找到。
修复:启用自动登录
如果您计算机上只有一个用户,并且该用户没有密码,这意味着 Windows 会在您启动计算机时自动登录该用户,您可能从未见过 Windows 登录屏幕。一旦您创建了新用户,您将不再自动登录。如果您觉得登录屏幕是最烦人的事情,请随时按照 Windows XP 或 Windows Vista 的指南进行自动登录 Windows。
邮件发送的异常日志
碰巧的是,我远非一个完美软件工程师,因此,我想知道应用程序何时会崩溃。服务器配置为当发生崩溃时,使用 log4net 支持的 SmtpAppender
发送应用程序异常,前提是用户愿意。邮件包含最后 1024 行日志以及有关托管计算机的信息。典型的邮件如下所示:

我知道这是一个敏感问题,但我唯一的目的是提高应用程序的质量,仅此而已。如果您不想参与提高质量,请在应用程序询问时随意拒绝。
历史
- 2009 年 11 月 7 日
- 更新了源代码
- 1.0.1.0 (2009 年 6 月 20 日)
- 由于邮件发送的异常日志,更改了主机地址。
- 1.0.0.0 (2009 年 1 月 14 日)
- 初始版本