Condividi tramite


Building an End-to-End Windows Store App - Part 1

In previous posts, I’ve alluded to one of our key focus areas for Visual Studio 2012 being the theme of connected devices and continuous services.  This includes creating a top-notch set of tools in Visual Studio 2012 to both design and build amazing Windows apps and the services that back them.

With Windows 8 and Visual Studio 2012 released, I’ve decided to explore and document the end-to-end development of a basic Windows Store app that uses services on the backend.  For this task I’ve chosen to use Visual Studio Express 2012 for Windows 8.

My primary goal here is to highlight just how straightforward it is now to build a modern, connected experience.  For the sake of simplicity and minimizing the coding involved, I’ll build a simple RSS reader: “News by Soma.” In this two-part blog post, I’ll document the experience of building this app.  Hopefully my notes are detailed enough that you can follow along and build (and then expand upon) your own version of this app, as well.

Getting Started

Since my goal here is to showcase the full end-to-end experience, I’m starting from a Windows 8 system without Visual Studio installed.  I download the Visual Studio Express 2012 for Windows 8 installer, click through the setup wizard, and begin the installation:

Within a few minutes, I have my development environment up and running:

From File | New Project, I create a new Windows Store “Grid App (XAML)” using C#:

The Grid App template maps nicely to the scenario I have in mind, that of being able to have multiple RSS feeds (the “groups”), each of which contains multiple posts (the “items” in each group).  The template provides all of the infrastructure necessary to build such an app very quickly.  After the project is created, I press F5 to see my (not yet modified) app in action:

With the basic structure in place, I can now begin customizing it for my specific needs.

Configuring Basic App Properties

I of course want my app to look nice, so I spend a little time in Visual Studio customizing its properties via the Package.appxmanifest file.  Opening this file presents a detailed editor in Visual Studio:

Here I configure both my app’s tile (for the Windows Start page) and splash screen (displayed when the app starts) the way I want them.  This involves creating several images, which I do by starting with a picture of myself, and creating various cuts of it in Paint to fit the sizes specified in the configuration editor (e.g. 150x150 for the Logo, 310x150 for the Wide Logo, etc.):

This results in a nice tile experience when the app is pinned to my Start screen, whether using the Small tile:

or the Wide tile:

My app also now has a nice splash screen experience:

Getting Data

The Grid App template gets all of its data from a SampleDataSource type (in the file DataModel\SampleDataSource.cs) that exposes an AllGroups property which returns an ObservableCollection<SampleDataGroup>.  This enables the UI to data bind to a collection of groups, each represented by the generic data model type SampleDataGroup.  SampleDataGroup in turn contains a collection of SampleDataItem instances.

In my app, SampleDataGroup maps to RSS feeds, and SampleDataItem maps to the entries in a feed.  Rather than replace SampleDataGroup and SampleDataItem with my own custom data types, for the sake of simplicity I simply repurpose them.  The template includes on these types enough relevant properties so I don’t actually need to modify them at all; rather, I just need to modify SampleDataSource to populate and return instances of these with the right data.

There’s a fair amount of code in the SampleDataSource type included with the template, much of which is about populating the “lorem ipsum” nonsensical text items shown in the previous screenshots.  I delete all of that, and replace the AllGroups property with a simple static declaration (fixing up all of the references in the process):

public static readonly ObservableCollection<SampleDataGroup> AllGroups =
    new ObservableCollection<SampleDataGroup>();

My UI can continue binding to AllGroups, which is initially empty.  As new groups (RSS feeds) are added to AllGroups, the UI will be notified automatically of the addition and will update itself accordingly.  Therefore, I need to expose a method to add groups:

public static async Task<bool> AddGroupForFeedAsync(string feedUrl)
{
    if (SampleDataSource.GetGroup(feedUrl) != null) return false;

    var feed = await new SyndicationClient().RetrieveFeedAsync(new Uri(feedUrl));

    var feedGroup = new SampleDataGroup(
        uniqueId: feedUrl,
        title: feed.Title != null ? feed.Title.Text : null,
        subtitle: feed.Subtitle != null ? feed.Subtitle.Text : null,
        imagePath: feed.ImageUri != null ? feed.ImageUri.ToString() : null,
        description: null);

    foreach (var i in feed.Items)
    {
        string imagePath = GetImageFromPostContents(i);
        if (imagePath != null && feedGroup.Image == null)
            feedGroup.SetImage(imagePath);
        feedGroup.Items.Add(new SampleDataItem(
            uniqueId: i.Id, title: i.Title.Text, subtitle: null, imagePath: imagePath,
            description: null, content: i.Summary.Text, @group: feedGroup));
    }

    AllGroups.Add(feedGroup);
    return true;
}

