wordpress hit counter
Adding Tweets to a PowerPoint presentation at runtime - OpenXML Developer - Blog - OpenXML Developer
Goodbye and Hello

OpenXmlDeveloper.org is Shutting Down

There is a time for all good things to come to an end, and the time has come to shut down OpenXmlDeveloper.org.

Screen-casts and blog posts: Content on OpenXmlDeveloper.org will be moving to EricWhite.com.

Forums: We are moving the forums to EricWhite.com and StackOverflow.com. Please do not post in the forums on OpenXmlDeveloper.org. Instead, please post in the forums at EricWhite.com or at StackOverflow.com.

Please see this blog post for more information about my plans moving forward.  Cheers, Eric

Adding Tweets to a PowerPoint presentation at runtime

Adding Tweets to a PowerPoint presentation at runtime

  • Comments 1

 

In the last 3 months I have got a request to create a solution that can add Tweets to a PowerPoint presentation on the fly. Basically the presenter wants to have a slide at the end of the presentation that displays X amount of Tweets with a specific tag. That way, the Q&A section at the end of the session can be used to answer some of the questions that have been Tweeted during the presentation.

 

Knowing that doing this with the Open XML SDK was a piece of cake, I decided to help out and build a PowerPoint add-in that would take care of doing this.

 

Intro

 

This article will show you how to build an add-in to PowerPoint that will look at every slide that is shown, and if the slide has been tagged with a Twitter tag, it will insert the latest Tweets in it. Actually, it won’t, but it will look like that. What actually happens is that the add-in copies the current slide into a new temporary slideshow and saves it to disk. It then calls on a class called Twitterizer that will get the latest Tweets and then use the Open XML SDK to add the tweets to the slide and save the temporary slideshow again. The add-in will then read the updated slideshow, copy the “twitterized” slide back into the current slideshow and tell PowerPoint to show it. As the slideshow moves to a new slide, the “twitterized” slide is deleted from the slideshow.

 

The Twitterizer

 

Let’s start by having a look at the Twitterizer class that is responsible for manipulating the slide and inserting the tweets...

 

I created the Twitterizer by creating a new Windows Class Library in VS2008. Next I added a reference to the Open XML SDK assembly called DocumentFormat.Open XML. If you have installed the Open XML SDK at the default location, that assembly should be available at C:\Program Files\Open XML SDK\V2.0\lib\DocumentFormat.OpenXml.dll. At least if you are running a 32-bit Windows. Otherwise the “Program Files” part of the path must be replaced with “Program Files (x86)”. The DocumentFormat.Open XML assembly requires us to also add a reference to the WindowsBase assembly, which is available in the GAC.

 

The Twitterizer will have 2 public methods, Twitterize and CancelTwitterization, and 2 public properties, IsLoadingTweets and Cancelled. The first method to start implementing is the Twitterize method. It takes four arguments and returns void. The three arguments are called filename, tweetCount, tag and callback. The filename is obviously the path to the file that contains the slide that needs to be “twitterized”. The tweetCount is an integer telling the method how many tweets to add to the slide. The tag is the name of the tag to use when searching for tweets and the callback is a delegate of type Action that will be used to notify the user when the method is done.

 

I start off by storing the filename, tweetCount and callback in a couple of global members so I have them available after the asynchronous call to Twitter has ended. I also set the Cancelled property to false, indicating that the current “twitterization” has not cancelled. Next I instantiate a WebClient and store a reference to it in a global member. This WebClient will be responsible for downloading the tweets from Twitter. As soon as I have a reference to a WebClient, I attach an eventhandler to the DownloadStringCompleted event and then call the DownloadStringAsync method. To the DownloadStringAsync method, I pass in a Uri that looks like this http://search.twitter.com/search.atom?q=%23<TAGNAME>. This will return an Atom feed based XML string that contains the requested tweets.

 

public void Twitterize(string filename, int tweetCount, string tag, Action callback)

