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

使用 Silverlight 4 构建 Pandora 克隆

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (50投票s)

2010年11月23日

Ms-PL

23分钟阅读

viewsIcon

204906

downloadIcon

1

利用 Silverlight 4 构建一个有趣、真实的应用程序

Chavah screen capture

对于不熟悉的人来说,Pandora 是一种流行的基于 Flash 的互联网电台服务:用户通过提供一位艺术家或一首歌曲作为种子,并通过对播放的歌曲进行“赞”或“踩”来创建自己的电台。该软件会根据这些输入播放更多用户喜欢的音乐。

在本文中,我将向您展示如何使用 Silverlight 4、WCF 和 Entity Framework 构建一个类似 Pandora 的音乐服务。最终成果是 Chavah(“HA-vah”),一个有趣的 Pandora Silverlight 克隆应用,可以在 judahhimango.com/chavah 在线体验。

引言

您可能会问,为什么要构建一个 Pandora 克隆应用?

我有一个自私的动机:Pandora 不播放我最感兴趣的音乐。作为一名耶稣的犹太追随者,我喜欢一种名为弥赛亚犹太音乐的微小宗教音乐流派,这个流派太小了,以至于 Pandora 不知道我们任何的音乐。

我想,“为什么不构建一个 Pandora 克隆应用来播放一些优秀的弥赛亚犹太歌曲呢?”它主要服务于我自己,但也服务于我们社区中的其他人。而且,它还能提高人们对一些优秀弥赛亚音乐的认识。

但更普遍的动机是,“为什么不呢?”无论如何,它都是一次很棒的学习体验,让一个人接触使用 Silverlight、WCF 和 Entity Framework 的完整客户端/服务器应用程序。这些技术目前很受欢迎,并且受到雇主的需求,所以它也是一份很棒的简历素材。

出于这些动机,Chavah 项目诞生了。Chavah 是我尝试构建一个播放弥赛亚犹太音乐的 Pandora 式克隆应用,也是本文的主题。

为什么选择 Silverlight?

在 2010 年专业开发者大会上,微软大肆宣传 IE9 及其 HTML 5 支持,而对 Silverlight 却鲜有评论。因此,有人猜测 Silverlight 是否会淡出,转而支持 HTML 5;人们质疑为什么要使用 Silverlight 而不是 HTML 5。

虽然微软后来重申了他们对 Silverlight 的长期支持,但对于像 Chavah 这样的 Web 应用程序来说,**为什么**要使用 Silverlight 仍然是一个非常现实的问题。

我最终选择 Silverlight 的原因很实际:

  • Silverlight 比 HTML 5 具有更好的跨平台性。我的大多数目标受众仍然运行着对 HTML 5 支持有限或没有支持的浏览器。即使是少数拥有 HTML 5 兼容浏览器的用户,支持也并非跨浏览器一致,也不稳定。我的目标受众是 Windows 和 Mac 用户,而 Silverlight 目前在这两个平台上,在大多数任何 Web 浏览器中都能很好地运行。
  • 工具棒极了。Visual Studio + Blend 是一个难以匹敌的组合。C# 是一种很棒的语言,拥有出色的工具和重构支持。动画、路径、柔和的 UI 边缘、发光效果、阴影效果以及所有性感的 UI 元素,使用 Blend 都能轻松实现。
  • Silverlight 总是会领先 HTML 规范一步。等到 HTML 5 广泛普及之时,Silverlight 将拥有只有 HTML 6 才能带来的功能。这就是 HTML 规范委员会设计的天性。Silverlight 将不断创新并带来 HTML 缺乏的新功能,而且交付速度和普及范围都将超过 HTML 规范。
  • Silverlight 是一个应用程序开发平台;HTML 的初衷是用于文档。Chavah 是一个应用程序,而不是一个文档。HTML + Javascript + CSS 技巧可以将文档变成应用程序,但最终,一个为应用程序而构建的平台对于像 Chavah 这样的应用程序来说更具吸引力。
  • 由于 Chavah 是一个应用程序,我希望用户能够将 Chavah 本地安装到他们的桌面。凭借 Silverlight 的离线功能,用户可以选择将 Chavah 安装为本地应用程序。这在 HTML + CSS + Javascript 中是不可能实现的,除非使用特定于浏览器的 hack,如 IE9 的“固定网站”或 Google Chrome 的“应用程序快捷方式”。

