Mar 24, 2010

Programming Windows Phone 7

Even though Windows Phone 7 programming model was published just a little over week ago at MIX '10, it's generated a lot of interest. And in my opinion, it's deserved. WP7 programming model is based on Silverlight 3.0, so if you know how to program it, you know how to program WP7. There are of course some limitations, but on the other hand some more freedoms. For example, cross domain limitations are not enforced.

As this is my first Silverlight app, I think the result proves that even a caveman can program WP7 (though probably not as well as more experienced SL programmers :)

-==- System requirements -==-

Windows Phone Developer Tools CTP

Contains:
  • Visual Studio 2010 Express for Windows Phone CTP
  • Windows Phone Emulator CTP
  • Silverlight for Windows Phone CTP
  • XNA 4.0 Game Studio CTP
Silverlight 4.0 RC You don't need to have pre-installed Visual Studio 2010 RC. With the expression version of VS2010 you can develop everything needed for WP7 and run it with the Emulator. Though, if you have VS2010 RC you can also use it to develop WP7 apps. For more design approach, you should download and install Expression Blend 4.

-==- Starting the project -==-

Firstly, we'll start by creating a Windows Phone List Application. The default template contains a master/detail app, with a list (obviously) and a simple detail form.



You should also notice that the template also created two ViewModels for us. To keep things more straightforward, I'm mostly going to skip the MVVM pattern usage.



-==- Result -==-

What the desired result should look like, is a three window (and one "popup") application. 1. The list of feeds 2. The list of feed items inside a selected feed 3. The contents of the post in the selected feed item and 4. "Add a feed" popup window.

-==- List of feeds -==-

The model:

public class Feed
{
 public string Name { get; set; }
 public string URL { get; set; }
}

And we'll change the MainViewModel to use the Feed model instead of the default ItemViewModel.

public class MainViewModel : INotifyPropertyChanged
{
 public MainViewModel()
 {
  // initialize the list
  Items = new ObservableCollection();
 }

 public ObservableCollection Items { get; private set; }
}

After we have the Model and ViewModel parts of the MVVM pattern done, we'll start on the view. If you're experienced with Silverlight, you already know that to support the designer, you have to provide some sample data so that the design can feel authentic. In the MainPage.xaml we have an attribute

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

that tell's the designer (Visual Studio or Expression Blend) what kind of data the designer should be shown. The MainViewModelSampleData.xaml contains a simple xaml file filled with sample data. We'll change it to fit our needs:

<mainviewmodel xmlns:local="clr-namespace:WP7RSSReader" xmlns:model="clr-namespace:WP7RSSReader.Model" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">    
    <mainviewmodel.items>
        <feed name="Toni Kielo's blog" url="http://tonikielo.blogspot.com/feeds/posts/default"></feed>
        <feed name="Scott Hanselman's ComputerZen.com" url="http://feeds.feedburner.com/ScottHanselman"></feed>
        <feed name="JohnPapa.net" url="http://feeds.johnpapa.net/JohnPapa"></feed>
    </mainviewmodel.items>
</mainviewmodel>

From Expression Blend, you can automatically generate datasets for almost any need. This of course works on normal Silverlight projects as well. Now that we have some design time data, we'll change the actual view (MainPage.xaml). The template model had Line1, Line2 and Line3 properties, so we'll change the view's databinding to fit our model:

<listviewitem details="{Binding URL}" layout="TextAndDetailsWithIcon" text="{Binding Name}"></listviewitem>

After this, we can see what the actual layout with out sample data will look like:



Now we can run the application. And remember, you don't have to close the emulator. Just stop the debug process, code some more and start the debug process again and the emulator will refresh the application. As you can see, even though the app looks nice on the design canvas, we don't have any actual data in our app, so the list is empty. You can pre-populate some default feeds in the MainViewModel if you like, but we'll need to create a component that will let the user add new feeds.



-==- New Feed dialog -==-

For this, we'll take a dependency to System.Windows.Controls.dll (which is included in the source). It's not included with the WP7 sdk, but contains Silverlight 3 classes that we can use.

