Click here to Skip to main content
Click here to Skip to main content

The WPF Podcatcher Series – Part 3 (The Podcast Management Conundrum)

By , 20 Mar 2008
 

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:

PodcastManagementDialog.png

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:

GrantPodcastManagement.png

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 CommandBindings 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 CommandBindings established for the features it exposes. Each Podder skin provides another UserControl that is dynamically loaded into the PodcastsControl, and executes the RoutedCommands to which the PodcastsControl is listening.

PodcastsControl has two constructors, as seen below:

/// <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 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 Podcasts. 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();            
    
    // 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);
}

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Josh Smith
Software Developer (Senior) Cynergy Systems
United States United States
Member
Josh creates software, for iOS and Windows.
 
He works at Cynergy Systems as a Senior Experience Developer.
 
Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.
 
Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.
 
Check out his Advanced MVVM[^] book.
 
Visit his WPF blog[^] or stop by his iOS blog[^].

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralDo you mind if I nick some of this.mvpPete O'Hanlon2 May '08 - 9:35 
I'm working on a tutorial showing how to build an RSS reader in WPF, and I was wondering if you would mind me using some of the ideas and concepts in this series of articles? Also - when it's finished, could you take a look at it and let me know what you think?
 

Deja View - the feeling that you've seen this post before.
 

My blog | My articles



GeneralRe: Do you mind if I nick some of this.mvpJosh Smith2 May '08 - 9:45 
Absolutely! That's what it's all about! Smile | :)
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: Do you mind if I nick some of this.mvpPete O'Hanlon2 May '08 - 9:58 
Cool. Thanks Josh. I want to be your disciple. Big Grin | :-D
 

Deja View - the feeling that you've seen this post before.
 

My blog | My articles



GeneralRe: Do you mind if I nick some of this.mvpJosh Smith2 May '08 - 10:01 
Pete O'Hanlon wrote:
Cool. Thanks Josh. I want to be your disciple.

 
We are all disciples of the one true WPF guru[^].
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralCoolmember Dr.Luiji 17 Apr '08 - 9:20 
Very cool, as the other part of this series, I like that.
 
Dr.Luiji
 
Trust and you'll be trusted.

GeneralRe: CoolmvpJosh Smith18 Apr '08 - 3:17 
Dr.Luiji wrote:
Very cool, as the other part of this series, I like that.

 
Thanks, I appreciate the feedback.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralSweetmvpPete O'Hanlon26 Mar '08 - 12:14 
I really am in information overload at the moment. There are so many great articles coming from you, Karl and Sacha that I'm having a really hard time keeping up with it all. For those who've downvoted, all I can say is that you don't appreciate just how far the boundaries are being pushed here.
 
Thanks Josh - some of us really appreciate it, and are glad that you are willing to share with the 'umble mortals.
 

Deja View - the feeling that you've seen this post before.
 

My blog | My articles



GeneralRe: SweetmvpJosh Smith26 Mar '08 - 13:37 
It is always great to know that some people really appreciate the articles I publish here. Thanks for the feedback, Pete. Much appreciated, as always.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralYes matemvpSacha Barber21 Mar '08 - 7:35 
This series is great...An interesting issue and solution elequently summarised.
 
Good job
 
Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same view, and never argue
 
My Blog : sachabarber.net

GeneralRe: Yes matemvpJosh Smith21 Mar '08 - 8:13 
Thanks Sacha. I thought that you would be interested in this article.
 
I am surprised at how many low votes this article received so quickly. I assumed this one would be a little too far "out there" for most people, since it is a rather esoteric problem being solved. I'm very happy to see some folks, such as yourself, recognizing how interesting the issues around structural skinning can be. Not only interesting, but important, as Bert pointed out.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: Yes matemvpSacha Barber21 Mar '08 - 9:03 
Josh Smith wrote:
I am surprised at how many low votes this article received so quickly.

 
Sadly this is the way at Codeproject sometimes. The ones that matter know you rock, and are a prince amongst the WPF world.
 
