Thursday, October 7, 2010

Building a ‘real’ Windows Phone Twitter app Part 6 – Tweet detail page, replies, re-tweets and more…

 

We’re getting pretty close a fully working twitter app.  In this part we’re going to add a details page for tweets. 

When a list item is selected from any of the lists (statuses, mentions, messages or favorites), the detail page will load the tweet. 

The detail page will also contain the users name, the tweet date and the source of the tweet (source is usually the name of the application the user used to Tweet and will have a link to their web site). 

We’ll also add an application bar with several options including add item as favorite, email item, reply, re-tweet, send direct message and open in Internet Explorer.

Let’s get started by creating our new “DetailPage”.  Right click on “Views” –> “Add” –> “New Item…”. Select “Windows Phone Portrait Page” and give it the name “DetailPage.xaml” then hit the “Add” button:

image

Next, open the new “DetailPage.xaml” file and add the WebBrowser control.  Here’s the complete xaml I’ve added:

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="ApplicationTitle" Text="Twitt" Style="{StaticResource PhoneTextNormalStyle}"/>
        <TextBlock x:Name="PageTitle" Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,68">
        <phone:WebBrowser x:Name="WebBrowser" Grid.Row="0" IsScriptEnabled="True"/>
    </Grid>
</Grid>

I’ve set “IsScriptEnabled” to “true” to allow Javascript to run as users navigate links in the browser control.

Next we need a new helper class to store the data we’re going to pass to this page. Under “Common” add a new class called “DetailPageData.cs” which looks like this:

using System;

namespace Twitt.Common
{
    public class DetailPageData
    {
        public String UserName { get; set; }
        public String UserDisplayName { get; set; }
        public String Text { get; set; }
        public String CreatedDate { get; set; }
        public String Source { get; set; }
        public long id { get; set; }
    }
}

 

We’ll also be using the same pattern I did in early lessons to store the data in IsolatedStorage to pass to the page.  This is helpful to pass objects from page to page and also helps with Tombstoning since re-starting the app from any page can reload the same data from the saved file.  There is probably a small perf hit, but I haven’t noticed it feeling slow in the app.  But this could be a good candidate for future refactoring.

We’ve been storing all our file names in the “Constants.cs” file, so let’s add 2 new ones.  One for the “DetailPageFileName” and another one we’ll use to pass data for replies and retweets called “TweetPageFileName”:

namespace Twitt.Common
{
    public class Constants
    {
        public static string TwitterAccess = "TwitterAccess";
        public static string StatusesFileName = "StatusesFile";
        public static string MentionsFileName = "MentionsFile";
        public static string DirectMessagesFileName = "MessagesFile";
        public static string FavoritesFileName = "FavoritesFile";
        public static string DetailPageFileName = "DetailPage";
        public static string TweetPageFileName = "TweetPage";
    }
}

Now we can work on our code behind file “DetailPage.xaml.cs”.  Here’s the complete source code for this file:

using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Shell;
using Twitt.Common;

namespace Twitt.Views
{
    public partial class DetailPage
    {
        private DetailPageData _detailItem;
        public DetailPage()
        {
            InitializeComponent();
            CreateApplicationBar();
            Loaded += Post_Loaded;
        }

        #region ApplicationBar

        private void CreateApplicationBar()
        {
            ApplicationBar = new ApplicationBar { IsMenuEnabled = true, IsVisible = true, Opacity = .9 };

            var retweet = new ApplicationBarIconButton(new Uri("/Resources/Images/retweet.png", UriKind.Relative));
            retweet.Text = "retweet";
            retweet.Click += RetweetClick;

            var reply = new ApplicationBarIconButton(new Uri("/Resources/Images/reply.png", UriKind.Relative));
            reply.Text = "reply";
            reply.Click += ReplyClick;

            ApplicationBar.Buttons.Add(retweet);
            ApplicationBar.Buttons.Add(reply);
        }

        private void RetweetClick(object sender, EventArgs e)
        {
            var tweetPage = new TweetPageData
            {
                Tweet = String.Format("RT @{0} {1}", _detailItem.UserName, _detailItem.Text)
            };

            // Save the detailpage object which the detailpage will load up
            Helper.SaveSetting(Constants.TweetPageFileName, tweetPage);

            NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
        }

        private void ReplyClick(object sender, EventArgs e)
        {
            var tweetPage = new TweetPageData
            {
                Tweet = String.Format("@{0} ", _detailItem.UserName)
            };

            // Save the detailpage object which the detailpage will load up
            Helper.SaveSetting(Constants.TweetPageFileName, tweetPage);

            NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
        }

