Udostępnij za pośrednictwem


Windowsストアアプリにおける グリッドアプリケーションについて(8)

前回まででグリッドアプリケーションの基本構造と良く行われるであろうカスタマイズ方法を説明しました。今回は、カスタマイズの具体例としてSomasegarのBlogで取り上げられていた「Build and End-toEnd Windows Store App」シリーズを補足します。

この記事を補足した方が良いと考えた理由は、作業が進むにつれて詳細を説明するのではなく、抜粋で解説しているからです。この理由から、正しく理解していないと、コードのコピー/ペーストでは動くものにならない可能性が高いので、初心者にはハードルが高いと考えたからです。以降で、それぞれのフェースに沿って補足していきます。

Getting Data

この解説で気になったのが、データソースを取得するコードなどが気になりましたので、少し詳細に解説します。
記載されていませんが、最初に考えないといけないのはサンプルデータの問題です。この記事では、SampleDataSourceのコンストラクタ内に記載されているサンプルデータを削除していると考えるべきです。サンプルデータを削除した場合の問題は、デザイナーでのデザインがし難いということです。この問題を回避するために、サンプルデータをデザイン時のみに作成するように変更すべきでしょう。具体的には、以下のようにします。

 public SampleDataSource()
{
    if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
    {
       CreateSampleData();
    }
}

void CreateSampleData()
{
       // ここにサンプルデータのコードをコピーします
}

                    
このようにすることで、デザイン用のサンプルデータを維持しながら、実行時にはサンプルデータを作成しなくなることは既に説明した通りです。
次に気になっているのが、以下のコードになります。

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

SampleDataSourceクラスは、もともとインスタンス プロパティとして「AllGrpoups」を持っており、AllGroupsフィールドを実装するのであれば、プロパティ名と同じになってしまうことから、インスタンスプロパティを削除した方が良いことになります。AllGroupsメソッドを追加しないで既存のプロパティやメソッドを使用する前提で、作成したのが次のAddGroupForFeedAsyncメソッドになります。

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

    SampleDataSource._sampleDataSource.AllGroups.Add(feedGroup);
    return true;
}