<childwindow d:designheight="800" d:designwidth="480" height="406" mc:ignorable="d" width="312" x:class="WP7RSSReader.Dialogs.NewFeed" xmlns:basics="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:navigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <grid background="{StaticResource PhoneBackgroundBrush}" x:name="LayoutRoot">
        <grid.rowdefinitions>
            <rowdefinition height="*"></rowdefinition>
        </grid.rowdefinitions>
        <grid grid.row="0" x:name="TitleGrid">
            <grid.projection>
                <planeprojection></planeprojection>
            </grid.projection>
            <textblock text="Enter a new feed name and url" x:name="textBlockPageTitle"></textblock>
            <textbox height="32" horizontalalignment="Left" margin="76,62,0,0" name="textBox1" text="{Binding Path=Name, Mode=TwoWay}" verticalalignment="Top" width="219"></textbox>
            <textbox height="32" horizontalalignment="Left" margin="76,127,0,0" name="textBox2" text="{Binding Path=URL, Mode=TwoWay}" verticalalignment="Top" width="217"></textbox>
            <button click="OKButton_Click" content="OK" horizontalalignment="Left" margin="1,197,0,0" name="OKButton" type="submit" verticalalignment="Top" width="126">
            </button><button click="CancelButton_Click" content="Cancel" horizontalalignment="Left" margin="145,197,0,0" name="CancelButton" type="submit" verticalalignment="Top">
            <textblock foreground="White" height="47" horizontalalignment="Left" margin="5,74,0,0" name="textBlock1" text="Name" verticalalignment="Top" width="74"></textblock>
            <textblock foreground="White" height="47" horizontalalignment="Left" margin="6,140,0,0" name="textBlock2" text="URL" verticalalignment="Top" width="74"></textblock>
        </button></grid>
    </grid>
</childwindow>

Which will render like this




And we'll wire up the onclick events in the codebehind:

private void OKButton_Click(object sender, RoutedEventArgs e)
{
 this.DialogResult = true;
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
{
 this.DialogResult = false;
}

After this, we can add a button to our main page and then we can simply open the dialog from button click event:

private void button1_Click(object sender, RoutedEventArgs e)
{
 NewFeed newFeed = new NewFeed();
 newFeed.DataContext = new Feed();
 newFeed.Show();
 newFeed.Closed += new EventHandler(newFeed_Closed);

}

void newFeed_Closed(object sender, EventArgs e)
{
 NewFeed dialog = sender as NewFeed;
 if (dialog.DialogResult.HasValue && dialog.DialogResult.Value)
 {
  var feed = dialog.DataContext as Feed;

  if (feed != null && !string.IsNullOrEmpty(feed.URL))
  {
   (this.DataContext as MainViewModel).Items.Add(feed);
  }
 }
}




We have now achieved points 1 and 4 in our result list, so let's move on to point 2, the feed item list.

-==- Added: Storing our feeds -==-

(Strange thing. I tried previously using IsolatedStorage on my devbox, but it didn't work, but on my laptop it works.) So here's how it's done.

First, we'll need to serialize our data to the IsolatedStorage. IsolatedStorage works basically like a file system, so we can just create a new file and use a stream writer to save the contents. XML serialization (which you've probably used before) isn't available on SL3 (and therefore in WP7), we'll have to use a different serializer and I chose to use DataContractSerializer. It basically works the same way as any other serializer and you can of course replace it with anything you like to use, for example Json.Net.

Let's mark our class as DataContract serializable.

[DataContract]
public class Feed
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public string URL { get; set; }

    // ...
}

After we've marked the class, we can simply serialize and deserialize the content from IsolatedStorage

public static void Write(ObservableCollection list) {
  using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
  {
    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("feeds.dat", System.IO.FileMode.OpenOrCreate, isf))
    {
        DataContractSerializer dcs = new DataContractSerializer(typeof(ObservableCollection));
        dcs.WriteObject(stream, list);
    }
  }
}

public static ObservableCollection Read()
  using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
  {
    if (!isf.FileExists("feeds.dat")) return null;

    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("feeds.dat", System.IO.FileMode.OpenOrCreate, isf))
    {
        DataContractSerializer dcs = new DataContractSerializer(typeof(ObservableCollection));
        return dcs.ReadObject(stream) as ObservableCollection;
    }
  }
}


So that our app state is always saved, we'll register the Observable list's "CollectionChanged" event and wire it up to save our collections.

Then we can simply get the list from IsolatedStorage whenever we initialize our view model or if we can't find anything in IS, we can simply give some default feeds.

Now if you go look at our main page in designer, you'll see that it's throwing an exception. Why? Because when we are in design mode, the designer will try to instantiate the data context. This won't work if we have an IsolatedStorage code active in it, so we have to skip the IS usage if we are in design mode. This can be achieved simply by

if (!DesignerProperties.IsInDesignTool)
{
    Items = IsolatedStorageUtility.Read();
}

In a simple application like this isn't too much of an nuisance, but with larger application you'll have to device a smarter way for doing this.

I recommend checking out Prism and Silverlight.FX for instantiating view models through Inversion of control containers.

-==- Feed items -==-

Of course, we have to actually download the item data, so let's do that next.

Add System.ServiceModel.Syndication.dll to your references. You need to get this from SL3 assemblies, as this doesn't come in the box in WP7. (Included in the source also). If you have already installed the Silverlight 3 SDK, you can find the binaries in
C:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Client

Remember to update our model and add a List to our Feed class. Then we can create a new ViewModel for the item list:

public class FeedItemListModel
{
 public Feed Feed { get; set; }
}

When the user selects an feed we wan't to download it's contents and move to a feed list page.

