WP7 Intro to MVVM & Blendability by building Twitter App (Part 1)

This is a recap of my session with the same name from Windows Phone 7 App Showcase organized by Microsoft Gulf. I can’t write a 1-hour-to-read article to replay the session, so I’ll do this in a Hands-On-Lab manner. Best way to understand is to read & code the example.

What is MVVM?

Model: Our application domain is a Twitter App. Our models will be an instance of a Tweet”, and Twitter service itself. Got it? Great, so you build your app based on an interaction of models. After you’ve written many apps, you will realize that some of your models can be reused liked Lego blocks to build a completely different application.

View: Our Twitter App will have 2 views:

  1. Timeline View: shows the recent 20 tweets of a user and has a textbox to input the screen_name of a user.
  2. Conversation View: if I click on a Tweet from the Timeline View, I should be able to see the whole threaded conversation of that tweet.

So views are usually a screen (if your screen is complex, you may have multiple views within a screen, if it helps you to manage your app better).

ViewModel: I don’t know why they call it this. I guess Controller was already taken (Model-View-Controller), and Presenter was already taken too (Model-View-Presenter). So Microsoft Architect John Gossman wrote that ViewModel means “Model of a View”. Still Confused? Here we go again:

In our Twitter App, the ViewModel of Timeline View will hold a collection of Tweets, which Screen_name the tweets belong to, and what is the Current Selected Tweet. The ViewModel of Timeline View will also hold stuff known as Commands: action that happens when you click “Get Timeline” button and select a Tweet item (when events are triggered).

The ViewModel of Conversation View will hold the Source Tweet and a collection of Tweets (the conversation thread). It will not have any Command, because Conversation View is a read-only view, you can only see a Conversation or press Back to go back to the Timeline View.

What The Heck Moment

If you have read this far, you’re thinking of 2:

  1. Cool, let’s get to the coding now man.
  2. What the heck, why would I build an application like that?

If you’re at No. 2, I hope you at least scan through Microsoft Architect John Gossman Introducing MVVM link I gave you above.

Read it? Good, so now you know that Expression Blend is created using MVVM. Let’s see some of these views:

01   02

The Left View is from the Project Explorer View, if you right click a project you can Add a New Item.

The Right View is from the Top Menu View, if you click File, you can Add a New Item.

Both Views can use the same Command: AddNewItemCommand.

Now the Top Menu View on the right image may have a different ViewModel than the Project Explorer View on the left image, so the interaction goes like this:

  1. The TopMenuViewModel sends a Message to the ExplorerViewModel saying that New Item is clicked.
  2. The ExplorerViewModel has a context of CurrentSelectedProject, and invoke the internal AddNewItem command.

Let’s see another example:

05   06

The Left image is the ObjectExplorerView and the Right image is the VisualPreviewView.

Now they both can share the same ViewModel!

You can Select a UI Element on both views, and when you right-click, the same list of Commands are shown.

The ViewModel for both will have a CurrentSelectedObject state and ShowAvailableActions Command. Since both are the same, it doesn’t make sense to create 2 ViewModels for each View.

 

MVVM Light Toolkit

From the above passage, I gave a peek into the internals of MVVM: sending Messages from one ViewModel to another ViewModel and hooking up View to Commands in ViewModel. To make implementing MVVM easier, MVVM framewoks exist. In this post, I will use Laurent Bugnion’s MVVM Light Toolkit.

 

** PLEASE FOLLOW THIS LINK TO INSTALL MVVM LIGHT TOOLKIT BEFORE PROCEEDING **

 

Setup MVVM Twitter App Project Structure