変更したのは、オリジナルをAllGroupsフィールドを、定義されている「_SampleDataSource」フィールドを使ってAllGroupsプロパティへと買い換えたことです。
次に変更するのは、GroupedItemPage.xaml.csのLoadStateメソッドになります。

 protected override async void LoadState(
          Object navigationParameter, 
          Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
     var sampleDataGroups = SampleDataSource .GetGroups((String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;
    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");
}

もともとのコードでは、「this.DefaultViewModel["Groups"] = SampleDataSource.AllGroups;」となっていたものをテンプレートが生成したコードのままにしています。この理由は、AllGroupsフィールドを追加していないためです。

WebViewコントロールへの変更

この解説で気になったのが、数点あります。それは、 XAMLの記述とHtmlSource extension propertyの記述です。私が作成したXAMLを以下に示します。

 <UserControl Loaded="StartLayoutUpdates" Unloaded="StopLayoutUpdates">
    <!--<ScrollViewer x:Name="scrollViewer" Style='{StaticResource HorizontalScrollViewerStyle}' Grid.Row='1'>-->
        <Grid >
            <Grid.RowDefinitions>
                <RowDefinition Height='Auto' />
                <RowDefinition Height='*' />
            </Grid.RowDefinitions>
            <TextBlock x:Name='itemTitle' Margin="20,10,10,10"
  
                   Text="{Binding Title}" 
                   Style='{StaticResource SubheaderTextStyle}'
                   IsHitTestVisible='False' Grid.Row='0' />
            <WebView x:Name='itemWebView' 
            local:WebViewExtension.HtmlSource='{Binding Content}'
                     Grid.Row='1' Margin="20,0,10,20"
  />
        </Grid>
    <!--</ScrollViewer>-->

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="ApplicationViewStates">
            <VisualState x:Name="FullScreenLandscape" />
            <VisualState x:Name="Filled"/>
            <VisualState x:Name="FullScreenPortrait">
            </VisualState>
            <VisualState x:Name="Snapped">
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
</UserControl>

 
もとのコードと変更しているのは、以下の2点となります。

  • Margin設定
    独自に値を変更したり、WebViewへ追加しています。この理由は、FlipViewのItemTemplateとしてWebViewを定義しているために、FlipViewのナビゲーションが操作しやすくするためです。現実的なアプリにするには、余白に関するデザインガイドラインを順守するようにすべきです。
  • VisualStateManagerの定義
    元の記事には記載されていませんが、要素を大きく変更するので削除すべきです。

後、注意すべきなのがHtmlSource Extension Propertyの記述がXAML上は、「local:WebViewExtension.HtmlSource」となっていることです。つまり、HtmlSource Extension Propertyのソースコードである「WebViewExtension.cs」を追加してから、名前空間をCallistoより変更するということです。

User Interaction

最初にAppBarの作成方法の解説がありますが、これが非常に難しいと私は考えています。AppBarをVisual Studioで作成するには、デザイン画面(ここでは、GroupedItemPage.xamlを使用します)、ドキュメントアウトラインとツールボックスを使用します。具体的な手順を以下に示します。

  • [ドキュメントアウトライン]-[]pageRoot]-[BottomAppbar]のコンテキストメニューから「アクティブなコンテナーの固定」を選択します。
    CreateAppBar
    この作業で、アクティブなコンテナーが設定されます。
  • [ツールボックス]より[AppBar」を選択して、デザイン画面にドラッグ&ドロップします。
  • [ドキュメントアウトライン]-[]pageRoot]-[BottomAppbar]のコンテキストメニューから「アクティブなコンテナーの固定」を選択します。
    この作業でアクティブなコンテナーが解除されます。
  • ドキュメントアウトラインで、作成したAppBarの中のGrid、そしてStackPanelと選択するか、XAMLでStackPanelを選択すれば、後はボタンを追加したりできるようになります。

ここで説明した方法は、GUIを使ってAppBarを作成する方法です。アクティブなコンテナーを指定する理由は、Visual Studioのデザイナーでは不可視のAppBarをドラッグ&ドロップで作成する位置(上側や下側)を指定することができないからです。もちろん、AppBarのXAMLコードをXAMLエディタで記述しても構いません。また、Bled for Visual Studio でも操作方法の考え方は同じですが、アクティブなコンテナーを使う必要はありません。この理由は、オブジェクトとタイムラインでドキュメントツリーのコントロールをドラッグできるからです。

次に解説がない作業として、Common/StandardStyle.xamlより「AddAppBarButtonStyle」の定義をApp.xamlへコピーして下さい。

 <Style x:Key="AddAppBarButtonStyle" 
          TargetType="ButtonBase" 
          BasedOn="{StaticResource AppBarButtonStyle}">
    <Setter 
          Property="AutomationProperties.AutomationId" 
          Value="AddAppBarButton"/>
    <Setter Property="AutomationProperties.Name" Value="Add"/>
    <Setter Property="Content" Value=""/>
</Style>

この作業ができてから、AppBar内の左側のStackPanelへTextBoxとButtonとProgressRingコントロールをツールボックスよりドラッグ&ドロップします。その後に、Buttonをマウスでクリックして、コンテキストメニューを使って[テンプレートの編集]-[リソースの適用]-[AddAppbarButtonStyle]をクリックして、スタイルを適用します。また、追加したコントロールの名前などを設定した結果のXAMLを以下に示します。また、ボタンをドラッグした場合は、Content属性を削除するから、プロパティウィンドウからコンテンツのリセットをして下さい。

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

オリジナルでは、TextBoxのWidthに言及はありませんが、500px程度に設定しないとURLを入力するにしても、幅が狭すぎるということになります。後は、ロジックとしてBtnAddFeed_Clickイベントハンドラー、AddFeedAsyncメソッドの実装になりますが、ここでは名前空間を使用するためにusing句を追加するから、フルネームで指定するかを行う必要があります。以下に、フルネームで指定したコードを示します。

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

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

 最後にフィードを追加している画像が以下のようにありますが、「https:///blogs.msdn.com/b/somasegar.rss.aspx」と指定してはいけません、
2.AddFeed
この理由は、somasegarのフィードは読み込んでいるので追加されることがないからです。

Enabling Live Tiles

ライブタイルを作成するコードは同じですが、注意する点は名前空間の指定となります。using句で指定するか、完全な名前で指定するようにします。

 void UpdateTitle()
{
    var groups = SampleDataSource.GetGroups("AllGroups").ToList();

    var xml = new Windows.Data.Xml.Dom.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 : ""
        ));

    Windows.UI.Notifications.TileUpdateManager
        .CreateTileUpdaterForApplication()
        .Update(new Windows.UI.Notifications.TileNotification(xml));
}

            
タイルテンプレートのTileWideText01は、テキストを5つまで指定できますが、この例では4つしか指定していません。また、TileSquarePeekImageAndText01テンプレートを使って正方形のタイルに対するライブタイルを設定していることにも注意してください。タイルに正方形とワイドを指定した場合ですが、アプリケーションから正方形とワイドのどちらかが使われているかを知る方法はありません。タイルの種類は、ユーザーが選択するものだからです。アプリケーションができることは、両方のタイルを用意した場合に、両方のタイルに対してライブタイルを設定しなかればならないということです。
次に、ライブタイルを更新するためにGroupedItemPage.xaml.csのLoadStateメソッドを変更するという記事になりますが、ここでは AddFeedAsyncメソッドを追加してから、LoadStateメソッドを変更します。

 // Live tile用に追加