\

关注点

在展示 Chavah 如何构建时,本文将向您展示如何模拟 Pandora 等应用程序的外观和感觉——流畅的 UI 布局、动画、流畅的体验——所有这些都在 Silverlight 4 中实现。我将向您展示一个真实世界的应用程序如何利用 Silverlight 中的一些新功能,例如离线功能、缓动动画和 GPU 加速。

此外,我们还将介绍与后端 WCF Web 服务通信,其中包含“根据用户的喜好选择歌曲”的逻辑。

我将向您展示如何使用 WCF、Entity Framework 和 SQLite 为您的应用程序添加一些有趣的社交功能。

此外,我还会涉及一些即将推出的热门技术——响应式扩展 (Reactive Extensions) 和代码契约 (Code Contracts)——并提供我对它们在现代应用中的可行性和实用性的看法。

需要注意的一点是:尽管 Chavah 的灵感来源于 Pandora,但请注意,与 Pandora 不同的是,Chavah 不使用音乐基因组计划来预测您会喜欢哪种音乐。这超出了本项目的范围。Chavah 会先播放所有歌曲,直到您开始对歌曲进行“赞”和“踩”操作,此时它会使用一种特殊的算法根据您的偏好选择歌曲。我们将在本文后面介绍这种更简单的算法。

Using the Code

说到视觉效果,Pandora 的核心是屏幕右侧出现的单个歌曲**磁贴**,它们流畅地移动到位。一旦到位,歌曲就开始播放。用户可以暂停歌曲、跳过歌曲、赞歌曲或踩歌曲。

Pandora's tile animation

这是我们要解决的第一部分——我们如何构建像上面图片中 Pandora 歌曲磁贴那样的 UI 控件?以及我们如何像那样流畅地将它移动到位?

构建歌曲磁贴

首要任务是构建一个歌曲磁贴。

作为 Silverlight 可扩展 UI 框架的证明,我们将完全使用 Silverlight 框架中的内置控件来构建歌曲磁贴控件。我们将为某些组件定制外观和感觉,但这里没有使用任何特殊的第三方工具包,所有功能都内置其中。

控件的轮廓非常简单

Chavah song tile XAML outline

正如您所看到的,这里没有什么太花哨的东西:我们有一个 `Border` 容纳整个磁贴。实现圆角效果很简单,只需设置 Border 的 `CornerRadius` 属性即可。

此 UI 中唯一需要付出努力的部分是“赞”和“踩”按钮。从功能上讲,它们是 `RadioButtons`:如果一个被选中,另一个就会取消选中。(也就是说,您不能同时“赞”和“踩”一首歌。)

显然,内置 `RadioButton` 的外观和感觉不适用。我们需要改变单选按钮的外观以显示点赞/点踩。

为了实现这一点,您可以为单选按钮构建一个自定义 ControlTemplate。对于 Chavah 的“赞”按钮,它非常简单:

<ControlTemplate x:Key="ThumbUpStyle" TargetType="RadioButton">
  <Grid>
      <Image x:Name="imageUnchecked" Width="16" Height="16" 
            Source="http://judahhimango.com/Chavah/asyncImages/thumbUp.png" />
      <Image x:Name="imageChecked" Width="16" Height="16" 
           Opacity="0" Source="http://judahhimango.com/Chavah/asyncImages/thumbUpChecked.png" />
      <VisualStateManager.VisualStateGroups>
          <VisualStateGroup x:Name="CheckStates">
              <VisualState x:Name="Checked">
                  <Storyboard>
                      <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="imageChecked" 
                           Storyboard.TargetProperty="(UIElement.Opacity)" To="1" />
                  </Storyboard>
              </VisualState>
              <VisualState x:Name="Unchecked">
                  <Storyboard>
                      <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetName="imageChecked" 
                            Storyboard.TargetProperty="(UIElement.Opacity)" To="0" />
                  </Storyboard>
              </VisualState>
          </VisualStateGroup>
      </VisualStateManager.VisualStateGroups>
  </Grid>
</ControlTemplate> 