Once you installed MVVM Light Toolkit (make sure you install the Project Templates and Code Snippets because I’ll be using them), fire up Visual Studio and Create New Project.

  1. From the template choose MvvmLight (WP7) and type MvvmTwitter for the project name.
    SS003
  2. Open up ViewModel/MainViewModel.cs , edit the ApplicationTitle and PageName, and delete the Welcome property:
    public class MainViewModel : ViewModelBase
    
    {
    
        public string ApplicationTitle
    
        {
    
            get
    
            {
    
                return "MVVM TWITTER";
    
            }
    
        }
    
        public string PageName
    
        {
    
            get
    
            {
    
                return "Timeline";
    
            }
    
        }
  3. Let’s start from the Timeline View. Double-click MainPage.xaml and arrange the UI to look like below (1 textbox, 1 button, 1 listbox):

    SS004

    Basically I just changed the TitlePanel and ContentGrid to the following:
    <!--TitlePanel contains the name of the application and page title-->
    
    <StackPanel x:Name="TitlePanel"
    
            Grid.Row="0"
    
            Margin="0">
    
      <TextBlock x:Name="ApplicationTitle"
    
              Text="{Binding ApplicationTitle}"
    
              Style="{StaticResource PhoneTextNormalStyle}" Margin="0" />
    
      <TextBlock x:Name="PageTitle"
    
              Text="{Binding PageName}"
    
              Margin="0"
    
              Style="{StaticResource PhoneTextGroupHeaderStyle}" />
    
                
    
        <StackPanel Orientation="Horizontal">
    
            <TextBox Width="290" />
    
            <Button Content="Get Timeline" />
    
        </StackPanel>
    
                
    
    </StackPanel>
    
    <!--ContentPanel - place additional content here-->
    
    <Grid x:Name="ContentGrid"
    
        Grid.Row="1">
    
                
    
        <ListBox />
    
    </Grid>

 

Twitter App Models

From the UI, we plan to fill the Listbox with a list of Tweets. So before continuing, we need to have the Tweet model defined.

  1. Create a class Tweet.cs under the Model folder of the project structure. Below is the code:
    namespace MvvmTwitter.Model
    
    {
    
        public class Tweet
    
        {
    
            public string Id { get; set; }
    
            public string Message { get; set; }
    
            public string InReplyToId { get; set; }
    
            public string ScreenName { get; set; }
    
            public string ImageUrl { get; set; }
    
        }
    
    }
    

    From the Twitter Timeline API, we know that each tweet has an ID and each tweet that was a response to previous tweet has an <in_reply_to_status_id> info.

 

Blendability

Now here’s the fun part: I want to design the UI as early as possible without waiting for the Twitter Service Layer to be completed first.

  1. Let’s put on our Designer Hat, and right-click MainPage.xaml to Open in Expression Blend.
    What do you, as a Designer, see? Exactly, nothing.
  2. Let’s put our Developer Hat back, return to Visual Studio, and open up MainViewModel.cs. Code as below:
    public class MainViewModel : ViewModelBase
    
    {
    
        public string ApplicationTitle
    
        {
    
            get
    
            {
    
                return "MVVM TWITTER";
    
            }
    
        }
    
        public string PageName
    
        {
    
            get
    
            {
    
                return "Timeline";
    
            }
    
        }
    
        public ObservableCollection<Tweet> _tweets;
    
        public ObservableCollection<Tweet> Tweets
    
        {
    
            get { return _tweets; }
    
            set
    
            {
    
                    _tweets = value;
    
                    RaisePropertyChanged("Tweets");
    
            }
    
        }
    
        /// <summary>
    
        /// Initializes a new instance of the MainViewModel class.
    
        /// </summary>
    
        public MainViewModel()
    
        {
    
            if (IsInDesignMode)
    
            {
    
                // Code runs in Blend --> create design time data.
    
                var sampleData = TweetSample.Generate();
    
                var list = new ObservableCollection<Tweet>();
    
                foreach (var d in sampleData)
    
                    list.Add(d);
    
                // Setting this property will trigger
    
                // update to the UI due to 2-way binding
    
                Tweets = list;
    
            }
    
            else
    
            {
    
                // Code runs "for real"
    
            }
    
        }

  3. Create a new folder “Design” under project structure, and add new class called TweetSample.cs. Code as below:
    using System.Collections.Generic;
    
    using MvvmTwitter.Model;
    
    namespace MvvmTwitter.Design
    
    {
    
        public static class TweetSample
    
        {
    
            public static IList<Tweet> Generate()
    
            {
    
                var list = new List<Tweet>();
    
                for (int i = 0; i < 10; ++i)
    
                {
    
                    var tweet = new Tweet
    
                                    {
    
                                        Message = "Lorem ipsum dolor sit amet, " +
    
                                                  "consectetuer adipiscing elit. " +
    
                                                  "Maecenas porttitor congue massa. " +
    
                                                  "Fusce posuere, magna sed pulvinar ultricies",
    
                                        ScreenName = "zeddyiskandar",
    
                                        ImageUrl =
    
                                            "http://a0.twimg.com/profile_images/643829395/zeddy_profile_photo_normal.jpg",
    
                                    };
    
                    list.Add(tweet);
    
                }
    
                return list;
    
            }
    
        }
    
    }
    

  4. Right-click project and Build. Any new Property we write for the ViewModel must be Built first to make them viewable for DataBinding.
  5. Go back to Blend, let’s DataBind the ListBox to the Tweets ObservableCollection, shall we? Select the ListBox from the Preview, find the ItemsSource property and click Advanced Options next to it. A popup menu will appear and select Data Binding:
    07
  6. From the DataContext, select Tweets:
    SS005
  7. As soon as you click OK, your preview gets updated with sample data!
    SS006
  8. Now all you need is modify the DataTemplate.
    Right-click ListBox –> Edit Additional Templates –> Edit Generated Items (ItemTemplate) –> Create Empty:

    08
  9. Click OK when asked about creating New Data Template.
    - Delete the Grid, and replace it with StackPanel w/ Horizontal Orientation
    - Inside the above StackPanel, add an Image and a StackPanel w/ Vertical Orientation
    - Inside the last StackPanel, add 2 TextBlocks
    SS007
  10. DataBind the Image to ImageUrl, TextBlock top to Message, and TextBlock bottom to ScreenName. Voila! You see some sample data appearing now.
  11. Style the Timeline View as you fit. Below is my XAML:
    <phone:PhoneApplicationPage.Resources>
    
      <DataTemplate x:Key="DataTemplate1">
    
        <StackPanel Orientation="Horizontal" Margin="0,0,0,15">
    
          <Image Height="48" Width="48" Margin="0,10,20,0" Source="{Binding ImageUrl}" VerticalAlignment="Top"/>
    
          <StackPanel>
    
            <TextBlock TextWrapping="Wrap" Text="{Binding Message}" Width="365"/>
    
                    <TextBlock x:Name="textBlock" TextWrapping="Wrap" Text="{Binding ScreenName}">
    
                        <TextBlock.Foreground>
    
                          <SolidColorBrush Color="{StaticResource PhoneAccentColor}"/>
    
                        </TextBlock.Foreground>
    
                    </TextBlock>
    
          </StackPanel>
    
        </StackPanel>
    
      </DataTemplate>
    
    </phone:PhoneApplicationPage.Resources>
    
    <!--LayoutRoot contains the root grid where all other page content is placed-->
    
    <Grid x:Name="LayoutRoot"
    
          Background="Transparent" Margin="0">
    
      <Grid.RowDefinitions>
    
        <RowDefinition Height="Auto" />
    
        <RowDefinition Height="*" />
    
      </Grid.RowDefinitions>
    
      <!--TitlePanel contains the name of the application and page title-->
    
      <StackPanel x:Name="TitlePanel"
    
                Grid.Row="0"
    
                Margin="0">
    
        <TextBlock x:Name="ApplicationTitle"
    
                  Text="{Binding ApplicationTitle}"
    
                  Style="{StaticResource PhoneTextNormalStyle}" Margin="10,0,0,0" />
    
        <TextBlock x:Name="PageTitle"
    
                  Text="{Binding PageName}"
    
                  Margin="10,0,0,0"
    
                  Style="{StaticResource PhoneTextGroupHeaderStyle}" />
    
                
    
            <StackPanel Orientation="Horizontal">
    
                <TextBox Width="290" />
    
                <Button Content="Get Timeline" />
    
            </StackPanel>
    
                
    
      </StackPanel>
    
      <!--ContentPanel - place additional content here-->
    
      <Grid x:Name="ContentGrid"
    
            Grid.Row="1" Margin="12,0,0,0">
    
                
    
            <ListBox ItemsSource="{Binding Tweets}" ItemTemplate="{StaticResource DataTemplate1}" />
    
      </Grid>
    
    </Grid>
    

    And the screenshots in both Blend and Visual Studio:
    SS008   SS009

We have just accomplished BLENDABILITY, where your designer can work on the UI without having to Run the project and call Twitter.com just to fill the data.

 

Runtime Data (Calling Twitter.com finally)

What we have provided is Design-time data. Now let’s finish up this Timeline View by coding the real deal, calling Twitter.com API.

  1. Open up MainViewModel.cs and code as below:
    - Add a RelayCommand<string> called GetTimelineForUser. This means a Command relayed from the UI with one parameter of type string (the ScreenName/UserName)
    - Initialize GetTimelineForUser in the constructor runtime block
    - Code the logic of calling Twitter API and converting from XML to Tweet model object
    public class MainViewModel : ViewModelBase
    
    {
    
        public string ApplicationTitle
    
        {
    
            get
    
            {
    
                return "MVVM TWITTER";
    
            }
    
        }
    
        public string PageName
    
        {
    
            get
    
            {
    
                return "Timeline";
    
            }
    
        }
    
        #region DATA
    
        public ObservableCollection<Tweet> _tweets;
    
        public ObservableCollection<Tweet> Tweets
    
        {
    
            get { return _tweets; }
    
            set
    
            {
    
                _tweets = value;
    
                RaisePropertyChanged("Tweets");
    
            }
    
        }
    
        #endregion
    
        #region COMMANDS
    
        public RelayCommand<string> GetTimelineForUser { get; private set; }
    
        #endregion
    
        /// <summary>
    
        /// Initializes a new instance of the MainViewModel class.
    
        /// </summary>
    
        public MainViewModel()
    
        {
    
            if (IsInDesignMode)
    
            {
    
                // Code runs in Blend --> create design time data.
    
                var sampleData = TweetSample.Generate();
    
                var list = new ObservableCollection<Tweet>();
    
                foreach (var d in sampleData)
    
                    list.Add(d);
    
                // Setting this property will trigger
    
                // update to the UI due to 2-way binding
    
                Tweets = list;
    
            }
    
            else
    
            {
    
                // Code runs "for real"
    
                GetTimelineForUser = new RelayCommand<string>( CallTwitterApi );
    
            }
    
        }
    
        private void CallTwitterApi(string screenName)
    
        {
    
            var wc = new WebClient();
    
            wc.DownloadStringCompleted += DownloadStringCompleted;
    
            var uri = new Uri("http://api.twitter.com/1/statuses/user_timeline.xml?screen_name=" + screenName);
    
            wc.DownloadStringAsync(uri);
    
        }
    
        private void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
    
        {
    
            if (e.Error != null) return;
    
            var list = new ObservableCollection<Tweet>();
    
            var tweets = TwitterApi.GetTimelineFromXml(e.Result);
    
            foreach (var t in tweets)
    
                list.Add(t);
    
            Tweets = list;
    
        }
    

  2. Add a new class called TwitterApi.cs to the Model folder of the project.
    Add reference System.Xml.Linq so we can transform XML using LINQ.
    Code as below:
    using System.Collections.Generic;
    
    using System.Linq;
    
    using System.Xml.Linq;
    
    namespace MvvmTwitter.Model
    
    {
    
        public static class TwitterApi
    
        {
    
            public static List<Tweet> GetTimelineFromXml(string xml)
    
            {
    
                var xmlTweets = XElement.Parse(xml);
    
                var lstTweets = (from tweet in xmlTweets.Descendants("status")
    
                                 select new Tweet
    
                                            {
    
                                                Id = tweet.Element("id").Value,
    
                                                Message = tweet.Element("text").Value,
    
                                                InReplyToId = tweet.Element("in_reply_to_status_id").Value,
    
                                                ScreenName = tweet.Element("user").Element("screen_name").Value,
    
                                                ImageUrl = tweet.Element("user").Element("profile_image_url").Value,
    
                                            }
    
                                ).ToList();
    
                return lstTweets;
    
            }
    
        }
    
    }

 

Binding Event To Command

So we want to bind the Click event of the button to the GetTimelineForUser command. We also want to bind the textbox input as a parameter to the GetTimelineForUser command.

  1. Build the Project so that the Command will be visible for DataBinding, then Edit MainPage.xaml in Blend.
  2. Find the EventToCommand behavior under Assets tab, Behaviors filter:
    SS010
  3. Drag the EventToCommand behavior to the Button object.
    09
  4. In the Command Property of EventToCommand, click Advanced Options –> Data Binding –> Data Context –> GetTimelineForUser RelayCommand
    SS011
  5. In the CommandParameter Property of EventToCommand, click Advanced Options –> Data Binding –> Element Property –> Find the Textbox input –> Text Property
    SS012

 

RUN The Timeline View

That’s it, now go back to Visual Studio and press F5.

Type any screenName you know and click the Button. You should see your Listbox is filled with Recent 20 Tweets of that user.

*RATE LIMIT: Twitter limits 150 requests from the same IP Address. If you get Error w/ HTTP Status 400, that means your IP has reached the limit and you need to wait for 1 hour.

 

10

 

Look Ma, NO Code-Behind

Your MainPage.xaml.cs should be as clean as below:

using Microsoft.Phone.Controls;
namespace MvvmTwitter
{
    public partial class MainPage : PhoneApplicationPage
    {
        // Constructor
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

 

What that means is now you can change your UI, and just update the Bindings to the Commands. No more copy-pasting from one code-behind to another code-behind.

 

Conversation View

Part 2 will continue to build the Conversation Thread View of this MVVM Twitter app. It’s 3am in the morning and I need to sleep now :)

In the Conversation View you will see how Messaging works for communication between ViewModels.

 

Download Source Code

Need a working sample to compare? Download from my Skydrive below:

Share this post: | | | |
Published Thursday, June 23, 2011 12:00 AM by zeddy
Filed under:

Comments

No Comments
Powered by Community Server (Commercial Edition), by Telligent Systems