Using the SyndicationClient class from Windows Runtime (WinRT), and the new async/await keywords in C#, I asynchronously download the feed at the requested URL.  I then create a SampleDataGroup to represent the feed, populating it with information about the feed from the SyndicationFeed I was handed.  And then for each item in the syndication feed, I map its properties into a new SampleDataItem.  These items are all added to the group, and then the group is added to the AllGroups collection.  With that, I’m almost done teaching the app how to get all of the data it needs.

The one remaining piece of code here has to do with images. The UI knows how to bind to SampleDataGroup and SampleDataItem, including showing an image for every group and item.  Typically, RSS feed items aren’t associated with an image, but I want something appropriate and interesting to show up in the UI for each feed item whenever possible.  As such, I have one more function that parses the RSS item looking for PNG and JPG images, returning the first one with a fully-qualified path it finds:

private static string GetImageFromPostContents(SyndicationItem item)
{
    return Regex.Matches(item.Summary.Text,
            "href\\s*=\\s*(?:\"(?<1>[^\"]*)\"|(?<1>\\S+))", 
            RegexOptions.None)
        .Cast<Match>()
        .Where(m =>
        {
            Uri url;
            if (Uri.TryCreate(m.Groups[1].Value, UriKind.Absolute, out url))
            {
                string ext = Path.GetExtension(url.AbsolutePath).ToLower();
                if (ext == ".png" || ext == ".jpg") return true;
            }
            return false;
        })
        .Select(m => m.Groups[1].Value)
        .FirstOrDefault();
}

Finally, before I can really run the app, I need one more change: to modify the GroupedItemsPage.LoadState method (in the file GroupedItemsPage.xaml.cs) to use this new SampleDataSource.AddGroupForFeedAsync method.  I replace the LoadState from the template with one line to hook up AllGroups to the UI, and add a few additional lines to initially populate the UI with a few blogs:

protected override async void LoadState(
    object navigationParameter, Dictionary<string, object> pageState)
{
    this.DefaultViewModel["Groups"] = SampleDataSource.AllGroups;

    // temporary hardcoded feeds
    await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/somasegar/rss.aspx");
    await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/jasonz/rss.aspx");
    await SampleDataSource.AddGroupForFeedAsync("https://blogs.msdn.com/b/visualstudio/rss.aspx");
}

And that’s it.  I’m now able to F5 again to see RSS data populated into my app:

Main grouped-items page:

Group page (when I click on a group header on the main page):

Item page (when I click on an item on the main or group pages):

One thing to note here is that I haven’t modified the default template for the item page yet, and it uses a RichTextBlock to display the post’s contents.  As a result, the HTML from the RSS item is displayed as the HTML source rather than as rendered content.

To make this a bit nicer, I can update the template to render the HTML.  The ItemDetailPage.xaml displays the SampleDataItem using a FlipView control, with a DataTemplate that uses a UserControl for the template item.  I replace the contents of that UserControl (which in the original code contains the RichTextBlock-related controls) with the following XAML that uses a WebView control:

<UserControl Loaded="StartLayoutUpdates" Unloaded="StopLayoutUpdates">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Margin="10,10,10,10" Text="{Binding Title}"
                   Style="{StaticResource SubheaderTextStyle}" 
                   IsHitTestVisible="false" Grid.Row="0" />
        <WebView local:WebViewExtension.HtmlSource="{Binding Content}" Grid.Row="1"/>
    </Grid>
</UserControl>

The WebView control itself doesn’t have a property that allows me to bind the WebView directly to the HTML string contentI already have, but I found code from Tim Heuer for an HtmlSource extension property that uses WebView’s NavigateToString method to achieve the same thing.  And with that addition to my project, I now see the feed item rendered nicely in the app:

User Interaction

In the previous section on Getting Data, I simply hardcoded which feeds I wanted the app to display.  However, I want to allow the user to enter such their own choices of feeds manually, so I’ll augment the template UI slightly to enable this user interaction.

One of the common design elements of a Windows Store app is an “app bar.” I’ll use an AppBar control to allow the user to enter a URL into a TextBox and click an Add button to get the feed included in the app.  I drag an AppBar control from the Visual Studio Toolbox onto the designer for my GroupedItemsPage.xaml file:

I then move the resulting XAML into a Page.BottomAppBar element so that the app bar shows up at the bottom of my app:

<Page.BottomAppBar>
    <AppBar>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <StackPanel Orientation="Horizontal"/>
            <StackPanel Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal"/>
        </Grid>
    </AppBar>
</Page.BottomAppBar>

And I add three controls to the left-aligned StackPanel:

<TextBox x:Name="txtUrl" VerticalAlignment="Center" />
<Button x:Name="btnAddFeed" Style="{StaticResource AddAppBarButtonStyle}" Click="btnAddFeed_Click" />
<ProgressRing x:Name="prAddFeed" IsActive="false" 
    Foreground="{StaticResource ApplicationForegroundThemeBrush} "/>

(Note that AddAppBarButtonStyle, along with ~140 other styles for AppBar buttons, is defined in the StandardStyles.xaml file included in the Grid App template, but it’s commented out by default.  I just uncomment it so that I can use it here.)

To complete the experience, I need to implement the btnAddFeed_Click method (in the GroupedItemsPage.xaml.cs file), wiring it up to the SampleDataSource.AddGroupForFeedAsync method I previously wrote (and, of course, removing the three lines I previously hardcoded in LoadState):

async void btnAddFeed_Click(object sender, RoutedEventArgs e)
{
    await AddFeedAsync(txtUrl.Text);
}

async Task AddFeedAsync(string feed)
{
    txtUrl.IsEnabled = false;
    btnAddFeed.IsEnabled = false;
    prAddFeed.IsActive = true;
    try
    {
        await SampleDataSource.AddGroupForFeedAsync(feed);
    }
    catch (Exception exc)
    {
        var dlg = new MessageDialog(exc.Message).ShowAsync();
    }
    finally
    {
        txtUrl.Text = string.Empty;
        txtUrl.IsEnabled = true;
        btnAddFeed.IsEnabled = true;
        prAddFeed.IsActive = false;
    }
}

With that in place, when a user brings up the app bar, types in a URL, and clicks the Add button, the feed will be added, and for the duration of the add operation, the app will display the progress ring and prevent the user from adding additional feeds.

Enabling Live Tiles

Windows 8 provides multiple mechanisms for creating live tiles on the Start screen.  For the purposes of my app, I want to update my live tile to list the current feeds in the app. 

To do this, in GroupedItemPage.xaml.cs I create a function to generate the template XML expected by the TileUpdateManager, and I use a TileUpdater to push the visuals to the tile:

private void UpdateTile()
{
    var groups = SampleDataSource.AllGroups.ToList();
    var xml = new XmlDocument();
    xml.LoadXml(
        string.Format(
            @"<?xml version=""1.0"" encoding=""utf-8"" ?>
            <tile>
                <visual branding=""none"">
                    <binding template=""TileWideText01"">
                        <text id=""1"">News by Soma</text>
                        <text id=""2"">{0}</text>
                        <text id=""3"">{1}</text>
                        <text id=""4"">{2}</text>
                    </binding>
                    <binding template=""TileSquarePeekImageAndText01"">
                        <image id=""1"" src=""ms-appx:///Assets/Logo.png"" alt=""alt text""/>
                        <text id=""1"">News by Soma</text>
                        <text id=""2"">{0}</text>
                        <text id=""3"">{1}</text>
                        <text id=""4"">{2}</text>
                    </binding>  
                </visual>
            </tile>", 
            groups.Count > 0 ? groups[0].Title : "", 
            groups.Count > 1 ? groups[1].Title : "",
            groups.Count > 2 ? groups[2].Title : ""));
    TileUpdateManager.CreateTileUpdaterForApplication().Update(new TileNotification(xml));
}

(For your own apps, the “App tiles and badges” Windows SDK Sample includes some helpful code for working with tiles.)

Then I modify GroupedItemsPage.LoadState to call this UpdateTile method after successfully adding a feed:

if (await SampleDataSource.AddGroupForFeedAsync(feed))
{
    UpdateTile();
}

Now, after starting my app and adding my blog feed and Jason Zander’s blog feed, I can see this information available on my tile:

Enabling Search

Having integrated with live tiles, the next feature I want to integrate is Search.  Windows 8 provides the Search charm that enables users to search relevant apps from anywhere in the system.

To start this, I right-click on my project in Visual Studio and select Add | New Item…, picking “Search Contract” as the item to be added:

This does a few things:

  • It updates my package manifest to declare Search:
  • It adds a new SearchResultsPage.xaml to my project.
  • It augments my App.xaml.cs with the necessary OnSearchActivated method override to correctly connect a Search request from the system with my new SearchResultsPage.xaml. 

The added SearchResultsPage.xaml (and associated SearchResultsPage.xaml.cs) already contains most of the UI and logic necessary to make this scenario work.  So as with the other templates we’ve seen Visual Studio create, I just need to plug in the logic specific to my application and its data.

The SearchResultsPage.xaml.cs file includes a simple view model type called Filter.  This type is used by the template to represent a group of search results, such that a user can see and select from multiple categories of results to quickly narrow down their choices. To simplify coding a bit, I first modify this Filter to make it Filter<T> and add a Results property to it… that way, I can perform one search and populate all of the filters, such that as the user chooses different filter categories in the UI, I don’t have to keep re-searching:

private sealed class Filter<T> : News_by_Soma.Common.BindableBase
{
    ...
    private List<T> _results;

    public Filter(string name, IEnumerable<T> results, bool active = false)
    {
        ...
        this.Results = results.ToList();
    }

    public int Count { get { return _results.Count; } }

    public List<T> Results
    {
        get { return _results; }
        set { if (this.SetProperty(ref _results, value)) this.OnPropertyChanged("Description"); }
    }
    ...
}

In the page’s LoadState override, I replace the hardcoded search results filter group that was provided in the template:

var filterList = new List<Filter>();
filterList.Add(new Filter("All", 0, true));

with code to actually do the search across each RSS feed, creating a filter for each feed, and then creating an “All” filter with aggregation of all of the results:

var filterList = new List<Filter<SampleDataItem>>(
    from feed in SampleDataSource.AllGroups
    select new Filter<SampleDataItem>(feed.Title,
        feed.Items.Where(item => (item.Title != null && item.Title.Contains(queryText)) || 
                                     (item.Content != null && item.Content.Contains(queryText))),
        false));
filterList.Insert(0, 
    new Filter<SampleDataItem>("All", filterList.SelectMany(f => f.Results), true));

Next, in the Filter_SelectionChanged, I store the results into the DefaultViewModel:

this.DefaultViewModel["Results"] = selectedFilter.Results;

I then add an ItemClick event handler to the resultsListView control from the template.  This navigates to the selected item when the user clicks on it:

private void resultsListView_ItemClick(object sender, ItemClickEventArgs e)
{
    var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}

Finally, the SearchResultsPage.xaml page contains two lines of XAML that should be deleted. The Grid App template already includes the application name in the App.xaml file, so we don’t need to also configure that here:

<!-- TODO: Update the following string to be the name of your app -->
<x:String x:Key="AppName">App Name</x:String>

With that, search is functioning in my application:

As it stands, this will only search the feeds that have been loaded into the app when the app’s main page loads.  That works great if the search is performed while the app is running.  If, however, the app isn’t running, its main page won’t have executed, and groups won’t have populated.  If I were to persist feed information, then if the app hadn’t been running when the search request arrived, I could update the search logic to first load the feeds that had been persisted.

What's Next?

In this first of two posts, I’ve explored getting up and running with Visual Studio Express 2012 for Windows 8, and using it to build a basic app that integrates with live tiles and search.  In my next post, I’ll focus on extending this application via multiple backend services. Stay tuned…

Namaste!

