Wednesday, October 6, 2010

Building a ‘real’ Windows Phone 7 Twitter App Part 5 – Adding Mentions, DMs & Favorites

 

In today’s post I’m going to show how to extend the work we did in part 4 to add mentions (Tweet’s with your name in it), direct messages to you and favorites.  I’ll also show how to add a ProgressBar to show users the progress while we’re downloading the tweets.

The first set of changes will make are to the MainPage.xaml file.  We’ll be adding 3 new parorama items and also the progress bar.

Let’s start with the progress bar.  I’m going to wrap the progress bar and panorama control in a StackPanel:

<Grid x:Name="LayoutRoot" Background="Transparent">
    <StackPanel>
        <ProgressBar x:Name="ProgressBar" VerticalAlignment="Top" IsIndeterminate="{Binding ProgressBarIsIndeterminate, Mode=OneWay}" Visibility="{Binding ProgressBarVisibility, Mode=OneWay }"/>
 
        <!--Panorama control-->
        <controls:Panorama Title="Twitt">
        …

        </controls:Panorama>
    </StackPanel>
</Grid>

Here you can see the “StackPanel” node at the top of the xaml followed by the “ProgressBar”.  I’m going to bind the visibility and IsIndeterminate properties to our ViewModel.  In MainViewModel.cs let’s add these two new properties “ProgressBarIsIndeterminate” and “ProgressBarVisibility”:

private Visibility _progressBarVisibility = Visibility.Collapsed;
public Visibility ProgressBarVisibility
{
    get
    {
        return _progressBarVisibility;
    }
    set
    {
        if (value != _progressBarVisibility)
        {
            _progressBarVisibility = value;
            NotifyPropertyChanged("ProgressBarVisibility");
        }
    }
}

private bool _progressBarIsIndeterminate;
public bool ProgressBarIsIndeterminate
{
    get
    {
        return _progressBarIsIndeterminate;
    }
    set
    {
        if (value != _progressBarIsIndeterminate)
        {
            _progressBarIsIndeterminate = value;
            NotifyPropertyChanged("ProgressBarIsIndeterminate");
        }
    }
}

Now let’s add the 3 new parorama items to “MainPage.xaml”.  You should notice they are almost identical to the “statuses” item we added in part 4.  The only difference is the "Header” which it the title for the panorma item as well as the binding property.

<controls:PanoramaItem Header="mentions">
    <ListBox Margin="0,0,-12,32" ItemsSource="{Binding MentionItems}">
        <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>

<controls:PanoramaItem Header="messages">
    <ListBox Margin="0,0,-12,32" ItemsSource="{Binding DirectMessageItems}">
        <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>

<controls:PanoramaItem Header="favorites">
    <ListBox Margin="0,0,-12,32" ItemsSource="{Binding FavoriteItems}">
        <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}"/>
                        <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 above 3 items have 3 new binding properties “MentionItems”, “ DirectMessageitems” and “FavoriteItems”.  These are just references to the same ItemViewModel data structure which we’ll add next to the MainViewModel.cs and instantiate them in the constructor.  These items will each hold their corresponding tweet list data:

public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<ItemViewModel> Items { get; private set; }
    public ObservableCollection<ItemViewModel> MentionItems { get; private set; }
    public ObservableCollection<ItemViewModel> DirectMessageItems { get; private set; }
    public ObservableCollection<ItemViewModel> FavoriteItems { get; private set; }

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

SampleData

To get our sample data to work within our xaml we have to add these new items to our “MainViewModelSampleData.xml” file.  These are just copies of the statuses sample data what we already added in part 4 except they are using  the new ViewModel items we defined above:

<ViewModels:MainViewModel.MentionItems>
    <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"/>
</ViewModels:MainViewModel.MentionItems>

<ViewModels:MainViewModel.DirectMessageItems>
    <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"/>
</ViewModels:MainViewModel.DirectMessageItems>

<ViewModels:MainViewModel.FavoriteItems>
    <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"/>
</ViewModels:MainViewModel.FavoriteItems>

You should now be able to see the data in the xaml editor:

image

We are now ready to get the real data from Twitter.  We will first modify the “MainViewModel.cs” file to build upon what we added in Part 4. 

We are going to use our progress bar here, we need to first add some variables to track when the 4 lists (statuses, mentions, DMs & favorites) are complete.  When they are all complete we want to turn off the progress bar. 

At the top of the file add these variables which will be used to sync each of the 4 calls to Twitter.

private static readonly Object ThisLock = new Object();
private int _syncCount;
private const int SyncItemCount = 4;

Now change the “LoadData” method to reset the “_syncCount”, turn the progress bar on and call the LoadList method for each.

private void LoadData(bool refresh)
        {
            _syncCount = SyncItemCount;
            ProgressBarIsIndeterminate = true;
            ProgressBarVisibility = Visibility.Visible;

            LoadList(TwitterListType.Statuses, refresh);
            LoadList(TwitterListType.Mentions, refresh);
            LoadList(TwitterListType.DirectMessages, refresh);
            LoadList(TwitterListType.Favorites, refresh);
        }

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

 