{

    _filename = filename;

    _tweetCount = tweetCount;

    _callback = callback;

 

    Cancelled = false;

 

    _client = new WebClient();

    _client.DownloadStringCompleted += TwitterDownloadComplete;

    _client.DownloadStringAsync(new Uri("http://search.twitter.com/search.atom?q=%23" + tag));

}

 

In the DownloadStringCompleted eventhandler, I detach the handler and then make sure that there is no error in the response and that it hasn’t been cancelled. If either of those is true, I just return. I definitely suggest some better error handling, but this is mostly a proof of concept, so it will just have to be ok with the lack of error handling.

 

private void TwitterDownloadComplete(object sender, DownloadStringCompletedEventArgs e)

{

    _client.DownloadStringCompleted -= TwitterDownloadComplete;

    if (e.Error != null || e.Cancelled)

        return;

 

    Tweet[] tweets = GetTweets(e.Result, _tweetCount);

 

As soon as I know that the request has gone through as expected, I call off to a private method called GetTweets. It takes the received Atom XML string and a count as parameters, and returns an array of Tweet objects. Let’s start by looking at GetTweets method.

 

GetTweets parses the string into an XDocument so that I can use LINQ-to-SQL to get the information I need from it. What I need to find in the XML is all elements with the name “entry” in the correct namespace. In this case the namespace is http://www.w3.org/2005/Atom, and since it is used in multiple places I have decided to store it as a constant in the Twitterizer class. The Tweet class takes an XElement parameter during creation and that makes it quite easy to get a query that does most of the job for us. So the GetTweets method will be a simple thing looking like this:

 

private Tweet[] GetTweets(string atomResponse, int count)

{

    XDocument doc = XDocument.Parse(atomResponse);

    var tweetQuery = from t in doc.Descendants(XName.Get("entry", ATOM_NAMESPACE))

                     select new Tweet(t);

    return tweetQuery.Take(count).ToArray();

}

 

The constructor for the Tweet class will use the passed in XElement to retrieve any information it needs. It uses the XElement to get hold of the title of the atom entry as well as the author’s name and url and the date when the Tweet was published.

 

public Tweet(XElement element)

{

    Text = element.Element(XName.Get("title", ATOM_NAMESPACE)).Value;

    Username = element.Element(XName.Get("author", ATOM_NAMESPACE)).Element(XName.Get("name", ATOM_NAMESPACE)).Value;

    UserHomepage = new Uri(element.Element(XName.Get("author", ATOM_NAMESPACE)).Element(XName.Get("uri", ATOM_NAMESPACE)).Value);

    When = DateTime.Parse(element.Element(XName.Get("published", ATOM_NAMESPACE)).Value);

}

 

Having retrieved the Tweets from Twitter, the eventhandler goes on with the actual Open XML manipulation. It does so by opening a new PresentationDocument inside of a using statement. Then it goes on to get hold of a shape inside the first SlidePart. This Shape will be found by looking for a name of “Content Placeholder 2”, which is the default name for the “content” shape in a PowerPoint slide of type “Title and Content”. Finding this Shape is not that hard now that we have LINQ. All we have to do is create a nice little LINQ query.

 

var myShape = from shape in doc.PresentationPart.SlideParts.First().Slide.Descendants<Shape>()

            where shape.Descendants< NonVisualDrawingProperties>().First().Name == "Content Placeholder 2"

            select shape).FirstOrDefault();

 

If the code doesn’t find a Shape with that name, it just goes on to call the callback method. If it does find it, as expected, it grabs the first TextBody inside the Shape. It then clears out any paragraphs in it. And why would there be paragraphs inside it? Well, this is going to be a copy of the slide that is shown to the viewer while the tweets are loading. So as a presenter, I probably want to add a text inside the Shape that indicates that it is loading tweets. This text needs to be removed before adding the tweets... This cannot be done by clearing out all children as it removes a bit too much. So instead I get all the Paragraph objects inside the TextBody and delete them one at the time. And as we are not allowed to modify a collection while “foreaching” over it, I just use a simple while loop. And just for the fun of it, I also remove the ListStyle that PowerPoint places in there by default.

 