        #endregion

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            _detailItem = Helper.LoadSetting<DetailPageData>(Constants.DetailPageFileName);
            if (_detailItem == null)
            {
                MessageBox.Show("Error loading page data");
                return;
            }

            PageTitle.Text = _detailItem.UserDisplayName;
        }

        private void Post_Loaded(object sender, RoutedEventArgs e)
        {
            var html = new StringBuilder();

            html.Append(String.Format("{0}<br><br>", _detailItem.CreatedDate));
            html.Append(MakeLinks(_detailItem.Text));
            if (!String.IsNullOrEmpty(_detailItem.Source))
                html.Append(String.Format("<br><br>Source: {0}", _detailItem.Source));

            WebBrowser.NavigateToString(html.ToString());
        }

        private static string MakeLinks(string txt)
        {
            var regx = new Regex(@"http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\&amp;\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?", RegexOptions.IgnoreCase);
            var mactches = regx.Matches(txt);

            return mactches.Cast<Match>().Aggregate(txt, (current, match) => current.Replace(match.Value, "<a href='" + match.Value + "'>" + match.Value + "</a>"));
        }
    }
}

Let’s walk through a bit of that code: “CreateApplicationBar” will setup our 2 application bar icons.  I created two icons for these and added them to the Images directory, make sure you set the properties to “content” or they won’t render.  If you want to use the ones I used, grab the source code from the link at the bottom of this post.

In the "OnNavigatedTo” method, it first loads the data file for this page and stores it in a member variabled named “_detailItem”.  The page title is then set to the users display name that sent that tweet:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    _detailItem = Helper.LoadSetting<DetailPageData>(Constants.DetailPageFileName);
    if (_detailItem == null)
    {
        MessageBox.Show("Error loading page data");
        return;
    }

    PageTitle.Text = _detailItem.UserDisplayName;
}

The constructor set a Post_Loaded event, this will be used to populate the Web Browser control with the tweet data.  I’ll use a StringBuilder to create the HTML.  I’ll first add the tweet date, then the tweet text followed by the source if there is one:

private void Post_Loaded(object sender, RoutedEventArgs e)
{
    var html = new StringBuilder();

    html.Append(String.Format("{0}<br><br>", _detailItem.CreatedDate));
    html.Append(MakeLinks(_detailItem.Text));
    if (!String.IsNullOrEmpty(_detailItem.Source))
        html.Append(String.Format("<br><br>Source: {0}", _detailItem.Source));

    WebBrowser.NavigateToString(html.ToString());
}

Notice in the above code I call a helper called “MakeLinks”  this helper looks for any imbedded html links in the tweet text and creates an HTML anchor link using a RegEx pattern:

private static string MakeLinks(string txt)
{
    var regx = new Regex(@"http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\&amp;\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?", RegexOptions.IgnoreCase);
    var mactches = regx.Matches(txt);

    return mactches.Cast<Match>().Aggregate(txt, (current, match) => current.Replace(match.Value, "<a href='" + match.Value + "'>" + match.Value + "</a>"));
}

The next step is to hook up the lists on our MainPage.xaml file to open this page if an item was selected.  Open up MainPage.xaml and for all the list panorama items add SelectionChanged=”ListBoxSelectionChanged” event handler.  We’ll have one handler handle them all:

<ListBox Margin="0,0,-12,32" ItemsSource="{Binding Items}" SelectionChanged="ListBoxSelectionChanged">

Now in the MainPage.xaml.cs file add the event handler.  The event handler will first ensure an item was selected, then extract the data and create a new “DetailPageData” item and save it.  Then it will navigate to our new “DetailPage.xaml” file:

private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (((ListBox)sender).SelectedIndex == -1)
        return;

    var selectedItem = (ItemViewModel)((ListBox)sender).SelectedItem;
    if (selectedItem == null)
        return;

    var detailPage = new DetailPageData
    {
        UserDisplayName = selectedItem.DisplayUserName,
        UserName = selectedItem.UserName,
        CreatedDate = selectedItem.CreatedDate,
        Text = selectedItem.TweetText,
        Source = selectedItem.Source,
        Id = selectedItem.Id
    };

    // Save the detailpage object which the detailpage will load up
    Helper.SaveSetting(Constants.DetailPageFileName, detailPage);

    NavigationService.Navigate(new Uri("/Views/DetailPage.xaml", UriKind.Relative));
}

Let’s go back to the “DetailPage.xaml.cs” file.  I want to explain how I did the “Reply” and “Retweet”.  Here’s a look at the code:

private void RetweetClick(object sender, EventArgs e)
{
    var tweetPage = new TweetPageData
    {
        Tweet = String.Format("RT @{0} {1}", _detailItem.UserName, _detailItem.Text)
    };

    // Save the detailpage object which the detailpage will load up
    Helper.SaveSetting(Constants.TweetPageFileName, tweetPage);

    NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
}

private void ReplyClick(object sender, EventArgs e)
{
    var tweetPage = new TweetPageData
    {
        Tweet = String.Format("@{0} ", _detailItem.UserName)
    };

    // Save the detailpage object which the detailpage will load up
    Helper.SaveSetting(Constants.TweetPageFileName, tweetPage);

    NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
}

Each method will create a new “TweetPageData” item and populate the “Tweet” variable with the data we want to show up.  For re-tweets, we just add “RT” then the users name before the tweet and for replies, we just add the “@” plus the user name.  We then navigate to our already existing TweetPage.xaml.  We now have to add a bit of code in this file to read from this new file.  I’ve changed the “OnNavigatedTo” method to look like this:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    _twitterSettings = Helper.LoadSetting<TwitterAccess>(Constants.TwitterAccess);
    if (_twitterSettings == null) return;

    ((ApplicationBarIconButton)ApplicationBar.Buttons[0]).IsEnabled = !String.IsNullOrEmpty(_twitterSettings.AccessToken) && !String.IsNullOrEmpty(_twitterSettings.AccessTokenSecret);
           
    var detailItem = Helper.LoadSetting<TweetPageData>(Constants.TweetPageFileName);
    if (detailItem != null)
    {
        TweetTextBox.Text = detailItem.Tweet;
    }
}

Let’s about all we need to add.  Run the app and you should now be able to click on any tweet in your lists and see the following screen:

image

You can now click on any link and have it open up in the browser.  Selecting “Retweet” will show:

image

Let’s add some more quick features.  We’ll now add the ability to “favorite” and item, e-mail it, open the link in Internet Explorer and sending a direct messages to the user that tweeted the item.  In DetailPage.xaml.cs file change the “CreateApplicationBar” method to:

private void CreateApplicationBar()
{
    ApplicationBar = new ApplicationBar { IsMenuEnabled = true, IsVisible = true, Opacity = .9 };

    var favorite = new ApplicationBarIconButton(new Uri("/Resources/Images/favorite.png", UriKind.Relative));
    favorite.Text = "favorite";
    favorite.Click += FavoriteClick;
    ApplicationBar.Buttons.Add(favorite);

    var sendEmail = new ApplicationBarIconButton(new Uri("/Resources/Images/mail.png", UriKind.Relative));
    sendEmail.Text = "E-mail tweet";
    sendEmail.Click += SendMailClick;
    ApplicationBar.Buttons.Add(sendEmail);

    var retweet = new ApplicationBarIconButton(new Uri("/Resources/Images/retweet.png", UriKind.Relative));
    retweet.Text = "retweet";
    retweet.Click += RetweetClick;
    ApplicationBar.Buttons.Add(retweet);

    var reply = new ApplicationBarIconButton(new Uri("/Resources/Images/reply.png", UriKind.Relative));
    reply.Text = "reply";
    reply.Click += ReplyClick;
    ApplicationBar.Buttons.Add(reply);

    var tweet = new ApplicationBarMenuItem("Send direct message");
    tweet.Click += SendDirectMessageClick;
    ApplicationBar.MenuItems.Add(tweet);

    var browserItem = new ApplicationBarMenuItem("Open in Internet Explorer");
    browserItem.Click += OpenInBrowserItemClick;
    browserItem.IsEnabled = false;
    ApplicationBar.MenuItems.Add(browserItem);
}

We’ll need the associated event methods for FavoriteClick, SendMailClick, SendDirectMessageClick and OpenInBrowserclick:

 

"SendMailClick” will set the Body to the tweet text and the subject to the user name:

private void SendMailClick(object sender, EventArgs e)
{
    var task = new EmailComposeTask
                    {
                        Body = _detailItem.Text,
                        Subject = String.Format("Tweet by {0}", _detailItem.UserDisplayName)
                    };
    task.Show();
}

 

“SendDirectMessageClick” will just append “d” then the user name to the front of the tweet:

private void SendDirectMessageClick(object sender, EventArgs e)
{
    var tweetPage = new TweetPageData
    {
        Tweet = String.Format("d {0} ", _detailItem.UserName),
    };

    Helper.SaveSetting(Constants.TweetPageFileName, tweetPage);

    NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
}

 

“OpenInBrowserItemClick” will open the current URL in the Browser Control in Internet Explorer:

private void OpenInBrowserItemClick(object sender, EventArgs e)
{
    var task = new WebBrowserTask { URL = WebBrowser.Source.ToString() };
    task.Show();
}

I also want to add a progress bar to the xaml as well as turning it on when the favorite item is clicked as well as navigating. 

<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <ProgressBar x:Name="ProgressBar" VerticalAlignment="Top" IsIndeterminate="False" Visibility="Collapsed"/>
    <TextBlock x:Name="ApplicationTitle" Text="Twitt" Style="{StaticResource PhoneTextNormalStyle}"/>
    <TextBlock x:Name="PageTitle" Text="page name" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>

 

I also want to turn the progress bar on while navigating links so in DetailPage.xaml change the browser control to:

<phone:WebBrowser x:Name="WebBrowser" Grid.Row="0" IsScriptEnabled="True" Navigated="WebBrowserNavigated" Navigating="WebBrowserNavigating"/>

 

Now add the 2 new event handlers for “WebBrowserNavigated” and “WebBrowserNavigating” to the DetailPage.xaml.cs file.  Notice once we’ve navigated I’m enabling the “open in internet explorer” link in the application bar.  It’s the second item added so we reference by array element “1”:

private void WebBrowserNavigating(object sender, NavigatingEventArgs e)
{
    ProgressBar.Visibility = Visibility.Visible;
    ProgressBar.IsIndeterminate = true;

    ((ApplicationBarMenuItem)ApplicationBar.MenuItems[1]).IsEnabled = true;
}

private void WebBrowserNavigated(object sender, NavigationEventArgs e)
{
    ProgressBar.Visibility = Visibility.Collapsed;
    ProgressBar.IsIndeterminate = false;
}

 

We’ll add the Favorites event handler.  We’ll turn the progress bar on at the top as well create a callback method to turn it off for success or failures. 

private void FavoriteClick(object sender, EventArgs e)
{
    ProgressBar.IsIndeterminate = true;
    ProgressBar.Visibility = Visibility.Visible;

    var twitter = new TwitterHelper();
    twitter.FavoriteCompletedEvent += (sender2, e2) =>
    {
        ProgressBar.IsIndeterminate = false;
        ProgressBar.Visibility = Visibility.Collapsed;
        MessageBox.Show("Item added to favorites");
    };
    twitter.ErrorEvent += (sender2, e2) =>
    {
        ProgressBar.IsIndeterminate = false;
        ProgressBar.Visibility = Visibility.Collapsed;
    };
    twitter.FavoriteItem(_detailItem.Id);
}

 

Next add the TwitterHelper.cs code that marks the favorite in Twitter including the new “FavoriteCompletedEvent” event handler:

public event EventHandler FavoriteCompletedEvent;

public void FavoriteItem(long id)
{
    if (!_authorized)
    {
        if (ErrorEvent != null)
            ErrorEvent(this, EventArgs.Empty);
        return;
    }

    var path = String.Format("/favorites/create/{0}.xml", id);

    var request = new RestRequest
    {
        Credentials = _credentials,
        Path = path,
        Method = WebMethod.Post
    };

    _client.BeginRequest(request, new RestCallback(FavoriteItemCompleted));
}

private void FavoriteItemCompleted(RestRequest request, RestResponse response, object userstate)
{
    Deployment.Current.Dispatcher.BeginInvoke(() =>
    {
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Helper.ShowMessage(String.Format("Error calling Twitter : {0}", response.StatusCode));
            if (ErrorEvent != null)
                ErrorEvent(this, EventArgs.Empty);
            return;
        }

        if (FavoriteCompletedEvent != null)
            FavoriteCompletedEvent(this, EventArgs.Empty);
    });
}

We now need to the 2 new images we used for favorites and email application bar items.  Look at the code link below to get the ones I used.

Running the app and selecting a tweet should now show the below shot.  Notice the “open in internet explorer” is disabled.

image

Clicking on a link should show the progress bar while it’s loading and then when complete look something like this:

image

Hitting the “favorite” button will save an item to your Twitter favorites and look like this:

image

If you then go back to the main page and hit the “Refresh” button you should also see the new item in your favorites:

image

This app is getting pretty complete.  One item missing might be to add a photo to a tweet, I won’t be covering this.  However in my next post I want to cover 2 more items.

The first it a change to how the app starts up and checks if you’ve setup your Twitter account.  If you haven’t it pops up the Twitter authorization page.  The problem is you now have to hit the back button twice to exit the app.  This will cause a failure if you submit the application to the Marketplace. 

The second item is adding Jeff Wilcox’s more performant progress bar.

The code can be found at: http://twitt.codeplex.com

Sam

No comments:

Post a Comment