Comments

  • Anonymous
    August 26, 2012
    Thanks that was great!

  • Anonymous
    August 26, 2012
    The comment has been removed

  • Anonymous
    August 26, 2012
    It's still the ugliest thing I've ever seen. I'd love to do Win 8 app development, but only if I can continue to use VS2010. I'm not spending 8 hours a day in front of this ugly interface.

  • Anonymous
    August 27, 2012
    Would love to see a tutorial that take The Grid Template (like this tutorial) but uses the MVVM class to store local data to an XML File. Show the ability to add, update, Delete records in the XML file. Thank you

  • Anonymous
    August 27, 2012
    The comment has been removed

  • Anonymous
    August 27, 2012
    The comment has been removed

  • Anonymous
    August 27, 2012
    Thanks for this.

  • Anonymous
    August 28, 2012
    Really nice post! But what I am missing is a little bit more diversity regarding design, not only using the standard templates. How do you want to learn more about the "right" Modern UI? Maybe this could be included in future posts. Thank you :)

  • Anonymous
    August 28, 2012
    I can't see any of the images in the post

  • Anonymous
    August 28, 2012
    All: Thanks for the suggestions on what you'd like to see Soma blog about in the future. Arne: I'm sorry you're having trouble with the images. Can you try again?  Perhaps it was a momentary network glitch?  Has anyone else had trouble seeing the images in Soma's post?

  • Anonymous
    August 28, 2012
    @Stephen Here is what I would like to see Soma blog about, the END OF VISUAL STUDIO AND .NET along with that dreadful user interface and instead return to the Visual Studio 6.0 DNA and building on a foundation that worked. That would be far better then forcing developers into an overly bloated, expensive and ugly IDE with a weak language that serves as nothing but glue to call API wrapper libraries none of which work well together. The VS team and managers has set development back 20 years. Here is another good blog post Soma and the visual studio team resign from Microsoft and vow never to write development tools again.

  • Anonymous
    August 28, 2012
    @Dave ditto! The screen shots are depressing like at 1960's black and white television completely brutally. What brain child looked at that and said let's go with it! LOL

  • Anonymous
    August 28, 2012
    I too don't see the images.

  • Anonymous
    August 29, 2012
    The VP of Microsoft website is buggy. Ignoring bugs and destroying the UI = Totally incompentance. I agree it is time for you go.

  • Anonymous
    August 29, 2012
    I look at you all see the tile there that's sleeping While my keyboard gently weeps I look at the screen and I see it needs sweeping Still my keyboard gently weeps.

  • Anonymous
    August 29, 2012
    The comment has been removed

  • Anonymous
    August 30, 2012
    Thanks so much Soma. Much appreciated :).

  • Anonymous
    September 16, 2012
    Great post! However when I navigate to an item, then click back to main page, the Allgroups get called again from the LoadState of the main page. this.DefaultViewModel["Groups"] = SampleDataSource.AllGroups; // temporary hardcoded feeds await SampleDataSource.AddGroupForFeedAsync("blogs.msdn.com/.../rss.aspx"); So it populates all the posts for the above feed a second time into the collection. Anyone else seeing this or have a work around? Thanks, Matt

  • Anonymous
    September 16, 2012
    @Matt: Good question.  Those hardcoded feeds were just temporary; it's a bit hidden, but later in the post, the post states "To complete the experience, I need to implement the btnAddFeed_Click method (in the GroupedItemsPage.xaml.cs file), wiring it up to the SampleDataSource.AddGroupForFeedAsync method I previously wrote (and, of course, removing the three lines I previously hardcoded in LoadState):"  So while LoadState might get called again, it'll end up just loading the same feeds that were already available rather than adding additional ones.  I hope that helps.

  • Anonymous
    September 17, 2012
    Thanks Stephen for pointing that out, it makes sense to me now. I’m wondering however, how to load the collection only once on startup and not fire every time I return to the main page. Is there a page method available for this as it is not preferred in my app to click a button to load initial data? Thanks, Matt

  • Anonymous
    September 17, 2012
    I think that I may have answered my question. It seems to work when overriding the OnLaunched method from App.xaml.cs. protected override async void OnLaunched(LaunchActivatedEventArgs args) {   ….. // Ensure the current window is active             Window.Current.Activate();             // Added the following code to only run when started. Matt             await SampleDataSource.AddGroupForFeedAsync("blogs.msdn.com/.../rss.aspx"); }

  • Anonymous
    October 30, 2012
    Great Som.

  • Anonymous
    November 29, 2012
    Can you share any code samples to use xml(feeds) api's instead of rss feeds.  

  • Anonymous
    November 30, 2012
    @K Kiran: What do you mean by "XML(feeds)"?  You can use the WinRT support for XML, e.g. msdn.microsoft.com/.../windows.data.xml.dom.xmldocument.aspx , or the .NET support for XML, e.g. msdn.microsoft.com/.../hh454055.aspx , to parse XML in your Windows Store apps.

  • Anonymous
    December 12, 2012
    This is a great article! you spend good time creating it and I spent good time following your instructions! appreciate it soma! Mostafa http://www.MostafaElzoghbi.com

  • Anonymous
    March 04, 2013
    I can't see any of the images.... Are the image links broken?

  • Anonymous
    March 04, 2013
    Apologies for previous question re images. It is local to my work network...

  • Anonymous
    June 24, 2013
    hi, how can I use every grid in windows store to open a different pdf file by the group in the grid?????? pleas help me!!! Thanks if you got any solution you can contact me in this email safiya282@gmail.com