控件模板定义了 2 个图像:“选中”的赞和“未选中”的赞。当 RadioButton 从选中到未选中反之亦然时,我们只需使用不透明度动画隐藏一个图像并显示另一个。最终结果是一个外观不错的赞控件,在选中状态之间切换时具有很好的动画效果。

ChavahThumbUp.png

Silverlight 的控件模板使其非常简单地创建了一个类似于 Pandora 点赞控件的自定义单选按钮。

您会注意到在赞和踩按钮之间有一个数字。这是我们稍后会介绍的一个有趣社交功能的一部分。

将歌曲磁贴动画到指定位置

我之前提到,当 Pandora 播放歌曲时,歌曲磁贴不会突兀地突然出现,而是磁贴的边缘出现在屏幕右侧,并流畅地移动到指定位置。

这种效果是构建流畅 UI 的更广泛努力的一部分,在软件开发领域,尤其是在富互联网应用领域,这种努力越来越受欢迎。

我们如何在 Silverlight 中实现相同的效果?

我的第一个想法是使用 Silverlight 内置的动画系统来动画磁贴的边距:将歌曲标题的边距设置为一个大数字,使其超出屏幕,然后将其动画到指定位置。

在 WPF 中,我们可以通过使用 ThicknessAnimation 来动画歌曲控件的边距来实现此目的。

不幸的是,截至 Silverlight 4,Silverlight 没有 ThicknessAnimation。

咨询了强大的谷歌大神们后,我发现其他人通过使用自定义 ThicknessAnimation 解决了这个问题。我用一些缓动动画进一步定制了它,稍后会详细介绍。

public class ThicknessAnimation 
{ 
   // The time along the animation from 0-1
    public static DependencyProperty TimeProperty = DependencyProperty.RegisterAttached("Time", 
      typeof(double), typeof(DoubleAnimation), new PropertyMetadata(OnTimeChanged));
    // The object being animated
    public static DependencyProperty TargetProperty = DependencyProperty.RegisterAttached("Target", 
      typeof(DependencyObject), typeof(ThicknessAnimation), null);
    // The thickness we're animating to
    public static DependencyProperty FromProperty = DependencyProperty.RegisterAttached("From", 
      typeof(Thickness), typeof(DependencyObject), null);
    // The tickness we're animating from
    public static DependencyProperty ToProperty = DependencyProperty.RegisterAttached("To", 
      typeof(Thickness), typeof(DependencyObject), null);
    // The target property to animate to.  Should have a property type of Thickness
    public static DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
      "TargetProperty", typeof(DependencyProperty), typeof(DependencyObject), null);
    
    public static Timeline Create(DependencyObject target, DependencyProperty targetProperty, 
      Duration duration, Thickness from, Thickness to)
    {
        DoubleAnimation timeAnimation = new DoubleAnimation() { From = 0, To = 1, Duration = duration };
        timeAnimation.EasingFunction = new ExponentialEase() { Exponent = 9, 
          EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut };
        timeAnimation.SetValue(TargetProperty, target);
        timeAnimation.SetValue(TargetPropertyProperty, targetProperty);
        timeAnimation.SetValue(FromProperty, from);
        timeAnimation.SetValue(ToProperty, to);
        Storyboard.SetTargetProperty(timeAnimation, new PropertyPath("(ThicknessAnimation.Time)"));
        Storyboard.SetTarget(timeAnimation, timeAnimation);
        return timeAnimation;
    }
 
    private static void OnTimeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        DoubleAnimation animation = (DoubleAnimation)sender;
        double time = GetTime(animation);
        Thickness from = (Thickness)sender.GetValue(FromProperty);
        Thickness to = (Thickness)sender.GetValue(ToProperty);
        DependencyProperty targetProperty = (DependencyProperty)sender.GetValue(TargetPropertyProperty);
        DependencyObject target = (DependencyObject)sender.GetValue(TargetProperty);
        target.SetValue(targetProperty, new Thickness((to.Left - from.Left) * time + from.Left,
                                                      (to.Top - from.Top) * time + from.Top,
                                                      (to.Right - from.Right) * time + from.Right,
                                                      (to.Bottom - from.Bottom) * time + from.Bottom)); 
    }
 
    public static double GetTime(DoubleAnimation animation)
    {
        return (double)animation.GetValue(TimeProperty);
    }
 
    public static void SetTime(DoubleAnimation animation, double value) 
    { 
        animation.SetValue(TimeProperty, value); 
    }
}   