async System.Threading.Tasks.Task<bool> 
             AddFeedAsync(string feed, bool tileUpdated)
{
    var added = await SampleDataSource.AddGroupForFeedAsync(feed);
    if (tileUpdated) return tileUpdated;
    return added;
}

AddFeedAsyncメソッドの目的は、フィードがある場合にライブタイルを更新できるようにするためです。次にLoadStateメソッドを以下に示します。

 protected override async void LoadState(Object navigationParameter, Dictionary pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;

    //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");

    var tileUpdated = false;
     tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/somasegar/rss.aspx", tileUpdated);
     tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/jasonz/rss.aspx", tileUpdated);
    tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/visualstudio/rss.aspx", tileUpdated);
    if (tileUpdated)     {
        UpdateTitle();
    }

赤字にしているところが、オリジナルの記事から変更している箇所になります。このコードによって、3種類のフィードのいずれかが取得できればライブタイルが更新できるようになります。

検索コントラクトを実装する説明です。Visual Studioでソリューションエクスプローラーから[追加]-[新しい項目]-[検索コントラクト]を行うことで作成したSearchResultPage.xaml.csのSearchResultsPageクラス内に、Filterインナークラスの定義があります。この定義をジェネリックに変更するのと、数か所のメンバーを変更します。変更したコードを以下に示します。

 /// <summary>
/// 検索結果の表示に使用できるフィルターの 1 つを表すビュー モデルです。
/// // ジェネリックに変更します
/// </summary>
private sealed class Filter<T> : SomaNews.Common.BindableBase
{
    private String _name;
    

      // private int _count; // 削除

     private bool _active;

    private List<T> _result; // 追加
  
     // コンストラクタを定義します
    

      public Filter(string name, IEnumerable<T> result, bool active = false)
      {        
             this.Name = name;
            this.Active = active;
            this.Results = result.ToList();
      }
     public List<T> Results // ジェネリックに変更
     {
             get { return _result; }
             set {
                     if (this.SetProperty(ref _result, value))
                     {  
                          // プロパティ変更イベントを発生させます
                          this.OnPropertyChanged("Description");
this.OnPropertyChanged("Count");

                    }
                }
    }

    public int Count
    {
        get 
        { 
            //return _count; //変更します
            return _result.Count;
         }   // セッターを削除します
        //set { if (this.SetProperty(ref _count, value)) this.OnPropertyChanged("Description"); }
    }

    public String Description
    {
        // 変更します
        //get { return String.Format("{0} ({1})", _name, _count); }
        get { return String.Format("{0} ({1})", _name, this.Count); }
    }
}

          変更箇所は、コンストラクタを変更して、Countプロパティを追加した_resultフィールドから値を算出するようにします。この変更に基づいて、関係する箇所を変更しています。このFilterクラスの役割は、検索結果の分類を表すことです。この例のRSSリーダーでは、読み込むRSSのサイト毎に検索結果を分類することです。
次に、LoadStateメソッドを以下に示します。

 protected override async void LoadState(Object navigationParameter, 
                     Dictionary<String, Object> pageState)
{
    var queryText = navigationParameter as String;

    // TODO: アプリケーション固有の検索ロジックです。検索プロセスでは、
    //       結果カテゴリのリストを作成する必要があります。
    //
    //       filterList.Add(new Filter("<フィルター名>", <結果数>));
    //
    //       アクティブな状態で開始するには、3 番目の引数として true を渡すフィルターが最初
    //       のフィルター (通常は "All") のみであることが必要です。アクティブ フィルターの
    //       結果は以下の Filter_SelectionChanged で提供されます。

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

    // 検索コントラクトによる起動時のデータソースの初期化
    var targetGroups = SampleDataSource.GetGroups("AllGroups");
    if (targetGroups.Count() == 0)     {
        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");
    }

    var filterList = new List<Filter<SampleDataItem>>(
        targetGroups         .Select(feed => 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));
    // ビュー モデルを介して結果を通信します
    this.DefaultViewModel["QueryText"] = '\u201c' + queryText + '\u201d';
    this.DefaultViewModel["Filters"] = filterList;
    this.DefaultViewModel["ShowFilters"] = filterList.Count > 1;

}

オリジナルと違うのは、起動時のデーターソースの初期化コードです。元のコードでは、SampleDataSourceに追加した静的フィールドであるAllGroupsを使用するだけですが、このコードに問題があるためです。具体的には、検索コントラクトによって起動される場合にフィードが読み込まれないことで検索結果がゼロになるという問題です。この問題を避けるために、データソースの初期化コードを追加しています。
そして、Filter_SelectionChangedイベントハンドラーを記述します。

 void Filter_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // 選択されたフィルターを確認します
    //var selectedFilter = e.AddedItems.FirstOrDefault() as Filter;
    var selectedFilter = e.AddedItems.FirstOrDefault()
                           as Filter<SampleDataItem>;
    if (selectedFilter != null)
    {
        this.DefaultViewModel["Results"] = selectedFilter.Results; // 検索結果をバインドします
         // 対応する Filter オブジェクト内に結果をミラー化し、
        // いない場合に RadioButton 表現を使用して変更を反映できるようにします
        selectedFilter.Active = true;

        // TODO: this.DefaultViewModel["Results"] をバインド可能な Image、Title、および Subtitle の
        //       バインドできる Image、Title、Subtitle、および Description プロパティを持つアイテムのコレクションに設定します
        // 結果が見つかったことを確認します
        object results;
        ICollection resultsCollection;

データソースにデータをバインドするコードだけを追加しています。後は、オリジナルの記事にあるようにresultsListView_ItemClickイベントハンドラーを記述してから、 SearchResultsPage.xaml 内のAppNameリソースを削除します。AppNameリソースを削除する理由は、App.xamlでAppNameリソースが定義してあり、この定義を利用するためになります。

Enabling Sharing

共有ソースを実装する説明です。共有コントラクトは、共有データを提供する「共有ソースコントラクト」とデータを受け取る「共有ターゲットコントラクト」の2種類に分類されます。  共有ソースコントラクトを記述するには、Visual Studio の支援機能がないので記事の通りにコードを記述することになります。このコードは、共有データの本体を除けば、ほとんど同一のコードになることから以下に示します(ItemDetailPage.xaml.cs)。

 #region 共有ソース
private DataTransferManager _dtm;

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    // イベントハンドラーを設定
    _dtm = DataTransferManager.GetForCurrentView();
    _dtm.DataRequested += _dtm_DataRequested;
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);
    // イベントハンドラーを解除
    _dtm.DataRequested -= _dtm_DataRequested;
    _dtm = null;
}