DocumentFormat.OpenXml.Drawing.Paragraph[] existingParagraphs = body.Descendants<DocumentFormat.OpenXml.Drawing.Paragraph>().ToArray();

int i = 0;

while (existingParagraphs.Length > i)

{

    body.RemoveChild(existingParagraphs[i]);

    i++;

}

body.RemoveChild(body.Descendants<DocumentFormat.OpenXml.Drawing.ListStyle>().First());

 

After having cleaned up the TextBody, it is time to add the tweets. This is just a matter of looping through the tweets and adding a Text class with the required text, which is added to a Run, which is added to a Paragraph which is added to the TextBody.

 

foreach (var tweet in tweets)

{

    Paragraph paragraph = new DocumentFormatParagraph();

    Run run = new Run();

    Text txt = new Text(tweet.Text);

    run.InsertAt<Text>(txt, run.ChildElements.Count);

    paragraph.InsertAt<Run>(run, paragraph.ChildElements.Count);

    body.InsertAt<Paragraph>(paragraph, body.ChildElements.Count);

}

 

When all the tweets have been added, it is just a matter of calling the callback. However, there is a little caveat. Since this method might take a little while, and works asynchronously, the user might go to the next slide before this completes. Therefore I added the CancelTwitterization method. This method will just return and do nothing if the object is not currently loading tweets. But if it is loading tweets, it sets Cancelled to true, detaches the DownloadStringCompleted eventhandler and tells the WebClient to cancel the current operation. It also sets the global member that holds the WebClient reference to null together with the one that holds the callback delegate.

 

public void CancelTwitterization()

{

    if (!IsLoadingTweets)

        return;

    Cancelled = true;

    _client.DownloadStringCompleted -= TwitterDownloadComplete;

    _client.CancelAsync();

    _client = null;

    _callback = null;

}

 

This makes the end of the DownloadStringCompleted handler a little more complicated. There is a chance that the CancelTwitterization method is called while the handler is executing. So before I can call the callback, I need to make sure it exists. After that, I set the WebClient and callback reference to null.

 

if (_callback != null)

    _callback();

 

_client = null;

_callback = null;

 

That covers the entire Twitterizer class, except for the properties. The Cancelled property is just a simple get/set property without even a backing field. The IsLoadingTweets is a little different. It is a read-only property that checks the existence of a WebClient. If there is a WebClient available, then object is busy loading tweets...

 

public bool IsLoadingTweets

{

    get { return _client != null; }

}

public bool Cancelled

{

    get;

    private set;

}

 

The Office Add-in

 

Having implemented the Twitterizer class, it is time to create a PowerPoint add-in. This is not that hard to do using the Visual Studio Tools for Office or VSTO. This extension to VS makes adding custom features to the Office package quite simple. This part is however somewhat out of scope, so I will just cover the basics.

 

I start off by creating a new “PowerPoing 2007 Add-in” project, which is available under the Office >2007 node in the “Add New Project” dialog window. That automatically sets up more or less everything for us. It generates a new add-in and makes sure that debugging is working as it should. So pressing F5 launches PowerPoint with the add-in installed.

 

But before I get started with the actual add-in, I need to add another thing to my project. I need a Ribbon item. This can be added by using the “Add New Item” dialog. This creates a new entry in the PowerPoint ribbon (the menu thingy at the top of the Office applications...). In my case I went for the visual one, and set it up with a tab called Twitter, a group with the same name and then a button inside it with a Twitter icon.

 

 

This button in the ribbon will be responsible for launching a dialog window where the user can decide what twitter tag to use as well as how many tweets to fetch. The code behind it is quite simple, it launches a custom dialog window that I have created, and if it returns DialogResult.OK, I use a couple of methods on the add-in to “tag” the current slide with the information.

 

 

