C# Code, Tutorials and Full Visual Studio Projects

Sojurn – The TV App

Posted by on May 14, 2013 in Code Snippets, Featured, Projects, Tutorials, WPF | 6 comments

Sojurn – The TV App

A MVVM application written using Caliburn.Micro and MahApps.metro, that gets TV data from TVRage and displays it in a slick interface. If your new to Caliburn.Micro this is a great way to get your feet wet.

This application requires both Caliburn.Micro and MahApps.metro. Both are included in the download package. It also features amazing icons from the WindowsIcons project.

 

 

Please grab a copy of the full source code and follow along:

GitHub Repository

 

 

 

 

 

What is Caliburn.Micro?

Caliburn.Micro is a framework that helps with doing Model-View-ViewModel style development. It provides ways to connect the View to the ViewModel without having to do any work in the code-behind file. It also contains a Window Manager that helps define the life-cycle of “windows” and how they are created.


What is MahApps.metro?

MahApps.metro is a library you can download that contains, controls and resources to make your applications look like the Microsoft Metro UI. (Or Visual Studio’s dark theme if you prefer.)


TVRage

TVRage provides some easy to use REST API’s for TV information. This application uses those to provide content. You can see the API documentation here: TVRage API


Getting Started

The bootstrapper is where it all begins.

using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Linq;
using Caliburn.Micro;
using Jarloo.Sojurn.ViewModels;

namespace Jarloo.Sojurn
{
    public class SojurnBootstrapper : Bootstrapper<MainViewModel>
    {
        private CompositionContainer container;

        protected override void Configure()
        {
            container = new CompositionContainer(new AggregateCatalog(AssemblySource.Instance.Select(x => new AssemblyCatalog(x)).OfType<ComposablePartCatalog>()));

            CompositionBatch batch = new CompositionBatch();

            batch.AddExportedValue<IWindowManager>(new AppWindowManager());
            batch.AddExportedValue<IEventAggregator>(new EventAggregator());
            batch.AddExportedValue(container);

            container.Compose(batch);
        }
        
        protected override object GetInstance(Type serviceType, string key)
        {
            string contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
            var exports = container.GetExportedValues<object>(contract);

            if (exports.Count() > 0)
            {
                return exports.First();
            }

            throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
        }
        
    }
}

The bootstrapper is the first thing Caliburn.Micro calls, and in this case it runs the MainViewModel.

Caliburn.Micro will use the custom Window Manager in the project called AppWindowManager to determine how to display the window:

using System.Windows;
using Caliburn.Micro;
using Jarloo.Sojurn.Windows;

namespace Jarloo.Sojurn
{
    internal class AppWindowManager : WindowManager
    {
        protected override Window EnsureWindow(object model, object view, bool isDialog)
        {
            Window window = view as BaseWindow;

            if (window == null)
            {
                if (isDialog)
                {
                    window = new BaseDialogWindow
                    {
                        Content = view,
                        SizeToContent = SizeToContent.WidthAndHeight
                    };
                }
                else
                {
                    window = new BaseWindow
                        {
                            Content = view,
                            SizeToContent = SizeToContent.Manual
                        };
                }

                window.SetValue(View.IsGeneratedProperty, true);
            }
            else
            {
                Window owner2 = InferOwnerOf(window);
                if (owner2 != null && isDialog)
                {
                    window.Owner = owner2;
                }
            }
            return window;
        }
    }
}

The AppWindowManager class returns a BaseWindow or BaseDialogWindow. The reason I added these is so I could have more control over templating the window. MahApps.metro lets us add items to the window chrome. In this case I’ve added a control that displays a push-pin, and allows the user to toggle the control topmost.


The Main ViewModel

This is where all the real action is. It exposes the TV information via the Models: Show, Season and Episode. When you click the “+” button it calls the AddShowViewModel and injects an IInformationProvider instance into it. The default one is the TvRageInformationProvider but you can provide any implementation as long as it fits the interface.


Information Providers

The TVRageInformationProvider handles downloading and parsing the data into the Models mentioned, which are then returned to the MainViewModel. Since the TVRage API is REST, it is very easy to work with.

To download and parse the full episode information for a show it boils down to this:

public Show GetFullDetails(int showId)
{
    string url = string.Format("{0}full_show_info.php?sid={1}", BASE_URL, showId);
    XDocument doc = XDocument.Load(url);

    var s = doc.Root;

    Show show = new Show
        {
            ShowId = Get<int>(s.Element("showid")),
            Name = Get<string>(s.Element("name")),
            Started = GetDate(s.Element("started")),
            Ended = GetDate(s.Element("ended")),
            Country = Get<string>(s.Element("origin_country")),
            Status = Get<string>(s.Element("status")),
            ImageUrl = Get<string>(s.Element("image")),
            AirTimeHour = GetTime(s.Element("airtime"),'H'),
            AirTimeMinute = GetTime(s.Element("airtime"),'M'),

            Seasons = (from season in s.Element("Episodelist").Elements("Season")
                        select new Season
                            {
                                SeasonNumber = Convert.ToInt32(season.Attribute("no").Value),
                                Episodes = (from e in season.Elements("episode")
                                            select new Episode
                                                {
                                                    EpisodeNumber = Get<int>(e.Element("epnum")),
                                                    AirDate = GetDate(e.Element("airdate")),
                                                    Title = Get<string>(e.Element("title")),
                                                    Link = Get<string>(e.Element("link")),
                                                    ImageUrl = Get<string>(e.Element("screencap")),
                                                    ShowName = Get<string>(s.Element("name")),
                                                    SeasonNumber = Convert.ToInt32(season.Attribute("no").Value)
                                                }).OrderBy(w => w.EpisodeNumber).ToList()
                            }).ToList()
        };

    return show;
}


