Introduction
The previous article in this series unveiled the second version of my WPF podcast player, called Podder. It discussed the idea of structural skinning, and showed how to create look-less applications using that pattern. This article reviews the solution to a difficult problem I faced while implementing support for structural skinning. The subject matter of this article is presented under the assumption that you have already read the previous article in this series.
Background
Podder allows the user to add and remove podcasts to and from a list. That list of podcasts is persisted between runs of the application, so that it is easy to find and listen to episodes from those podcasts. Since Podder can have any user interface to display its data and expose its features, there are many ways that the UI could potentially display the add/remove interface.
The default skin, which I created, uses a separate dialog window to provide podcast management to the user. That dialog is in the screenshot below:
The Podder skin created by Grant Hinkson does not have a separate dialog window to expose this functionality. His skin allows the user to add and remove podcasts directly in the main window, as seen below:
The red circles in the image above point out where the user can add and remove podcasts. From a usability perspective, Grant’s approach makes much more sense. As Alan Cooper, the Interaction Design guru, might put it, Grant’s approach adheres to the “mental model” while my approach stays too close to the “implementation model”. It turns out that supporting both approaches was quite tricky.
The Problem
The first version of Podder, which is available from the first article in this series, had all of the podcast management functionality baked into the PodcastsDialog
class. That window contained all of the CommandBinding
s and had its own Controller, which handled user input. This worked fine because Podder had only my skin at the time, since Grant was not involved with the project yet.
When Grant started to work on his Podder skin, he almost immediately pointed out that he did not want to have a separate dialog window to handle podcast management. This presented a problem for me. I needed to find some way to generically provide the UI with a way to supply a podcast RSS feed URL, validate it, add a valid feed URL to the application’s list of podcasts, report feed validation errors, and remove podcasts. Since my skin puts this functionality in a separate window, but Grant’s skin does not, the application can make no assumptions about where or how the UI exposes these features.
That alone is a tricky requirement, but the problem did not stop there. When displaying this functionality in a separate dialog window, I am showing the same list of Podcast
objects in two places. The main window shows the list in a ComboBox
, and the podcast management dialog shows them in a ListBox
. Both controls are bound to the same underlying list of objects, but if the user selects a podcast in the dialog window it should not affect the selected podcast in the main window. The way to prevent this from happening is to provide the ListBox
in the dialog window with a new ListCollectionView
, so that it will not modify the CurrentItem
of the ListCollectionView
to which the main window’s ComboBox
is bound.
However, in Grant’s skin this problem did not exist. His skin did not have two controls displaying the same list of podcasts. The XamCarouselListBox
in Grant’s skin, which displays the list of podcasts, must be bound to the default collection view for the list. This is necessary because selecting a podcast in the list must update the CurrentItem
of the default collection view, so that the rest of the UI stays coordinated with the selected podcast.
To summarize the problem, I needed to create a way to expose application functionality in such a way that the UI could consume it either from a separate window, or from the main window. In addition, when consumed from a separate window, the UI needed to bind to a new collection view wrapped around the list of podcasts, so that it would not interfere with the selected podcast in the main window.
The Solution
The first version of Podder had all podcast management functionality in a separate dialog window, which was a problem. The solution to that problem was simple; move the necessary logic into a UserControl
. To that end, I created the PodcastsControl
to handle adding and removing podcasts from the application. The XAML for that UserControl
is below:
<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>
Notice that the Content
property of PodcastsControl
is assigned via a dynamic resource reference. This is in keeping with the structural skinning technique examined in the previous article of this series. The control only has CommandBinding
s established for the features it exposes. Each Podder skin provides another UserControl
that is dynamically loaded into the PodcastsControl
, and executes the RoutedCommand
s to which the PodcastsControl
is listening.
PodcastsControl
has two constructors, as seen below:
public PodcastsControl()
{
InitializeComponent();
_controller = new PodcastsControlController(this, null);
}
public PodcastsControl(ListCollectionView podcastsView)
{
InitializeComponent();
_controller = new PodcastsControlController(this, podcastsView);
}
PodcastsControl
has a Controller that handles user interaction. That Controller uses a collection view wrapped around the application’s list of podcasts to know which podcast is currently selected in the UI. When the application’s main window hosts PodcastsControl
, such as in Grant’s skin, PodcastsControlController
uses the default collection view wrapped around the application’s list of Podcast
s. When hosted in the PodcastsDialog
, it has a reference to a new collection view created by PodcastsDialog
.
Here is the PodcastsDialog
constructor, which creates a new collection view and the PodcastsControl
that it hosts.
public PodcastsDialog(PodcastCollection podcasts, Podcast selectedPodcast)
{
InitializeComponent();
ListCollectionView podcastsView = new ListCollectionView(podcasts);
podcastsView.Filter = podcast => podcast is FavoriteEpisodes == false;
if (podcastsView.PassesFilter(selectedPodcast))
podcastsView.MoveCurrentTo(selectedPodcast);
base.DataContext = podcastsView;
PodcastsControl podcastsControl = new PodcastsControl(podcastsView);
base.Content = podcastsControl;
Commands.AutoDetectFeedUrlOnClipboard.Execute(null, podcastsControl);
}
As seen in the code above, PodcastsDialog
sets its DataContext
property to the new collection view that it indirectly passes to the PodcastsControlController
. This ensures that the PodcastsControl
it hosts is bound to the same collection view as the one referenced by the Controller. None of this is necessary when PodcastsControl
is shown in the main window, since there is no need for a separate collection view in that situation.
Conclusion
Designing an application to support structural skinning has its own set of challenges. It requires you to expose application functionality in a truly UI-agnostic way. The solution to the podcast management problem seen in this article is one example of this. Looking back at it now, it seems quite simple, but before I knew a solution it was a rather difficult nut to crack. It took me a while to figure out a clean way to solve this problem, so I thought it was worthwhile to write an article about it. I hope my solution is useful, or at least interesting, for others creating look-less applications.
Revision History
- March 20, 2008 - Created the article