使用 Silverlight 缓动动画尽情发挥

请注意创建动画的代码,您会发现我们正在使用 Silverlight 新的缓动动画。

timeAnimation.EasingFunction = new ExponentialEase() 
{ 
   Exponent = 9, 
   EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut 
}; 

缓动动画是一种动画,顾名思义,它从源状态到目标状态平滑过渡。缓动动画很重要,因为它有助于为应用程序构建流畅的感觉。

如果没有动画,歌曲磁贴凭空出现会让人感到突兀。

使用普通的动画,歌曲磁贴的滑入会显得机械。

只有使用缓动动画,我们才能实现那种让用户惊叹不已的温暖、模糊、粘稠的流畅感。

缓动动画由两部分组成

  1. 缓动函数
  2. 缓动模式

对于**缓动函数**,您有一系列令人印象深刻的选择(鸣谢 Mike Snow

  • BackEase – 元素先向后移动由其振幅指定的量,然后再向前移动。
  • BounceEase – 创建类似弹跳元素的效果。
  • CircleEase – 基于圆形函数加速动画。
  • CubicEase – 基于三次函数加速动画。
  • ElasticEase – 使用弹性与震荡进行动画。
  • ExponentialEase – 基于指数值加速动画。
  • PowerEase – 基于时间的幂加速动画。
  • QuadraticEase – 基于时间的平方加速动画。
  • QuarticEase – 基于时间的三次方加速动画。
  • QuinticEase – 基于时间的五次方加速动画。
  • SineEase – 沿着正弦波加速动画。

而对于**缓动模式**,

  • EaseIn – 动画开始时缓慢,然后加速到全速
  • EaseOut – 动画开始时全速,然后减速到结束
  • EaseInOut – 动画开始时缓慢,加速到全速,然后减速到结束

对于 Chavah,我们将使用具有 EaseOut 模式的 ExponentialEase 函数。结果是歌曲磁贴迅速出现在 UI 中,然后慢慢地回到原位。既美观又流畅。用户看到后会惊叹不已。

使用动画

现在我们已经完成了定义 ThicknessAnimation 的艰巨工作,并且对缓动动画有了深入的了解,实际使用动画简直是小菜一碟。

创建一个故事板,将我们的厚度动画添加到故事板中,然后 .Begin() 动画。

var animationTime = TimeSpan.FromSeconds(1.5); 
var timeline = ThicknessAnimation.Create(control, SongControl.MarginProperty, 
   new Duration(animationTime), new Thickness(5, 5, -150, 5), new Thickness(5, 5, 5, 5)); 
Storyboard storyBoard = new Storyboard(); 
storyBoard.Children.Add(timeline);
storyBoard.Begin();  

我们将歌曲磁贴的右边距从 -150(例如,推到屏幕右侧)动画到 5,这样当歌曲添加到 UI 时,它会移动所有现有歌曲磁贴,平滑地为新的 UI 腾出空间。流畅而优美。

通过这些效果,我们模仿了 Pandora 的歌曲磁贴控件,包括其圆角外观和流畅动画,开发人员付出的努力非常少。纯粹的成功!

将 Silverlight 应用程序安装到用户桌面

由于 Chavah 是一个应用程序,我希望用户能够从他们的桌面运行该应用程序,而无需处理浏览器、地址、书签等问题。他们应该能够在不启动网络浏览器的情况下运行我的应用程序。

Silverlight 的一个相对较新的功能是“脱离浏览器”支持。通过“脱离浏览器”,您可以让用户有机会安装您的应用程序,并可选择在桌面和开始菜单上创建快捷方式。

对于 Chavah,我希望安装过程完全可选,并且从用户体验的角度来看,是隐蔽的。对于许多用户来说,直接跳出来的安装提示是可怕的,通常是桌面软件的一个痛点。我的目标受众可能不熟悉软件,因此我不想用安装提示吓跑他们。

为了避免那些令人害怕的安装提示,Chavah 只有在用户特意点击一个不引人注目的、微妙的链接时才会安装。

InstallChavah.png

我们为该链接提供了一个工具提示:“将 Chavah 安装到您的桌面”。

点击该链接将触发安装代码。Silverlight 的离线安装代码非常简单。

if (!Application.Current.IsRunningOutOfBrowser && 
       Application.Current.InstallState != InstallState.Installed)
{
   var success = Application.Current.Install();
   …
}   

`Application.Current.Install()` 方法就足够了。调用后,Silverlight 将提示用户安装。令人担忧的安装提示来了。

InstallChavahPrompt.png

还不错。再说一次,只有当用户特意尝试安装时,他们才会看到这个。

安装体验很好:安装只需几毫秒,提示没有关于我的应用程序会损害用户系统的可怕红色叉号,用户除了点击“确定”之外无需做任何操作。

这与典型的安装形成对比,典型的安装中,您会看到管理员权限提示,一个可怕的屏幕询问您是否确定要下载一个会摧毁您 C:\ 驱动器的 .exe 文件,一个多步骤的安装向导,EULA 协议,安装目录选择,安装组件自定义,以及其他多余的**东西**,这些都让用户害怕在 Windows 上安装桌面应用程序。

是的,Silverlight 在这方面有所进步,感谢上帝。它接近于 iPhone/iPad 应用程序安装的无痛、无摩擦体验。

如果用户点击“确定”安装,Chavah 将像典型应用程序一样安装,并在开始菜单和桌面创建快捷方式。

ChavahStartMenu.png

此外,该应用程序会像典型的原生应用程序一样显示在已安装程序列表中。

UninstallChavah.png

如果用户从桌面或开始菜单运行该应用程序,它看起来就像一个标准的桌面应用程序,无需浏览器边框。

ChhavahOutOfBrowser.png

借助 Silverlight 的离线功能,您可以将您的 Web 应用程序转变为桌面应用程序,具有原生窗口边框、开始菜单和桌面快捷方式、应用程序图标以及您对正常安装所期望的一切,而无需担心安全问题。

完美,正是我对 Chavah 的期望。

利用您的 500 美元显卡

与仅限 Windows 且完全访问 DirectX 和所有本地硬件功能的 WPF 不同,Silverlight 主要在 CPU 上运行。

但随着 Silverlight 的最新添加,您现在可以将部分 UI 卸载到 GPU,以获得更好的渲染性能。

关于如何做到这一点,网络上存在大量困惑和错误信息。(希望我没有加剧这种情况,干杯!

大多数人似乎认为,将“EnableGPUAcceleration=true”参数传递给 HTML 页面中的 Silverlight 对象就足够了。

这是一个错误的假设。

要使用 GPU,必须发生两件事:

  • 将“EnableGPUAcceleration=true”参数传递给 HTML 页面中的 Silverlight 对象。(如果您正在离线运行,您还需要在 Visual Studio 的离线发布设置中设置“启用 GPU 加速”选项。)
  • 将所有您希望通过 GPU 加速的 UI 元素(或顶级元素)的 CacheMode 设置为 BitmapCache。

对于 Chavah,我希望歌曲磁贴能够进行 GPU 加速;我正在对它们进行动画处理,我正在改变它们的不透明度等等。这似乎是 GPU 加速的好选择。

为此,我做了以下更改:

在包含 Chavah 应用程序的 HTML 文档中,我传入了指示 GPU 加速的参数。

<object data="data:application/x-silverlight-4," 
           type="application/x-silverlight-4" width="100%" height="100%"> 
     <param name="EnableGPUAcceleration" value="true" />
     … 
</object> 

此外,我已经在我们的歌曲磁贴控件上设置了 CacheMode。

<UserControl x:Class="JudahHimango.Chavah.SongControl"
   ...
   CacheMode="BitmapCache"
   ...
</UserControl>  

此外,由于我们可能会在浏览器外运行,我们也需要在那里启用 GPU 加速。您可以通过 Silverlight 项目的属性页面完成此操作。

ChavahOutOfBrowserGpuAcceleration.png

尽管 Silverlight GPU 加速非常出色,但并非一切都完美无缺;截至 Silverlight 4,仍有一些重要的注意事项:

某些 UI 组件**无法**进行硬件加速

  • 效果(模糊、阴影等)
  • 可写入位图 (WriteableBitmap)
  • 透视变换 (PerspectiveTransforms)
  • 非矩形裁剪
  • 不透明度蒙版 (OpacityMasks)

此外,一些硬件/操作系统组合根本无法进行 GPU 加速。

  • 在 Windows XP 上,如果您的显卡不是来自 NVidia、ATI 或 Intel,或者您的驱动程序日期早于 2004 年 11 月,您将无法获得任何 GPU 加速。

这些限制并不可怕。唯一影响 Chavah 的是效果限制——我们大量使用了 DropShadowEffect,这些效果将不会被加速。也就是说,屏幕上同时出现的效果很少,所以 CPU 的负载很小。事实上,在运行时,Chavah 通常运行在我 2.8 GHZ CPU 的 < 1% 资源下。一点也不差。

那么,有了所有这些注意事项和在 Silverlight 应用程序中实现 GPU 加速的步骤,您如何知道它是否正常工作?您如何知道您已正确设置所有内容,并且您想要的元素正在通过 GPU 管道?

答案是通过一个您可以发送给 HTML 中 Silverlight 对象的调试参数提供的。

<param name="enableCacheVisualization" value="true" />

启用该调试参数后,Silverlight 将对每个未进行硬件加速的元素着色。运行 Chavah 时,您会看到我们的歌曲磁贴确实得到了加速。

ChavahGpuTint.png

Silverlight 已对所有未加速的元素着色。正如您所看到的,歌曲磁贴没有着色(忽略已播放歌曲的不透明度)。这表明 Silverlight 正在通过 GPU 硬件加速管道发送这些 UI 元素位图缓存。

有关 Silverlight GPU 加速功能的更多信息,我建议您阅读 MSDN 的综合文章

社交云协作 = 流行词过载(但功能很酷)

对于 Chavah,我希望比 Pandora 更进一步,通过软件促进社区意识。Chavah 面向的是一个小众社区,如果能有一些功能让社区共同赞扬好歌曲、淘汰差歌曲,那不是很好吗?

特别是,我想为 Chavah 添加一些社交功能,以促进社区发展并提高人们对优秀弥赛亚犹太音乐的认识:

  • 弥赛亚社区每首歌的综合排名(赞 = +1,踩 = -1)
  • 弥赛亚社区排名靠前的歌曲
  • 弥赛亚社区热门歌曲(最近被赞的歌曲)
  • 总播放歌曲数

当然,我们希望 Chavah 能够智能地播放更多用户喜欢的音乐,减少播放用户不喜欢的音乐。

为了实现这些功能,我在后端使用了 WCF Web 服务。这很好用;Silverlight 可以生成一个不错的异步 API 来执行服务器上的调用并返回结果,而不是编写代码与服务器通信。

对于服务器上的数据存储,因为 Chavah 不需要大规模,所以我不需要完整的 SQL Server。即使是 SQL Express 也超出了我的需求。

我真正想要的是一个影响小、单文件数据库,无需服务。另外,我不想手动将对象映射到数据库;我想要某种 ORM 在上面,使它变得非常容易。

虽然我曾考虑过使用 No-SQL,但最终对我奏效的是使用免费的 SQLite 数据库。其 ADO.NET 驱动程序也是免费的:System.Data.SQLite

对于对象关系映射,我希望避免手动编写 ADO.NET 数据读取器代码。我惊喜地发现 Entity Framework 可以与 SQLite 配合使用,并提供完整的设计时支持。Entity Framework 将为您生成所有映射代码,让您可以编写简单的 LINQ 查询来获取数据。完美。

最终结果是这样干净整洁的代码

public Song[] GetTrendingSongs(int count)
{
   using (var entities = new ChavahEntities())
   {             
      var trendingSongs = from like in entities.Likes
                          where like.LikeStatus == true
                          orderby like.Id descending
                          select like;
                   
      var songs = from like in trendingSongs
                  where like.LikeStatus == true
                  join song in entities.SongInfos on like.SongId equals song.Id
                  select song;
                   
      return songs                       
        .Take(count)
        .ToArray();         
   }
} 

看啊,妈妈,没有丑陋的 SQL 字符串!只有性感、智能感知友好的 LINQ。

由于 Entity Framework 以及 Silverlight 和 WCF 之间良好的集成,构建热门歌曲、社区排名、评分最高歌曲等社交功能变得轻而易举。

绝密歌曲选择算法

嘘。过来。对,就是你。你能保守秘密吗?好吧。

我不是算法向导,但我能够想出(阅读:从软件社区偷来)一种根据用户的喜好和厌恶选择歌曲的算法。

值得再次强调的是,与 Pandora 不同,Chavah 不使用音乐基因组计划来预测您会喜欢的音乐。相反,Chavah 使用一种更简单的算法,主要播放您喜欢的音乐,一些您未评分的音乐,并且很少播放您不喜欢的音乐。

引入歌曲权重算法。

为每首歌分配一个权重

  • 喜欢的歌曲获得高权重(例如 5)
  • 未排名的歌曲获得正常权重(例如 1)
  • 不喜欢的歌曲获得低权重(例如 0.01)

要选择下一首要播放的歌曲,我们排好所有歌曲,将它们的权重相加,然后选择一个介于 0 和总和之间的随机数。

例如,假设我们的整个歌曲库中只有 3 首歌曲:Shalom、Blessings 和 King。

如果用户喜欢 Shalom,Blessings 未评分,King 不喜欢,我们的算法会为 Shalom 分配 0 到 5,为 Blessings 分配 5 到 6,为 King 分配 6 到 6.01。我们将生成一个介于 0 到 6.01 之间的随机数。如果随机数是 3,我们将播放 Shalom。如果是 5.5,我们将播放 Blessings。如果是 6.01,我们将播放 King。

简单但有效。结果是,喜欢的歌曲比普通歌曲更有可能播放,而普通歌曲比不喜欢的歌曲更有可能播放。此外,如果用户没有评价很多歌曲,他仍然会得到一个喜欢的歌曲和未评价歌曲的良好组合。控制“多多少”阈值只是调整喜欢、未评价和不喜欢的歌曲的权重常数的问题。

CPian 特色甜点:代码契约奶油,配 Rx 樱桃

虽然与构建 Pandora 克隆应用没有直接关系,但我想谈谈两个在未来 5 年内可能对 .NET 软件世界产生影响的主题:代码契约 (Code Contracts) 和响应式扩展 (Rx) 项目。在像 Chavah 这样的真实 Silverlight 应用中使用它们一直很有启发性,我希望将我的开悟的禅宗智慧传递给你们这些优秀的 CPian 们。

响应式扩展 (Rx)

响应式扩展 (Rx) 框架将 LINQ 的思想应用于事件。这里的“事件”不仅指标准的 .NET 事件,而是指任何以异步、不可预测方式传入的东西:队列消息、鼠标点击、按键、用户手势、对象更改通知、服务器消息,等等。

“响应式”是关键:Rx 不是像使用 LINQ 和 IEnumerable 那样轮询数据,而是让您通过观察 IObservable<T>(.NET 4 中的一种新类型)来**响应**数据的变化。

Rx 目前是微软的一个研究项目,但它的一些部分已经集成到 .NET 框架中。Rx 项目背后的几位杰出人物——Wes Dyer、Bart deSmet、Eric Meijer——是为我们带来 LINQ 的几位才华横溢的智者,LINQ 已被证明取得了巨大成功。

Rx 真正闪光的地方在于组合。想象一下拖放处理代码:你可能需要存储鼠标第一次按下的位置。你会监听鼠标按下事件。你会监听鼠标移动事件。你会监听鼠标抬起事件。你会协调整个过程。当你完成时,会有一大堆事件处理程序、可变状态和普遍的丑陋;你已经看不到森林,只看到树木了。

相比之下,Rx 可以通过一个单一的声明式调用将 MouseUp、MouseDown、MouseMove 事件组合在一起:瞧!您已经实现了拖放功能。

在 Chavah 中,我使用 Rx 观察我的视图模型的变化,从而取代了重复的更改通知管道。我还使用它在组件之间进行松散通信:我不是让视图模型 1 与视图模型 2 对话,而是向中介发布消息,然后感兴趣的各方可以组合和处理该消息。例如,要监听我们何时开始播放 Shalom 歌曲,我可以这样写:

// Use Rx to filter and react to messages, listening for the "Shalom" song to play.
mediator.Messages
 .OfType<SongPlayingEventArgs><playsongeventargs>()
 .Where(s => s.Name == “Shalom”)
 .Subscribe(OnShalomPlaying);

Rx 将组合式 LINQ 的强大功能与事件结合起来。事实证明,这是一个强大的组合,在这个 Silverlight 项目中非常有用。

就像 LINQ 改变了我们处理数据的方式一样,我预测 Rx 将改变我们处理事件的方式。我现在已经到了这样的程度:很难看待一个问题而不将其视为对事件流的操作。

我建议微软在近期将 Rx 提升为 .NET 框架的一等公民。

代码契约

代码契约是 .NET 4 框架(和 Silverlight 4)的新增功能。它源于微软的另一个研究项目:Spec# 语言,旨在融合 C# 和契约式设计原则。然而,代码契约选择了语言无关的路径,仅作为 .NET 框架中的 API 实现,没有任何语言支持。

代码契约的概念并不新鲜:契约式编程,开发人员直接在代码中指定所需条件——关于方法、类、接口。然后,一个工具在编译期间运行,以验证您的条件永远不会被违反。

理论上,最终结果是代码错误更少,因为您的所有假设现在都已编码为契约并由工具验证。不再有 NullReferenceExceptions,不再有 ArgumentExceptions——那该多好?

在 Chavah 中,我主要在 Silverlight 端使用代码契约,结果喜忧参半。偶尔,代码契约会捕获到一个 bug——这很棒!但有时我写了很多契约,却感觉不到爱。

你看,当你控制整个 API 时,代码契约才能很好地工作。例如,如果你的整个项目都使用利用代码契约的 API,你将获得很棒的体验,并且代码中的错误会更少。

但这本质上就是问题所在:要让代码契约真正**发挥作用**,必须将整个“大海”煮沸。也就是说,所有你调用的 API 都需要使用代码契约,否则你会听到很多噪音。

尽管微软拥有“大海”,但他们并没有做太多“煮沸”的工作。

现实是,.Net 框架的很大一部分缺少契约;我个人遇到了一些并非不常见的 Silverlight API,它们明显缺少契约。结果,我不得不编写代码来向契约检查器保证某个 .NET API 确实会返回一个非负数。

此外,代码契约框架目前还没有广泛支持,这又带来了更多噪音。例如,Chavah 使用 Ninject 控制反转框架进行依赖解析和注入,但 Ninject 没有契约,所以为了避免噪音,我不得不再次编写更多代码来向契约工具保证天并没有塌下来。

代码契约的另一个困难是语言方面没有支持。C# 对代码契约不友好。例如,在接口上放置契约时,你必须费尽周折并编写更多琐碎的代码。另一个例子是契约检查器不理解 C# 构造,例如:

private readonly string foo = “bar”; 

“哦不!”契约检查器说,“foo 可能是 null!”

呃……不,foo 只被赋值一次,但契约检查工具因为对自己不自信,所以喋喋不休地说 foo 可能是 null。再一次,C# 和代码契约彼此没有爱,他们的婚姻问题都转嫁给了你,开发者。

底线:代码契约有潜力,但在获得广泛框架支持之前,它会产生太多噪音。此外,它不够智能,无法识别某些事物不会为 null。(我真的必须检查某个事件处理程序的 EventArgs 不为 null 吗?真的吗?)一些语言集成也会有所帮助。

摘要

用 Silverlight 构建一个类似 Pandora 的音乐应用程序出奇地简单。我在一个周末内就写出了第一个版本。

Silverlight 结合 WCF 集成,可以用于创建在 Web 或桌面上运行良好的客户端/服务器应用程序。它是构建在 Web 上运行的应用程序的不错选择。

即使对于像我这样对设计有困难的程序员来说,在 Silverlight 中构建漂亮、流畅的 UI 也很自然。控件模板、数据模板、缓动动画和命令支持,用很少的代码就能创建出精美的 UI。

Entity Framework 正在成熟。将 Entity Framework 与 WCF 结合使用,让将数据返回给客户端变得异常简单。

© . All rights reserved.