Saturday, October 2, 2010

Building a ‘real’ Windows Phone 7 Twitter App Part 4 – Getting Friend Statuses

 

In part 4 we’ll be adding friend statuses.  The next part we’ll add mentions, direct messages and favorites, each will be on their own ‘pivot’ of the panorama control.

In MainPage.xaml, there is a section for each Panorama item, the first one looks like this:

<!--Panorama item one-->
<controls:PanoramaItem Header="first item">
    <!--Double line list with text wrapping-->
    <ListBox Margin="0,0,-12,0" ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="0,0,0,17" Width="432">
                    <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</controls:PanoramaItem>

The first change I’m going to make is on the “Header” attribute of the “PanoramaItem” node and change to “statuses” which is the title for this pivot.   Next is the layout for the status details.  I want to show the tweeted text, user name, date of the tweet and the user image.  I’ve wrapped them all inside of a StackPanel.

Here’s the changed code:

<controls:PanoramaItem Header="statuses">
    <ListBox Margin="0,0,-12,32" ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,17">
                    <Image Source="{Binding Image}" Margin="12,0,12,0" Width="50" Height="50" />
                    <StackPanel Width="361" >
                        <TextBlock Text="{Binding DisplayUserName}" FontSize="{StaticResource PhoneFontSizeExtraLarge}" Foreground="{Binding TitleColor}"/>
                        <TextBlock Text="{Binding CreatedDate}" Margin="0,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                        <TextBlock Text="{Binding TweetText}" TextWrapping="Wrap" Margin="0,-6,12,0" />
                    </StackPanel>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</controls:PanoramaItem>

The UI in design view will not show any data yet and look like this:

image

We’ll need to define our data structure that holds the tweet statuses and also the sample data for data to show up in the design view.

In the above xaml, I declared 4 binding properties for our data: Image, DisplayUserName, CreatedDate and TweetText.

Next, we need to define a class to hold these properties.  When we created the solution it created our first ViewModel and ViewModel Item for us called “MainViewModel.cs” and “ItemViewModel.cs” under the “ViewModels” folder:

image

Open up the ItemViewModel.cs file and you’ll see some properties in there that we’re going to change.  They look similar to this:

private string _lineOne;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding.
/// </summary>
/// <returns></returns>
public string LineOne
{
    get
    {
        return _lineOne;
    }
    set
    {
        if (value != _lineOne)
        {
            _lineOne = value;
            NotifyPropertyChanged("LineOne");
        }
    }
}

We’re going to reuse this file for the properties we need.  Here’s what the completed new file looks like:

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;

namespace Twitt
{
    public class ItemViewModel : INotifyPropertyChanged
    {
        private string _userName;
        public string UserName
        {
            get
            {
                return _userName;
            }
            set
            {
                if (value != _userName)
                {
                    _userName = value;
                    NotifyPropertyChanged("UserName");
                }
            }
        }
       
        private string _displayUserName;
        public string DisplayUserName
        {
            get
            {
                return _displayUserName;
            }
            set
            {
                if (value != _displayUserName)
                {
                    _displayUserName = value;
                    NotifyPropertyChanged("DisplayUserName");
                }
            }
        }

        private string _tweetText;
        public string TweetText
        {
            get
            {
                return _tweetText;
            }
            set
            {
                if (value != _tweetText)
                {
                    _tweetText = value;
                    NotifyPropertyChanged("TweetText");
                }
            }
        }

        private string _createdDate;
        public string CreatedDate
        {
            get
            {
                return _createdDate;
            }
            set
            {
                if (value != _createdDate)
                {
                    _createdDate = value;
                    NotifyPropertyChanged("CreatedDate");
                }
            }
        }

        private string _image;
        public string Image
        {
            get
            {
                return _image;
            }
            set
            {
                if (value != _image)
                {
                    _image = value;
                    NotifyPropertyChanged("Image");
                }
            }
        }