Now in the “LoadList” method add the new lists to our switch statement.

        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;
                case TwitterListType.Mentions:
                    fileName = Constants.MentionsFileName;
                    parentList = MentionItems;
                    break;
                case TwitterListType.DirectMessages:
                    fileName = Constants.DirectMessagesFileName;
                    parentList = DirectMessageItems;
                    break;
                case TwitterListType.Favorites:
                    fileName = Constants.FavoritesFileName;
                    parentList = FavoriteItems;
                    break;
            }

            if (String.IsNullOrEmpty(fileName))
               return;

           // If 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) =>
            {
                CheckCount();

                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));
            };

I’ve also added a helper method “CheckCount”, to the callback.  This method will sync all 4 callbacks from Twitter after they are called and turn off the progress bar when they are done.  I’ve used a simple thread lock here since the callbacks will come back on different threads.

private void CheckCount()
        {
            lock (ThisLock)
            {
                _syncCount--;
                if (_syncCount == 0)
                {
                    Deployment.Current.Dispatcher.BeginInvoke(() =>
                    {
                        ProgressBarIsIndeterminate = false;
                        ProgressBarVisibility = Visibility.Collapsed;
                    });
                }
            }
        }

Notice above that list item in the “LoadList” method has a constant filename associated with it.  So we need to add these new constants to the Constants.cs file:

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";
}

 

The last part left is the calls to the Twitter API.  These will follow the same pattern I used in part 4 for the “LoadStatuses” method.  Here’s the new additions in TwitterHelper.cs.  The first addition is to the “LoadList” method to load the appropriate method for each list.

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

Next we’ll add each list method and their callback functions.  Here is the code for the first one to get “Mentions”:

public void LoadMentions(long sinceId)
{
    if (!_authorized)
    {
        if (LoadedCompleteEvent != null)
            LoadedCompleteEvent(this, EventArgs.Empty);

        return;
    }

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

    request.AddParameter("count", MaxCount);

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

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

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

private void TwitterGetMentionsCompleted(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 mentionsList = (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.MentionsFileName);
    if (oldItems != null)
    {
        var maxCount = (oldItems.Count < 200) ? oldItems.Count : 200;
        for (var i = 0; i < maxCount; i++)
        {
            oldItems[i].NewTweet = false;
            mentionsList.Add(oldItems[i]);
        }
    }

    Helper.SaveSetting(Constants.MentionsFileName, mentionsList);

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

 

Here’s similar code to get direct messages:

public void LoadDirectMessages(long sinceId)
{
    if (!_authorized)
    {
        if (LoadedCompleteEvent != null)
            LoadedCompleteEvent(this, EventArgs.Empty);

        return;
    }

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

    request.AddParameter("count", MaxCount);

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

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

private void TwitterGetDirectMessagesCompleted(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 mentionsList = (from item in xmlElement.Elements("direct_message")
                        select new ItemViewModel
                        {
                            UserName = GetChildElementValue(item, "sender", "screen_name"),
                            DisplayUserName = GetChildElementValue(item, "sender", "name"),
                            TweetText = (string)item.Element("text"),
                            CreatedDate = GetCreatedDate((string)item.Element("created_at")),
                            Image = GetChildElementValue(item, "sender", "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.DirectMessagesFileName);
    if (oldItems != null)
    {
        var maxCount = (oldItems.Count < 200) ? oldItems.Count : 200;
        for (var i = 0; i < maxCount; i++)
        {
            oldItems[i].NewTweet = false;
            mentionsList.Add(oldItems[i]);
        }
    }

    Helper.SaveSetting(Constants.DirectMessagesFileName, mentionsList);

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

 

Here’s the last code to get favorites:

public void LoadFavorites()
{
    if (!_authorized)
    {
        if (LoadedCompleteEvent != null)
            LoadedCompleteEvent(this, EventArgs.Empty);

        return;
    }

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

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

private void TwitterGetFavoritesCompleted(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 list = (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();

    if (list.Count > 0)
    {
        Helper.SaveSetting(Constants.FavoritesFileName, list);
    }

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

That’s it, rebuild the app now and run it.  Once you’ve entered in your Twitter Account information (if you haven’t yet), you should see the progress bar moving at the top, while new data is fetched.  After it’s been run at least once the next time you launch it, the cached data will be displayed for all lists then an update will happen to get new tweets.

Here’s a complete list of what the UI should now look like:

image  image image  imageimage

In the next post I’ll show how to handle selecting a tweet to display on it’s on page.  I’ll also translate any links in the tweets to real links and place the tweet in a web browser control.  This page will also allow you to reply, re-tweet or direct message the user, using the tweet page we already created in a previous post.

Download part 5 source code from: http://twitt.codeplex.com/

Sam

No comments:

Post a Comment