private void AddTwitterTagButton_Click(object sender, RibbonControlEventArgs e)

{

    TwitterTagInputWindow dlg = new TwitterTagInputWindow();

    dlg.TwitterTag = Globals.TwitterAddIn.GetTag();

    dlg.Count = Globals.TwitterAddIn.GetCount() ?? 1;

    if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)

    {

        if (!string.IsNullOrEmpty(dlg.TwitterTag))

        {

            Globals.TwitterAddIn.SetTag(dlg.TwitterTag);

            Globals.TwitterAddIn.SetCount(dlg.Count);

        }

        else

        {

            Globals.TwitterAddIn.SetTag(null);

            Globals.TwitterAddIn.SetCount(null);

        }

    }

}

 

When working with slideshows from code, each slide has a property called Tags. It is a place where we can store information about the slide. So I thought it was a pretty good place to store the twitter information. The actual getting and setting of the tag values is handled by the add-in and exposed as public 4 methods.

 

The add-in has a few responsibilities. First of all, it is as you have seen before responsible for setting and getting the tag values. These methods are quite simple and more or less just forward the calls to a couple private methods that look like this:

 

private void SetTagValue(string tagName, string tagValue)

{

    if (!string.IsNullOrEmpty(tagValue))

        Application.ActiveWindow.Selection.SlideRange[1].Tags.Add(tagName, tagValue);

    else

        Application.ActiveWindow.Selection.SlideRange[1].Tags.Delete(tagName);

}

private string GetTagValue(string tagName)

{

    Slide slide = Application.ActiveWindow.Selection.SlideRange[1];

    if (!string.IsNullOrEmpty(slide.Tags[tagName]))

        return slide.Tags[tagName];

    return null;

}

 

Besides that, the add0in is responsible for handling the Application.SlideShowNextSlide event, as well as the Application.SlideShowEnd event. So I attach handler to these events in the pre-created ThisAddIn_Startup method, and detach them in the ThisAddIn_Shutdown method.

 

The SlideShowNextSlide handler does all of the heavy lifting. It starts by checking if the slideshow has moved forward or backwards. It has to do this, as we have to do a bit of extra work if we are moving backwards. Next it checks if we have a “current slide” available. Basically the add-in tracks the slide changes and keeps the current slide in a global member. If there is one, which means it is not the first slide in the slideshow, it checks if it is one of my dynamically created slides. If it is, it deletes it. As the add-in generates and adds a new slide to the slideshow, I need to remember to remove it when it isn’t needed anymore.

 

Next it checks if the local instance of the Twitterizer class is loading tweets, if it is, it cancels it. We don’t want it to load a “twitter slide” into the presentation if the show has moved on...

 

After that, it updates a global member to hold a reference to the current slide, and gets its Twitter tag. If it doesn’t have a tag, the method just returns. If it does have a tag, some more execution is needed. First it needs to see if the slideshow has gone backwards. If that is the case, it needs to move back yet another step. Why? Well, as the slideshow reaches a Twitter tagged slide, a new slide will be created and added to the slideshow just after the tagged slide and then the slideshow will move to that slide. If the slideshow then moves backwards, it will end up on that tagged page again and start all over. What the add-in does, is that it goes backwards once more to end up before that page.

 

If the slideshow hasn’t moved backwards, it creates a new temporary file on disk and saves the current slide to it. Then it calls the Twitterizer and tells it to Twitterize the slide. When the “twitterization” is done, it inserts the slides from the temporary slideshow and moves the slideshow one step forward.

 

Conclusion

 

This is probably not the way I would normally have solved this, but it shows how easy it is for us to open up and modify Open XML documents with the Open XML SDK.

 

Article by Chris Klug

Attachment: Twitter Add-In.zip
  • Nice article.

    How are you manipulating the active document, using OpenXML or standard PowerPoint InsertFromFile Method.


    Would you be making code available for download?

    Thanks
    Rakesh
Page 1 of 1 (1 items)