void _dtm_DataRequested(DataTransferManager sender, DataRequestedEventArgs args)
{
    var toShare = (SampleDataItem)flipView.SelectedItem;
    if (toShare != null)
    {
        args.Request.Data.Properties.Title = toShare.Title;
        args.Request.Data.Properties.Description = string.Empty;
        args.Request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(toShare.Content));
     }
}

#endregion

 
共有するデータ が異なれば、「args.Request.Data.SetHtmlFormat...」の記述を変更します。詳細は、ドキュメントを読んでください。上記のコードの注意点は、OnNavigatedFromイベントハンドラーで、DataRequestedのイベントハンドラーを忘れずに解除することです。こtれを忘れると、メモリーリークの原因になります。

Enabling Roaming

ローミングを有効するために、静的クラスとしてPersistedStateを追加しています(PersistedState.cs)。このコードを以下に示します。

 using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using SomaNews.Data;
using Windows.Storage;

namespace SomaNews
{
    internal static class PersistedState
    {
        internal static void SaveFeedUrls()
        {
            var serializer = new DataContractSerializer(typeof(List<string>));
            var feeds = SampleDataSource.GetGroups("AllGroups").Select(f => f.UniqueId).ToList();
             using (var tmpStream = new MemoryStream())
            {
                serializer.WriteObject(tmpStream, feeds);
                ApplicationData.Current.RoamingSettings.Values["feeds"] = tmpStream.ToArray();
            }
        }

