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

WPF Podcatcher 系列 – 第 3 部分 (The Podcast Management Conundrum)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (17投票s)

2008年3月20日

CPOL

6分钟阅读

viewsIcon

68694

这是关于一个用于播放互联网流式音频播客的 WPF 应用程序系列的第三篇文章。本文讨论了关于结构化皮肤化(structural skinning)问题的一个解决方案。

引言

本系列的上一篇文章介绍了我的 WPF 播客播放器 Podder 的第二个版本。它讨论了结构化皮肤化的概念,并展示了如何使用该模式创建无外观(look-less)的应用程序。本文回顾了我自己在实现结构化皮肤化支持时面临的一个棘手问题的解决方案。本文的主题假定您已经阅读了本系列的上一篇文章。

背景

Podder 允许用户在列表中添加和删除播客。播客列表会在应用程序运行时之间持久化,以便于查找和收听这些播客的节目。由于 Podder 可以使用任何用户界面来显示其数据和公开其功能,因此 UI 可能有很多种方式来显示添加/删除界面。

我创建的默认皮肤使用一个单独的对话框窗口来为用户提供播客管理功能。该对话框在下面的截图中

PodcastManagementDialog.png

Grant Hinkson创建的 Podder 皮肤没有单独的对话框窗口来公开此功能。他的皮肤允许用户直接在主窗口中添加和删除播客,如下所示

GrantPodcastManagement.png

上面图像中的红色圆圈指向用户可以添加和删除播客的位置。从可用性角度来看,Grant 的方法更有意义。正如交互设计大师Alan Cooper可能会说的那样,Grant 的方法遵循了“心智模型”,而我的方法过于贴近“实现模型”。事实证明,支持这两种方法相当棘手。

问题

Podder 的第一个版本,可在本系列的第一篇文章中找到,它将所有播客管理功能都嵌入到了 PodcastsDialog 类中。该窗口包含所有 CommandBinding,并拥有自己的 Controller 来处理用户输入。这在当时是可行的,因为 Podder 只有我的皮肤,当时 Grant 还没有参与到项目中。

当 Grant 开始制作他的 Podder 皮肤时,他几乎立即指出他不想有一个单独的对话框窗口来处理播客管理。这给我带来了一个问题。我需要找到一种通用的方法,让 UI 能够提供一种方式来提供播客 RSS feed URL,验证它,将有效的 feed URL 添加到应用程序的播客列表中,报告 feed 验证错误,以及删除播客。由于我的皮肤将此功能放在一个单独的窗口中,而 Grant 的皮肤则没有,因此应用程序无法对 UI 如何公开这些功能做出任何假设。

仅此一项就是一个棘手的要求,但问题并未就此停止。当在一个单独的对话框窗口中显示此功能时,我在两个地方显示了相同的 Podcast 对象列表。主窗口在 ComboBox 中显示列表,播客管理对话框在 ListBox 中显示列表。这两个控件都绑定到同一个对象列表,但如果用户在对话框窗口中选择了播客,则不应影响主窗口中选定的播客。防止这种情况发生的方法是为对话框窗口中的 ListBox 提供一个新的 ListCollectionView,这样它就不会修改绑定到主窗口 ComboBoxListCollectionViewCurrentItem

然而,在 Grant 的皮肤中,这个问题并不存在。他的皮肤没有两个控件显示相同的播客列表。Grant 皮肤中的 XamCarouselListBox 显示播客列表,它必须绑定到列表的默认集合视图。这是必需的,因为在列表中选择播客必须更新默认集合视图的 CurrentItem,以便其余 UI 与选定的播客保持同步。

总结一下这个问题,我需要创建一种方法来公开应用程序的功能,以便 UI 可以通过单独的窗口或主窗口来使用它。此外,当从单独的窗口使用时,UI 需要绑定到一个围绕播客列表的新集合视图,以便它不会干扰主窗口中选定的播客。

解决方案

Podder 的第一个版本将所有播客管理功能都放在一个单独的对话框窗口中,这是一个问题。那个问题的解决方案很简单;将必要的逻辑移到一个 UserControl 中。为此,我创建了 PodcastsControl 来处理应用程序中播客的添加和删除。下面是该 UserControl 的 XAML