private void ListBoxOne_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
 // Capture selected item data
 _selectedItem = (sender as ListBox).SelectedItem;

 var feed = _selectedItem as Feed;

 WebClient http = new WebClient();
 http.DownloadStringCompleted += new DownloadStringCompletedEventHandler(http_DownloadStringCompleted);
 http.DownloadStringAsync(new Uri(feed.URL), feed);

 // Here we could show a loading image.
}

void http_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
 var feed = e.UserState as Feed;

 XmlReader reader = XmlReader.Create(new StringReader(e.Result));
 SyndicationFeed rssFeed = SyndicationFeed.Load(reader);

 feed.Items = rssFeed.Items.ToList();

 // Start page transition animation
 PageTransitionList.Begin();
}

private void PageTransitionList_Completed(object sender, EventArgs e)
{
 // Set datacontext of details page to selected listbox item
 NavigationService.Navigate(new Uri("/FeedItemList.xaml", UriKind.Relative));
 FrameworkElement root = Application.Current.RootVisual as FrameworkElement;
 root.DataContext =
  new FeedItemListModel { Feed = _selectedItem as Feed };
}


FeedItemList.xaml doesn't exist yet, so let's create it. Here we can simply copy the MainPage, as it already closely fits the bill and modify it a little. Things we have to change:
- Class name to FeedItemList (both xaml and codebehind)
- ListBox ItemsSource="{Binding Feed.Items}"
- ListViewItem Text="{Binding Title.Text}" Details="{Binding LastUpdatedTime}"
- Create a "back" transition in addition to the already existing PageTransitionList transition.

If you want to have a design experience here also, you can create a new sample dataset and set it in xaml

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

Wiring everything up on the codebehind is the same as in MainPage, except remove everything pertaining to moving to the FeedItemList (of course) and replace it with moving to details view:

private void ListBoxOne_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
 // Capture selected item data
 _selectedItem = (sender as ListBox).SelectedItem as SyndicationItem;

 // Start page transition animation
 PageTransitionDetails.Begin();
}

void PageTransitionDetails_Completed(object sender, EventArgs e)
{
 // Set datacontext of details page to selected listbox item
 NavigationService.Navigate(new Uri("/DetailsPage.xaml", UriKind.Relative));
 FrameworkElement root = Application.Current.RootVisual as FrameworkElement;
 root.DataContext = _selectedItem;
}

Remember also to wire both transitions

PageTransitionDetails.Completed += new EventHandler(PageTransitionDetails_Completed);
PageTransitionListBack.Completed += new EventHandler(PageTransitionListBack_Completed);

That's about it for feed items list.

-==- Details (post) view -==-

We'll use the original Details view as a template for this, and simply replace the TextBlock with a WebBrowser component and databind the title to SyndicationItem's Title.Text property.

<webbrowser grid.row="1" isscriptenabled="True" x:name="browser"></webbrowser>

With WebBrowser component we have two choices. Either navigate to an URL or "navigate" (show) to a given html string.

void  DetailsPage_Loaded(object sender, RoutedEventArgs e)
{
 var item = DataContext as SyndicationItem;

 var link = item.Links.FirstOrDefault().Uri;

 var content = item.Content as TextSyndicationContent != null ? (item.Content as TextSyndicationContent).Text : null;

 var summary = item.Summary != null ? item.Summary.Text : null;

 if (string.IsNullOrEmpty(content) && string.IsNullOrEmpty(summary))
 {
  browser.Navigate(link);
 }
 else
 {
  if (!string.IsNullOrEmpty(content))
   browser.NavigateToString(string.Format(ContentWrapper, content));
  else
   browser.NavigateToString(string.Format(ContentWrapper, summary));
 }
}

I didn't test this thoroughly, but still found out that there are a few syntax differences to feeds. You can find the content either from Summary property or from Content property (or possibly elsewhere).



-==- Bugs -==-

I didn't have enough time to detail this app, so a few bugs remain:
- Going back from details to feed item list results in empty list. Don't know why it doesn't work as the transition from item list to feed list is similar and works normally.
- Post viewing in browser is quite horrible experience. The page is maximized and when you zoom it (double tap/click) it'll zoom too close. Perhaps a html layout wrapper will help with this.

-==- Resources -==-

I recommend watching MIX'10 videos [http://live.visitmix.com/videos]. Also check out the MVVM parts.

WP7 teams blog

And Scott Hanselman already posted a list of resources.

-==- Source -==-

.. can be downloaded from here.

-==- Conclusions -==-

Even though this was my first Silverligh app, this seemed to be pretty easy. Most of the things I did on the sample, I'm pretty sure I could've done lot easier. Expression Blend will ease the graphical development a lot; hopefully you've got a graphical designer for that part, if you're as bad at it as I am :)
Here's for hoping many apps to WP7 appstore.

Added: now that I got the IsolatedStorage working I really wish there was an easy way to do the swipe/delete (like in iPhone) in WP7 lists. Still, this is a CTP and I'm sure this is just a subset stuff that'll be supported on the RTM.

0 comments:

Post a Comment