        internal static IEnumerable<string> LoadFeedUrls()
        {
            if (!ApplicationData.Current.RoamingSettings.Values.ContainsKey("feeds"))
            {
                return Enumerable.Empty<string>();
            }

            var serializer = new DataContractSerializer(typeof(List<string>));
            using (var tmpStream = new MemoryStream(
                        (byte[])ApplicationData.Current.RoamingSettings.Values["feeds"]))
            {
                return (List<string>)serializer.ReadObject(tmpStream);
            }
        }
    }
}

   
オリジナルから変更しているのは、SaveFeedUrlsメソッドのデータソースの扱い方だけです。これは、AllGroups静的フィールドを追加していないためです。ローミングは、Windows ストアアプリでは標準機能として用意されているので、このコードのように簡単に記述することができます。注意点としては、データ容量に制限があることです。この理由から、この例ではフィードのURLだけを保存するようにしています。
次にフィードを保存するのと、読み込むコードを追加することです。このためにGroupedItemPage.xaml.csのLoadStateメソッドとbtnAddFeed_Clickイベントハンドラーにコードを追加します。

 protected override async void LoadState(Object navigationParameter, 
                   Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;

    var tileUpdated = false;
    if (sampleDataGroups.Count() == 0) 
 {
  foreach (var feed in PersistedState.LoadFeedUrls()) 
 {
 tileUpdated = await AddFeedAsync(feed, tileUpdated); 
 } 
 } 
 if (sampleDataGroups.Count() == 0) 
 {
 tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/somasegar/rss.aspx", tileUpdated);
 tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/jasonz/rss.aspx", tileUpdated);
 tileUpdated = await AddFeedAsync("https://blogs.msdn.com/b/visualstudio/rss.aspx", tileUpdated); }
     if (tileUpdated)
    {
        UpdateTitle();
    }

    PersistedState.SaveFeedUrls(); // フィードの保存
 }


private async void btnAddFeed_Click(object sender, RoutedEventArgs e)
{
    await AddFeedAsync(txtUrl.Text);
    PersistedState.SaveFeedUrls(); // フィードの保存
 }

赤字を見れば理解できることでしょう。この段階になれば、LoadStateメソッドで文字列でフィードを追加している所を削除しても良いかも知れません。何故なら、フィードの追加ができますし、追加したフィードをローミングデータとして保存することができるようになっているからです。

記事では、Windows Azure モバイルサービスを使って通知サービスの機能を作り込むことが解説されています。こちらに関しては、Virtuoso - Shotaro Suzuki's Blogが詳しいのでこちらを参照してください。