Handling Images

The ImageHelper class is used to download and process all the images for each episode, as well as the artwork for each show. The URL’s to the images for the shows and episodes are stored in the corresponding models. All image downloading is done on background threads to ensure the application stays responsive. Once each image is downloaded it’s saved (see Persistence) and then displayed.

public static void GetEpisodeImages(Show show)
{
    foreach (var episode in show.Seasons.SelectMany(season => season.Episodes))
    {
        episode.IsLoading = true;
    }

    Task.Factory.StartNew(() =>
        {
            foreach (var season in show.Seasons.OrderByDescending(w=>w.SeasonNumber))
            {
                foreach (var episode in season.Episodes.OrderByDescending(w=>w.EpisodeNumber))
                {
                    Episode e = episode;

                    if (episode.ImageUrl != null)
                    {
                        string extension = Path.GetExtension(e.ImageUrl);
                        string file = string.Format("{0}_{1}_{2}{3}", show.ShowId, season.SeasonNumber, e.EpisodeNumber, extension);
                        string folder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationManager.AppSettings["IMAGE_CACHE"]);

                        if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);

                        string filename = Path.Combine(folder, file);

                        if (!File.Exists(filename))
                        {
                            using (WebClient web = new WebClient())
                            {
                                web.DownloadFile(e.ImageUrl, filename);
                            }
                        }

                        Execute.BeginOnUIThread(() =>
                            {
                                if (extension.ToUpper() == ".PNG")
                                {
                                    Stream imageStreamSource = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
                                    PngBitmapDecoder decoder = new PngBitmapDecoder(imageStreamSource, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
                                    e.ImageSource = decoder.Frames[0];
                                }
                                else
                                {
                                    try
                                    {
                                        e.ImageSource = new BitmapImage(new Uri(filename));
                                    }
                                    catch 
                                    {
                                        //File most likely corrupted
                                        File.Delete(filename);
                                    }
                                }
                            });
                    }

                    Execute.BeginOnUIThread(() => e.IsLoading = false);
                }
            }
        });
}


Persistence

The LocalJsonPersistanceManager class is used to save and load information from a data store. In this case the hard-disk. This class uses one main file that can be found in your Application Folder’s “Data” directory. It’s called index.json. This is where all the show and episode data is stored. The Images that are downloaded are all stored in the Application Folder’s “ImageCache” folder.


Conclusion

This was a fun application to build and I’m happy with the result, hopefully you’ll find it useful.

The app uses several icons for the user interface. These are all free created by the wonderfull WindowsIcons project. (license file included in the download)

Future Directions

  • I’ve found other TV information providers that could provide additional capabilities which I’ll probably explore and update the project.
  • I would like to integrate this interface with Hulu or a similar provider of streaming TV. Unfortunately Hulu doesn’t provide access to Canadians…
  • Looked at setting up a service to go with this UI, so you can schedule TV shows and download them via NZB and integration with SabNZB. The idea is interesting for me so I’ll probably follow it up.

6 Comments

Join the conversation and post a comment.

  1. Simon

    Fantastic code even though it is far beyond my capabilities (I am quite new to coding but a fast learner.

    Tell me, how would you do to get a picture from a URL streamed to an imagesource without saving it to disk as I am dealing with thousands and it would just consume far too much space in the end.

    Regards,

    Simon

  2. Kelly Elias

    Your looking for the MemoryStream. Take this code from Sojurn and replace the FileStream with a MemoryStream and it will not write to disk but still load into the ImageSource. Keep in mind each image consumes memory so you can’t show thousands at once.

    Stream imageStreamSource = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
    PngBitmapDecoder decoder = new PngBitmapDecoder(imageStreamSource, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
    e.ImageSource = decoder.Frames[0];

  3. Simon

    Okay, I am in awe of your coding there and I can’t help but suspect it was easy for you. WOudl you be able to tell me where (you went or)I can go to learn to use Json in my wpf project and especially how to use those Persistance managers you implemented in your project (I suspect that is not the proper name of it since google is not helping here).

    Seriously, hoping I can learn a new way of data usage by analysing your project if you don’t mind.

  4. Kelly Elias

    For this project I decided to store the data in JSON format, but it could have been binary, XML or any other format really. That’s why I ensured I created an interface the IPersistanceManager class so that a different PersistanceManager could be swapped in instead if desired. JSON has a few things going for it over XML. 1. It’s smaller so it takes less time to transfer large amounts of data in JSON then in XML. 2. The DataContractJsonSerializer can serialize a dictionary!! (can’t do that with the XML one out of the box)

    As for learning JSON I’ve been working with it for sometime, but there really isn’t one place I can point you at to learn it. http://www.json.org/ is a decent resource if your trying to understand how the format works, but really I would just recommend you build a few objects and serialize them to json and look at the results. One thing of note is you’ll notice the attributes on the models such as the Episode class. The DataContract, DataMemeber, IgnoreDataMember etc provide information to the DataContractJsonSerializer to tell it how to create the JSON.

  5. Anthony

    Can you show me how to show a window

  6. johnny

    I need help showing a window

Leave a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>