        private string _source;
        public string Source
        {
            get
            {
                return _source;
            }
            set
            {
                if (value != _source)
                {
                    _source = value;
                    NotifyPropertyChanged("Source");
                }
            }
        }

        public long Id { get; set; }
        public bool NewTweet { get; set; }

        public SolidColorBrush TitleColor
        {
            get
            {
                return NewTweet ? (SolidColorBrush)Application.Current.Resources["PhoneForegroundBrush"]
                               : (SolidColorBrush)Application.Current.Resources["PhoneBackgroundBrush"];
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (null != handler)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

I’ve added some extra properties we’ll use later.  This includes the UserName as well as the DisplayUserName since Twitter stores the users account name and their “screen” name.  DisplayUserName is the longer form we’ll be using in our UI list.  I’ve also added a “NewTweet” boolean property which we’ll set to true for new tweets and false for old texts.  This boolean is used in the “TitleColor” property which will return the phones accent color for the titles of new tweets.  Old tweets will show with the phones Foreground brush color.

Adding SampleData

In “MainViewModel.cs” file the constructor currently adds some sample data to display at runtime.  Let’s clear out this data which is set in the constructor, we’ll add real data later.

public void LoadData()
{
    this.IsDataLoaded = true;
}

Under the “SampleData” folder in the solution there is a “MainViewModelSampleData.xaml” file.  This fills in the ItemViewModel properties to add sample data to the design environment.  Change this data to show some sample data which will make it much easier to tweak our xaml.  Here are the changes:

<local:MainViewModel
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"      
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Twitt"
   
    <local:MainViewModel.Items>
        <local:ItemViewModel DisplayUserName="User One" CreatedDate="09/9/2010 4:35pm" TweetText="Maecenas praesent accumsan bibendum dictumst eleifend facilisi faucibus habitant inceptos interdum lobortis nascetur" TitleColor="White"/>
        <local:ItemViewModel DisplayUserName="User Two" CreatedDate="09/9/2010 3:35pm" TweetText="Pharetra placerat pulvinar sagittis senectus sociosqu suscipit torquent ultrices vehicula volutpat maecenas praesent" TitleColor="White"/>
        <local:ItemViewModel DisplayUserName="User Three" CreatedDate="09/9/2010 2:35pm" TweetText="Accumsan bibendum dictumst eleifend facilisi faucibus habitant inceptos interdum lobortis nascetur pharetra placerat" TitleColor="Black"/>
        <local:ItemViewModel DisplayUserName="User Four" CreatedDate="09/9/2010 1:35pm" TweetText="Pulvinar sagittis senectus sociosqu suscipit torquent ultrices vehicula volutpat maecenas praesent accumsan bibendum" TitleColor="Black"/>
    </local:MainViewModel.Items>
   
</local:MainViewModel>

One last change is to the reference to our sample data at the top of the MainPage.xaml.cs.  Since we moved this file under the “View” folder in an earlier post we need to change the reference to the sample data:

d:DataContext="{d:DesignData ../SampleData/MainViewModelSampleData.xaml}"

Rebuild and then the design view for the MainPage.xaml should now look like this:

image

If you run the app, the list will still be empty of course since we haven’t called Twitter yet to get any data.  Let’s add a refresh button so that we can manually call twitter to refresh our data.

In MainPage.xaml change the third parorama item to include a “Refresh” button.  Note that I’ve also added some padding:

image

<!--Panorama item three-->
<controls:PanoramaItem Header="settings">
    <StackPanel>
        <Button x:Name="SettingsButton" Content="Account Settings" Click="SettingsButtonClick"/>
        <Button x:Name="TweetButton" Margin="0,30,0,0" Content="New Tweet" Click="TweetButtonClick"/>
        <Button x:Name="RefreshButton" Margin="0,30,0,0" Content="Refresh" Click="RefreshButtonClick"/>
    </StackPanel>
</controls:PanoramaItem>

Now let’s add some code so that when the app launches it will load the Twitter data.  I’ve also added some code to check if we’ve saved the users authorization codes from Twitter, if not we’ll load the Twitter auth page we created in an earlier post.  Here’s the new code for MainPage.xaml.cs:

using System;
using System.Windows;
using Twitt.Common;

namespace Twitt
{
    public partial class MainPage
    {
        private bool _firstLaunch;

        public MainPage()
        {
            InitializeComponent();

            _firstLaunch = true;

            // Set the data context of the listbox control to the sample data
            DataContext = App.ViewModel;
            Loaded += MainPage_Loaded;
        }

        // Load data for the ViewModel Items
        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            // Check if user has credentials setup if not navigate to twitter auth screen
            var twitterSettings = Helper.LoadSetting<TwitterAccess>(Constants.TwitterAccess);
            if (_firstLaunch && (twitterSettings == null ||
                String.IsNullOrEmpty(twitterSettings.AccessToken) ||
                String.IsNullOrEmpty(twitterSettings.AccessTokenSecret)))
            {
                _firstLaunch = false;

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

            if (!App.ViewModel.IsDataLoaded)
            {
                App.ViewModel.LoadData();
            }

            _firstLaunch = false;
        }

        private void SettingsButtonClick(object sender, RoutedEventArgs e)
        {
            NavigationService.Navigate(new Uri("/Views/TwitterAuthPage.xaml", UriKind.Relative));
        }
       
        private void TweetButtonClick(object sender, RoutedEventArgs e)
        {
            NavigationService.Navigate(new Uri("/Views/TweetPage.xaml", UriKind.Relative));
        }

        private void RefreshButtonClick(object sender, RoutedEventArgs e)
        {
            App.ViewModel.Refresh();
        }
    }
}

We’ll now add the needed code in our view model and TwitterHelper class.

We first need to add a new reference to System.Xml.Linq. Right click on References in the project and select “Add Reference…”.  Then select the “System.Xml.Linq” as shown below:

image 

Let’s first modify TwitterHelper.cs file to fetch the data. Here is the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using System.Windows;
using Hammock;
using Hammock.Authentication.OAuth;
using Hammock.Web;
using Twitt.ViewModels;

namespace Twitt.Common
{
    public class TwitterHelper
    {
        private const String MaxCount = "200";
        private readonly TwitterAccess _twitterSettings;
        private readonly bool _authorized;
        private readonly OAuthCredentials _credentials;
        private readonly RestClient _client;
        public event EventHandler TweetCompletedEvent;
        public event EventHandler LoadedCompleteEvent;
        public event EventHandler ErrorEvent;

        public TwitterHelper()
        {
            _twitterSettings = Helper.LoadSetting<TwitterAccess>(Constants.TwitterAccess);

            if (_twitterSettings == null || String.IsNullOrEmpty(_twitterSettings.AccessToken) ||
               String.IsNullOrEmpty(_twitterSettings.AccessTokenSecret))
            {
                return;
            }

            _authorized = true;

            _credentials = new OAuthCredentials
            {
                Type = OAuthType.ProtectedResource,
                SignatureMethod = OAuthSignatureMethod.HmacSha1,
                ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
                ConsumerKey = TwitterSettings.ConsumerKey,
                ConsumerSecret = TwitterSettings.ConsumerKeySecret,
                Token = _twitterSettings.AccessToken,
                TokenSecret = _twitterSettings.AccessTokenSecret,
                Version = TwitterSettings.OAuthVersion,
            };

            _client = new RestClient
            {
                Authority = "http://api.twitter.com",
                HasElevatedPermissions = true
            };
        }

        public void NewTweet(string tweetText)
        {
            if (!_authorized)
            {
                if (ErrorEvent != null)
                    ErrorEvent(this, EventArgs.Empty);
                return;
            }

            var request = new RestRequest
            {
                Credentials = _credentials,
                Path = "/statuses/update.xml",
                Method = WebMethod.Post
            };

            request.AddParameter("status", tweetText);

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

        private void NewTweetCompleted(RestRequest request, RestResponse response, object userstate)
        {
            // We want to ensure we are running on our thread UI
            Deployment.Current.Dispatcher.BeginInvoke(() =>
                {
                    if (response.StatusCode == HttpStatusCode.OK)
                    {
                        if (TweetCompletedEvent != null)
                            TweetCompletedEvent(this, EventArgs.Empty);
                    }
                    else
                    {
                        if (ErrorEvent != null)
                            ErrorEvent(this, EventArgs.Empty);
                    }
                });
        }

        public void LoadList(TwitterListType listType, long sinceId, string searchTerm)
        {
            switch (listType)
            {
                case TwitterListType.Statuses:
                    LoadStatuses(sinceId);
                    break;
                default:
                    return;
            }
        }

        private void LoadStatuses(long sinceId)
        {
            if (!_authorized)
            {
                if (LoadedCompleteEvent != null)
                    LoadedCompleteEvent(this, EventArgs.Empty);
                return;
            }

            var request = new RestRequest
            {
                Credentials = _credentials,
                Path = "/statuses/friends_timeline.xml",
            };

            request.AddParameter("count", MaxCount);

            if (sinceId != 0)
                request.AddParameter("since_id", sinceId.ToString());

            request.AddParameter("include_rts", "1");

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

        private void TwitterGetStatusesCompleted(RestRequest request, RestResponse response, object userstate)
        {
            if (response.StatusCode != HttpStatusCode.OK)
            {
                Helper.ShowMessage(String.Format("Twitter Error: {0}", response.StatusCode));
                return;
            }

            var xmlElement = XElement.Parse(response.Content);
            var statusList = (from item in xmlElement.Elements("status")
                              select new ItemViewModel
                              {
                                  UserName = GetChildElementValue(item, "user", "screen_name"),
                                  DisplayUserName = GetChildElementValue(item, "user", "name"),
                                  TweetText = (string)item.Element("text"),
                                  CreatedDate = GetCreatedDate((string)item.Element("created_at")),
                                  Image = GetChildElementValue(item, "user", "profile_image_url"),
                                  Id = (long)item.Element("id"),
                                  NewTweet = true,
                                  Source = (string)item.Element("source"),
                              }).ToList();

            // Load cached file and add them but only up to 200 old items
            var oldItems = Helper.LoadSetting<List<ItemViewModel>>(Constants.StatusesFileName);
            if (oldItems != null)
            {
                var maxCount = (oldItems.Count < 200) ? oldItems.Count : 200;
                for (var i = 0; i < maxCount; i++)
                {
                    oldItems[i].NewTweet = false;
                    statusList.Add(oldItems[i]);
                }
            }

            Helper.SaveSetting(Constants.StatusesFileName, statusList);

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

        private static string GetChildElementValue(XElement itemElement, string parentElement, string childElement)
        {
            var userElement = itemElement.Element(parentElement);
            if (userElement == null)
                return String.Empty;

            var iteem = userElement.Element(childElement);
            if (iteem == null)
                return String.Empty;

            return iteem.Value;
        }

        private static string GetCreatedDate(string createdDate)
        {
            DateTime date = Helper.ParseDateTime(createdDate);
            return date.ToShortDateString() + " " + date.ToShortTimeString();
        }
    }
}

The above code includes a method to get the status list from Twitter called “LoadStatuses” however I’ve made this private and exposed a generic “LoadList” method that we’ll also use to load other lists in a future post.

The “LoadStatuses” method will first check to ensure we’ve already been authorized with Twitter.  It will then call Twitter  using  path = "/statuses/friends_timeline.xml”.  I also set a couple query string parameters.  The first is the “count” which I set to get 200 posts, you may want to set this lower. The next one is “since_id” which passes in the last Tweet id I got back to tell Twitter to only give us new items since this one.   

When the call is complete the callback  method “TwitterGetStatusesCompleted” is called. If there was an error I show a messageBox, this is for debugging and should be changed in the future, probably to an error message in the returned event handler arguments.  We now can extract the data we need for each tweet that was returned in the xml.  For each tweet we get back the user name, their screen name, tweet date, tweet text and the id of the item. 

After all the new tweets are extracted and stored in our List, I then load the last stored list of statuses from IsolatedStorage if there was one (first time running this there won’t be one of course).  I then add the old items to the new list up to a maximum of 200, so that we never store more than 200 items at a time.  I also mark them as old with the “NewTweet” property by setting it to false.  Lastly I resave the file overwriting the old one if it was there and fire off the completed event to notify our ViewModel that the data has changed.

Here is the complete ViewModel code that shows how the TwitterHelper code is called and the returned data is bound back to our UI, MaInViewModel.cs file:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows;
using Twitt.Common;

namespace Twitt.ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public ObservableCollection<ItemViewModel> Items { get; private set; }

        public MainViewModel()
        {
            Items = new ObservableCollection<ItemViewModel>();
        }

        public bool IsDataLoaded
        {
            get;
            private set;
        }

        public void LoadData()
        {
            LoadData(false);
            IsDataLoaded = true;
        }

        public void Refresh()
        {
            LoadData(true);
        }

        private void LoadData(bool refresh)
        {
            LoadList(TwitterListType.Statuses, refresh);
        }

        private void LoadList(TwitterListType listType, bool refresh)
        {
            LoadList(listType, refresh, String.Empty);
        }

        private void LoadList(TwitterListType listType, bool refresh, string searchTerm)
        {
            string fileName = null;
            ObservableCollection<ItemViewModel> parentList = null;

            switch (listType)
            {
                case TwitterListType.Statuses:
                    fileName = Constants.StatusesFileName;
                    parentList = Items;
                    break;
            }

            if (String.IsNullOrEmpty(fileName))
                return;

            // If a cached file exists, bind it first then go update unless we are refreshing
            if (!refresh)
            {
                var itemList = Helper.LoadSetting<List<ItemViewModel>>(fileName);
                if (itemList != null)
                {
                    BindList(parentList, itemList);
                }
            }
           
            var twitterHelper = new TwitterHelper();
            twitterHelper.LoadList(listType, (parentList != null && parentList.Count > 0) ? parentList[0].Id : 0, searchTerm);
            twitterHelper.LoadedCompleteEvent += (sender, e) =>
            {
                var list = Helper.LoadSetting<List<ItemViewModel>>(fileName);
                if (list == null)
                {
                    Helper.ShowMessage("Error Loading Data from Twitter.");
                    return;
                }

                Deployment.Current.Dispatcher.BeginInvoke(() => BindList(parentList, list));
            };
        }

        private static void BindList(ObservableCollection<ItemViewModel> parentList, IEnumerable<ItemViewModel> list)
        {
            parentList.Clear();

            foreach (var item in list)
            {
                parentList.Add(item);
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (null != handler)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    public enum TwitterListType { Statuses, Mentions, DirectMessages, Favorites }
}

In the above code you may notice a couple helper methods such as “LoadData” and “LoadList”, I’ll be adding to these in the next post to pull in mentions, direct messages and favorites.  I’ve also added the “TwitterListType” enum which defines each of these types which we’ll use in the next blog post.

The main method above “LoadList” will call the TwitterHelper method to go and get the list from Twitter.  The list will be saved in IsolatedStorage when complete and the callback handler in “LoadList” will call “BindList” to bind the data to the control.  The nice part about saving the data to IsolatedStorage is at the next load, we can load the data immediately and then go fetch any new data, so the user will have instant access to their past tweet data.  This also helps if the user is disconnected or has low connectivity.

If you run the app you should now see a complete list of your friend statuses. 

image

In the next post we’ll extend this data to add mentions, direct messages and favorites.  I’ll also add a ProgessBar that will animate while the data is being fetched from the Twitter Service.

The complete code for this part can be found at: http://twitt.codeplex.com/

Sam

No comments:

Post a Comment