Thats what really counts.
 
All the best.
 
Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same view, and never argue
 
My Blog : sachabarber.net

GeneralRe: Yes matemvpJosh Smith21 Mar '08 - 9:05 
Sacha Barber wrote:
The ones that matter know you rock, and are a prince amongst the WPF world.

 
Blush | :O Thanks Sacha.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralThanks for the "Ah ha" momentmemberBert delaVega21 Mar '08 - 6:43 
I'm in the stages of putting a vertical market app into production for my system's first customer. I designed the architecture 3 years ago and my focus was proper layering to achieve a generic front end (UI) and back end (DB). The UI is designed so that it can be interchangeable. For example, this customer is okay installing a C# client windows app, others may want ASP with no client installation. WPF came later but I also know I can plug that in.
 
My premise was being able to duplicate the UI using other platforms (the functional UI). What you're saying is that the functional level should be agnostic enough to the UI that it can accomodate multiple interpretations using the "mental model". Brilliant.
 
I've been working from the bottom up and lost sight at the top!
 
Thanks again.
GeneralRe: Thanks for the "Ah ha" momentmvpJosh Smith21 Mar '08 - 7:16 
Bert delaVega wrote:
My premise was being able to duplicate the UI using other platforms (the functional UI). What you're saying is that the functional level should be agnostic enough to the UI that it can accomodate multiple interpretations using the "mental model".

 
Exactly. The application functionality should be decoupled from how a UI exposes it. We, as developers, should allow UI designers to be concerned with things like "mental model" visualizations and interaction patterns. The code should not place mandates on the UI.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

Generalmental model meets implementation modelmvpKarl Shifflett21 Mar '08 - 3:36 
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.”
 
Josh, the above sentence says volumes. Now I'll be thinking about this all day!!
 
WPF gives us the ability to do this; simple yet powerful UI gestures.
 
Thanks for sharing this information and challenging us to take our applications to places they have not yet been. Cool | :cool:
 
Cheers, Karl
 
» CodeProject 2008 MVP
 
My Blog | Mole's Home Page | How To Create Screen Capture Videos For Your Articles
 

Just a grain of sand on the worlds beaches.


GeneralRe: mental model meets implementation modelmvpJosh Smith21 Mar '08 - 3:42 
Karl Shifflett wrote:
Josh, the above sentence says volumes. Now I'll be thinking about this all day!!

 
I'm glad you appreciate that, too. Cooper's book, About Face 3.0, turned me on to the idea of implementation model vs. mental model. My skin uses a separate dialog window to expose the add/remove feature. To me, that was the natural, obvious way to expose that functionality. After long reflection, I realized that it made sense for me to do it that way because that add/remove code is separate from the rest of the code base. I was structuring the UI based on the implementation details, instead of doing it the right way and thinking about it from a non-technical user's perspective. The key difference is asking yourself "How does it work?" (implementation model) versus "What is it?" (mental model). Grant did this right away and ended up creating a much nicer UI.
 
:josh:
My WPF Blog[^]
All of life is just a big rambling blog post.

GeneralRe: mental model meets implementation modelmvpKarl Shifflett21 Mar '08 - 3:49 
You know me, Mr. Brick and Mortar Business Applications.
 
I'm going to be thinking about this for a while, how to use this technique in everyday LOB form applications that have "always" had a New, Save, Delete toolbar button. I think these will be around for a long time, but...
 
I'll try and order Cooper's book also.
 
It's a good thing to constantly check, prove and expand our thought & design processes.
 
Cheers, Karl
 
» CodeProject 2008 MVP
 
My Blog | Mole's Home Page | How To Create Screen Capture Videos For Your Articles
 

Just a grain of sand on the worlds beaches.


General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 20 Mar 2008
Article Copyright 2008 by Josh Smith
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid