Microsoft Architect Forum、Enterprise Windows 8 開発セッションフォローアップ Part 4

皆様、こんにちは!さてPart 4、遅くなりすみません。

Part 1~Part 3ではWindows ストアアプリとWindows Azure連携デモにつき書きます。Part 1では主に今回のデモで使用された技術の基本的な使い方、Part 2では実際の実装コード(店舗管理者用ストアアプ側)、Part 3では一般ユーザー用ストアアプリ側(Mobile Services)、そしてPart 4で、Office 365およびSharePointとの連携のコード、についてご説明します。今回は、Part 4について解説します。

1.セッションスライドについて

こちらに公開してあります。

https://www.slideshare.net/shosuz/maf2013-enterprise-windows-8-architecture-for-rapid-development-of-windows-store-lob-apps 

MAF2013 Enterprise Windows 8 – Architecture for rapid development of WinRT apps from Shotaro Suzuki

 

2.デモについて

Part 1~Part 3では、Windows Azure 連携アプリ(2種類)をご紹介しました。Part4 では、SharePoint /O365 連携シナリオについて、ご紹介します。デモはこのようなものでした。

 

image

image

image

image

image

image

image 

(1) SharePointのListに登録したニュースをストアアプリに配信

このようなアプリでした。

image

image

SharePointのListに登録したニュースを配信する、Read のみ(Create/Update/Deleteなし)のアプリです。早期導入でまずはこのような形で導入を求められることも多々あります。Listは下記の通り、実際にSharePoint上で今まで通り登録・編集等を行います。

image

Model

MVVM(Model-View-ViewModel)の構成にしてあるので、Model のフォルダから見ていきます。この中に、Repositry.cs ファイルを配置し、ここで、SharePointからデータを取ってくるロジックを格納しています。

最初に、プログラム内で使う定数や変数を作成しておきます。

 private const string SHAREPOINT_BASE_URL = "https://sharepoint/sites/vwxyz/pqrstu/"; (例)
  
 private const string SHAREPOINT_ONLINE_BASE_URL = "https://abcdefg.sharepoint.com/"; (例)
  
 private static List<Product> allProducts;
  
 public static List<Product> AllProducts
 {
     get { return Repository.allProducts; }
 }
  
 private static List<ProductCategory> allCategories;
  
 public static List<ProductCategory> AllCategories
 {
     get { return Repository.allCategories; }
 }

次に、RegionとしてSharePointアクセス部分を記述しています(SharePoint Online はこれと似たような形でRegionを準備しておき、切り替えます)。

 #region SharePoint
 private static async Task<List<Product>> GetAllProductsFromSharePointAsync()
 {
     DataServiceContext context;
     var uri = SHAREPOINT_BASE_URL + "_vti_bin/listdata.svc";
     context = new DataServiceContext(new Uri(uri));
     context.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
     context.IgnoreMissingProperties = true;
  
     var query = context.CreateQuery<SharePointProduct>("Product")
                     .OrderBy(item => item.ProductID) as DataServiceQuery<SharePointProduct>;
  
     var result = await query.ExecuteAsync();
  
     var products = from sp in result
                    select new Product()
                    {
                        CategoryID = sp.CategoryID,
                        CategoryName = sp.CategoryName,
                        ProductID = sp.ProductID,
                        ProductName = sp.ProductName,
                        AbstractStr = sp.AbstractStr,
                        Description = sp.Description,
                        Price = int.Parse(sp.Price),
                        LargeImage = SHAREPOINT_BASE_URL + "ProductImage/" + sp.LargeImage,
                        SmallImage = SHAREPOINT_BASE_URL + "ProductImage/" + sp.SmallImage,
                        LocalLargeImage = "ms-appdata:///local/" + "ProductImage/" + sp.ProductID + "_Product_Large" + Path.GetExtension(sp.LargeImage),
                        LocalSmallImage = "ms-appdata:///local/" + "ProductImage/" + sp.ProductID + "_Product_Small" + Path.GetExtension(sp.SmallImage),
                        MailContact = sp.MailContact,
                        LyncContact = sp.LyncContact
                    };
  
     return products.ToList();
  
 }
 private static async Task<List<ProductCategory>> GetAllCategoriesFromSharePointAsync()
 {
     DataServiceContext context;
     var uri = SHAREPOINT_BASE_URL + "_vti_bin/listdata.svc";
     context = new DataServiceContext(new Uri(uri));
     context.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
     context.IgnoreMissingProperties = true;
  
     var query = context.CreateQuery<SharePointProductCategory>("ProductCategory")
                     .OrderBy(item => item.CategoryID) as DataServiceQuery<SharePointProductCategory>;
  
     var result = await query.ExecuteAsync();
  
     var categories = from sp in result
                      select new ProductCategory()
                      {
                          CategoryID = sp.CategoryID,
                          CategoryName = sp.CategoryName,
                          AbstractStr = sp.AbstractStr,
                          Description = sp.Description,
                          LargeImage = SHAREPOINT_BASE_URL + "ProductImage/" + sp.LargeImage,
                          ZoomOutImage = SHAREPOINT_BASE_URL + "ProductImage/" + sp.ZoomOutImage,
                          LocalLargeImage = "ms-appdata:///local/ProductImage/" + sp.CategoryID + "_Category_Large" + Path.GetExtension(sp.LargeImage),
                          LocalZoomOutImage = "ms-appdata:///local/ProductImage/" + sp.CategoryID + "_Category_ZoomOut" + Path.GetExtension(sp.ZoomOutImage),
                      };
  
     return categories.ToList();
  
 }
 private static async Task LoadSharePointImagesAsync()
 {
     var localFolder = ApplicationData.Current.LocalFolder;
  
     var folders = await ApplicationData.Current.LocalFolder.GetFoldersAsync();
  
     var imageFolder = folders.Where(f => f.Name == "ProductImage").FirstOrDefault();
  
     if (imageFolder == null)
     {
         imageFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("ProductImage");
     }
  
     var httpClient = new HttpClient(new HttpClientHandler()
     {
         UseDefaultCredentials = true
     });
  
     foreach (var cat in allCategories)
     {
         try
         {
             var largeFile = await imageFolder.CreateFileAsync(Path.GetFileName(cat.LocalLargeImage), CreationCollisionOption.FailIfExists);
             await DownloadFileAsync(httpClient, largeFile, cat.LargeImage);
         }
         catch (Exception)
         {
         }
  
         try
         {
             var zoomOutFile = await imageFolder.CreateFileAsync(Path.GetFileName(cat.LocalZoomOutImage), CreationCollisionOption.FailIfExists);
             await DownloadFileAsync(httpClient, zoomOutFile, cat.ZoomOutImage);
         }
         catch (Exception)
         {
         }
     }
  
     foreach (var pro in allProducts)
     {
         try
         {
             var largeFile = await imageFolder.CreateFileAsync(Path.GetFileName(pro.LocalLargeImage), CreationCollisionOption.FailIfExists);
             await DownloadFileAsync(httpClient, largeFile, pro.LargeImage);
         }
         catch (Exception)
         {
         }
  
         try
         {
             var smallFile = await imageFolder.CreateFileAsync(Path.GetFileName(pro.LocalSmallImage), CreationCollisionOption.FailIfExists);
             await DownloadFileAsync(httpClient, smallFile, pro.SmallImage);
         }
         catch (Exception)
         {
         }
     }
 }
 #endregion

SharePoint 2010以降では、クライアントに対してXML形式でデータを返す機能が搭載されています。これを利用すれば、SharePointに関する特別な知識がなくても、.NET の開発に関する知識さえあれば、SharePointからデータを取得してデータをアプリに表示することが可能です。

このブロックの中では、DataService という機能(最初からExecuteAsync までの部分)が特に重要です。この10行ほどのコードで、特定のURLにアクセスし、XML のデータを取ってきます。そのデータを、このアプリケーションで使えるデータに詰め替える処理を行っています。

その、SharePoint対応の(SharePointが返してくるXMLに対応するための)データクラスはこちらになります。

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
  
 namespace ProductCatalog.Data
 {
     [global::System.Data.Services.Common.DataServiceKeyAttribute("ID")]
     public class SharePointProduct
     {
         public int ID { get; set; }
         public string コンテンツタイプのID { get; set; }
         public string コンテンツタイプ { get; set; }
         public string タイトル { get; set; }
         public DateTime? 作成日時 { get; set; }
         public DateTime? 登録日時 { get; set; }
         public int? 作成者Id { get; set; }
         public int? 登録者Id { get; set; }
         public DateTime? 更新日時 { get; set; }
         public int? 更新者Id { get; set; }
         public string パス { get; set; }
         public int? Owshiddenversion { get; set; }
         public string バージョン { get; set; }
  
         public string CategoryID { get; set; }
         public string CategoryName { get; set; }
         public string ProductID { get; set; }
         public string ProductName { get; set; }
         public string AbstractStr { get; set; }
         public string Description { get; set; }
         public string Price { get; set; }
         public string LargeImage { get; set; }
         public string SmallImage { get; set; }
         public string MailContact { get; set; }
         public string LyncContact { get; set; }
     }
  
 }

Model 内で一旦こちらにデータを取得し、そのデータを、画面で使うためのデータクラス(Product.cs)に詰め替えて、ViewModel 以降ではこちらのデータクラスを利用しています。

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
  
 namespace ProductCatalog.Data
 {
     public class Product
     {
         public string CategoryID { get; set; }
         public string CategoryName { get; set; }
         public string ProductID { get; set; }
         public string ProductName { get; set; }
         public string AbstractStr { get; set; }
         public string Description { get; set; }
         public int Price { get; set; }
         public string LargeImage { get; set; }
         public string SmallImage { get; set; }
         public string LocalLargeImage { get; set; }
         public string LocalSmallImage { get; set; }
         public string MailContact { get; set; }
         public string LyncContact { get; set; }
     }
 }

ViewModel と View

次にViewModel と View に移ります。一つのViewに対して一つのViewModel を用意してあります。

image

例えばトップページにあたる画面のView(LandingPage.xaml) では、商品の一覧がカテゴライズされて、Office の商品一覧とか、Visual Studio の商品一覧とか、カテゴリー別に表示する画面となります。

image

この View の分離コード(LandingPage.xaml.cs)を見てみましょう。このView が初期表示される際に呼び出されるメソッドが、LoadState です。

 protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
 {
     viewModel = new LandingPageViewModel();
     viewModel.Initilize(null);
  
     this.DefaultViewModel["ViewModel"] = viewModel;
  
 }

この中で、初期化メソッド Initialize() メソッドが呼び出されています。ここをハイライトして、右クリック→定義に移動してみますと、下記のようなコードになっています。

 using ProductCatalog.Common;
 using ProductCatalog.Data;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
  
 namespace ProductCatalog.ViewModel
 {
     public class LandingPageViewModel
     {
         public void Initilize(object parameter)
         {
             var categories = (from c in Repository.AllCategories
                               select ViewModelUtility.CreateCategory(c)).ToList();
  
             foreach (var cat in categories)
             {
                 cat.TopItems = (from p in Repository.AllProducts
                                 where p.CategoryID == cat.CategoryID
                                 select ViewModelUtility.CreateProduct(p)
                                                     ).Take(6).ToList();
  
             }
  
             Categories = categories;
         }
  
         public List<ProductCategoryViewModel> Categories { get; set; }
     }
 }

上述した、SharePointからデータを取ってくるクラス(Repositry.cs)から、必要なデータ(AllCategories~)を、取得する処理が入っています。カテゴリーの情報、商品の詳細情報、を取り出して、画面で使いやすいような形(カテゴリーと商品の親子関係を持たせた塊のようなもの)を生成して、自らの変数に保持します。

もう一度View を見ると、データバインディングを用いて、ViewModel の中から適宜データを取り出して、GridView にバインドし、各商品の表示をしたりカテゴリーを表示したり、といった処理を行います。

例えば、次の画面に遷移するところを見てみましょう。上記のView で、商品の画像や、Office という文字の箇所をクリックした場合に、次のページに遷移するようになっています。例えば Header をクリックして、Office のカテゴリーにある商品の一覧を出したいという場合です。下記の XAML の中の下記のボタン (Category_Click) をハイライトして、

 <GridView.GroupStyle>
                         <GroupStyle>
                             <GroupStyle.HeaderTemplate>
                                 <DataTemplate>
                                     <Grid Margin="1,0,0,6">
                                         <Button
                                     AutomationProperties.Name="Group Title"
                                     Style="{StaticResource TextPrimaryButtonStyle}" Click="Category_Click">
                                             <StackPanel Orientation="Horizontal">
                                                 <TextBlock Text="{Binding CategoryName}" Margin="3,-7,5,10" Style="{StaticResource GroupHeaderTextStyle}" />
                                                 <TextBlock Text="{StaticResource TriangleGlyph}" FontFamily="Segoe UI Symbol" Margin="0,-7,0,10" Style="{StaticResource GroupHeaderTextStyle}" FontSize="20"/>
                                             </StackPanel>
                                         </Button>
                                     </Grid>
                                 </DataTemplate>
                             </GroupStyle.HeaderTemplate>

右クリックして、イベントハンドラ―へ移動を選択してください。すると下記のイベントハンドラ―に移動します。

 private void Category_Click(object sender, RoutedEventArgs e)
 {
     var element = sender as FrameworkElement;
     var category = element.DataContext as ProductCategoryViewModel;
     this.Frame.Navigate(typeof(CategoryDetailPage), category.CategoryID);
 }

この中では、今クリックされた商品のヘッダーから商品のカテゴリーを取ってきて、次のカテゴリーの詳細ページに遷移するという処理を行っています。カテゴリーの詳細ページには、CategoryID(Office、Visual Studio 等)を渡して、次の画面に遷移していきます。

カテゴリーの詳細ページ(CategoryDetailPage.xaml) では、Office であればOffice に所属する商品の一覧を表示するレイアウトになっています。

image

このページの分離コードの中にある初期化のメソッドを見ると、下記の通り記述してあります。

 protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
 {
     var categoryID = (string)navigationParameter;
  
     viewModel = new CategoryDetailPageViewModel();
     viewModel.Initilize(categoryID);
     this.DefaultViewModel["ViewModel"] = viewModel;
  
 }

このメソッドの中の Initialize をハイライトして、右クリックで定義に移動を選択すると処理内容がわかります。

 using ProductCatalog.Common;
 using ProductCatalog.Data;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
  
 namespace ProductCatalog.ViewModel
 {
     public class CategoryDetailPageViewModel
     {
         public void Initilize(object parameter)
         {
             var categoryID = (string)parameter;
             var categories = (from c in Repository.AllCategories
                               select ViewModelUtility.CreateCategory(c)).ToList();
  
  
             var category = (from c in Repository.AllCategories
                             where c.CategoryID == categoryID
                             select ViewModelUtility.CreateCategory(c)).First();
  
             category.Items = (from p in Repository.AllProducts
                               where p.CategoryID == category.CategoryID
                               select ViewModelUtility.CreateProduct(p)
                                                 ).ToList();
  
             Category = category;
             Categories = categories;
             Products = category.Items;
  
         }
  
         public ProductCategoryViewModel Category { get; set; }
         public List<ProductCategoryViewModel> Categories { get; set; }
         public List<ProductViewModel> Products { get; set; }
     }
 }

Top Page から、ある カテゴリーの ID を貰ってきて、その ID に紐づく商品の情報を、ViewModel 経由で取り出して、SharePointからデータを取ってきたデータの中から特定のカテゴリーに属するものだけをフィルタリングして、画面を構成して表示します。

テストをするには、このデモでは(後ほどビデオを見て戴ければわかりますが)、カテゴリーの名前を変更したうえで、SharePointからデータを取ってくるコード(上述 Repositry.cs )の変数 Result のところにブレイクポイントを設定してデータの中身を表示していますので、ご確認ください。

 ---  Repositry.cs ---
 ・・・
 var result = await query.ExecuteAsync();
 ・・・

image

image

SharePoint 2010 以降で、.svc サービスから公開されるデータ形式は、XML 形式になっていますので、Listのサービス(List.svc)も同様です。ストアアプリ側では OData Client を使えばよく、それは既にライブラリに入っていますので、上記 Repositry.cs の数行の処理でデータを取得する処理が可能となる訳です。

(2) SharePoint Online に登録した商品カタログをストアアプリに配信

0365連携はSharePointからデータを取って来る場合とほぼ同じですが、違うのは当然ながら認証の部分ですね。Office 365 の場合には、token を取ってきてアクセス時にその token 付の HttpClient のリクエストを送る必要があります。あとは、データを取得して保持しておく仕組みが若干異なります。このあたりは詳細は弊社エバンジェリスト、松崎のBlogでご確認ください。

Windows 8 store apps と SharePoint 2013 の連携アプリケーション開発 (認証)

Office & Office 365 Development Sample (Cloud 開発サンプル) (その他の参考)

 

image

image

 

下記に、SharePointと、O365(SharePoint Online) のそれぞれのメソッドを示します。Model 内部で簡単に切り替えることができるようになっています。

 private static async Task LoadFromSharePointAsync()
 {
     allProducts = await GetAllProductsFromSharePointAsync();
     allCategories = await GetAllCategoriesFromSharePointAsync();
     await LoadSharePointImagesAsync();
 }
  
 private static async Task LoadFromSharePointOnlineAsync()
 {
     await LoginSharePointOnlineAsync();
     allCategories = await GetAllCategoriesFromSharePointOnlineAsync();
     allProducts = await GetAllProductsFromSharePointOnlineAsync();
     await LoadSharePointOnlineImagesAsync();
 }

以上です。いかがでしたでしょうか?

このPart 4 で、Microsoft Architect Forum のセッションフォローアップは終わりにしたいと思います。この後、ビデオの公開もありますので、そちらもお楽しみに!

それではまた!

鈴木 章太郎