<UserControl 
  x:Class="Podder.UI.PodcastsControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cmd="clr-namespace:Podder"
  Content="{DynamicResource VIEW_PodcastsControl}"
  >
  <UserControl.CommandBindings>
    <CommandBinding
      Command="{x:Static cmd:Commands.AutoDetectFeedUrlOnClipboard}"
      CanExecute="AutoDetectFeedUrlOnClipboard_CanExecute"
      Executed="AutoDetectFeedUrlOnClipboard_Executed"
      />
    <CommandBinding 
      Command="Delete" 
      CanExecute="Delete_CanExecute" 
      Executed="Delete_Executed" 
      />
    <CommandBinding 
      Command="New" 
      CanExecute="New_CanExecute" 
      Executed="New_Executed" 
      />
    <CommandBinding 
      Command="Open" 
      CanExecute="Open_CanExecute" 
      Executed="Open_Executed" 
      />
  </UserControl.CommandBindings>
</UserControl>

请注意,PodcastsControlContent 属性是通过动态资源引用设置的。这符合本系列上一篇文章中研究的结构化皮肤化技术。该控件仅建立了它公开的功能的 CommandBinding。每个 Podder 皮肤都提供了另一个动态加载到 PodcastsControl 中的 UserControl,并执行 PodcastsControl 正在监听的 RoutedCommand

PodcastsControl 有两个构造函数,如下所示

/// <summary>
/// This constructor is used when the PodcastsControl 
/// is placed directly on the main window.
/// </summary>
public PodcastsControl()
{
    InitializeComponent();
    _controller = new PodcastsControlController(this, null);
}

/// <summary>
/// This is used by the PodcastsDialog.
/// </summary>
/// <param name="podcastsView"></param>
public PodcastsControl(ListCollectionView podcastsView)
{
    InitializeComponent();
    _controller = new PodcastsControlController(this, podcastsView);
}

PodcastsControl 有一个 Controller 来处理用户交互。该 Controller 使用一个围绕应用程序播客列表的集合视图来了解 UI 中当前选定的播客。当应用程序的主窗口托管 PodcastsControl(如 Grant 的皮肤中)时,PodcastsControlController 使用一个围绕应用程序 Podcast 列表的默认集合视图。当托管在 PodcastsDialog 中时,它拥有一个对 PodcastsDialog 创建的新集合视图的引用。

这是 PodcastsDialog 构造函数,它创建一个新的集合视图和它托管的 PodcastsControl

public PodcastsDialog(PodcastCollection podcasts, Podcast selectedPodcast)
{
    InitializeComponent();            
    
    // Create a PodcastsControl and give it a new collection view
    // so that changes made in this dialog do not reflect in the main UI.
    ListCollectionView podcastsView = new ListCollectionView(podcasts);
    podcastsView.Filter = podcast => podcast is FavoriteEpisodes == false;

    if (podcastsView.PassesFilter(selectedPodcast))
        podcastsView.MoveCurrentTo(selectedPodcast);

    // Set the DataContext to our special collection view 
    // so that it takes effect in the dialog.
    base.DataContext = podcastsView;

    PodcastsControl podcastsControl = new PodcastsControl(podcastsView);
    base.Content = podcastsControl;

    Commands.AutoDetectFeedUrlOnClipboard.Execute(null, podcastsControl);
}

如上面的代码所示,PodcastsDialog 将其 DataContext 属性设置为它间接传递给 PodcastsControlController 的新集合视图。这确保了它托管的 PodcastsControl 绑定到 Controller 引用的同一个集合视图。当 PodcastsControl 显示在主窗口中时,所有这些都不需要,因为在这种情况下不需要单独的集合视图。

结论

设计一个支持结构化皮肤化的应用程序有其自身的挑战。它要求您以一种真正的 UI 无关的方式公开应用程序的功能。本文中看到的播客管理问题的解决方案就是一个例子。现在回顾一下,它似乎很简单,但在我找到解决方案之前,它是一个相当难啃的骨头。我花了一段时间才找到一个干净的解决方案,所以我认为写一篇文章来介绍它是有价值的。我希望我的解决方案对其他创建无外观应用程序的人有用,或者至少有趣。

修订历史

  • 2008 年 3 月 20 日 - 创建文章
© . All rights reserved.