WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WCF サービス経由で非接続型データアクセス

by WebSurfer 2018年3月4日 14:41

型付 DataSet + TableAdapter を用いて非接続型データアクセスを行う Windows Forms アプリケーションで、WCF サービス経由で SQL Server のデータの取得更新を行う例の紹介です。(Windows Forms アプリから直接 SQL Server に接続するのではなく)

DataGridView で表示

接続先を Web サービスとした場合の例は先の記事「Web サービス経由で非接続型データアクセス」に書きました。

Microsoft によると、Web サービスは "Legacy Technology" なので Windows Communication Foundation (WCF) を使うようにとのことです。なので、この記事では、Web サービスに代えて WCF サービスを使い、さらにサービス参照を作成する際に生成される非同期版メソッドを使って、非同期にアクセスするサンプルを作ってみました。

Visual Studio のデータソース構成ウィザードを使うと、MSDN ライブラリの Windows フォーム アプリケーションでのデータへの接続 の図のような構成の Windows アプリケーションを作成できます。具体的な作成手順例は、チュートリアル「10 行でズバリ !! 非接続型のデータ アクセス」を見てください。

上に紹介したチュートリアルの例では Windows アプリから直接 SQL Server に接続していますが、セキュリティ上の問題などで、Windows アプリ ⇔ サーバー ⇔ SQL Server というようにサーバーを介してアクセスしたいということがあると思います。

Web サーバー (IIS) 上で動いている ASP.NET Web Forms アプリケーションに WCF サービスを実装し、Windows Forms アプリケーションとの間で型付 DataSet をやりとりすることで Windows アプリ ⇔ Web サーバー ⇔ SQL Server という構成を実現できます。

(1) WCF サービス

まず WCF サービスを IIS 上で動いている既存の ASP.NET Web Forms アプリに実装します。Visual Studio の「ソリューションエクスプローラー」から[追加(D)]⇒[新しい項目の追加(W)...]で表示される「新しい項目の追加」ダイアログで[WCF サービス]を選択して追加します。

WCF サービスの追加

ここでは Web サイトプロジェクトのアプリケーションルート直下に WCF サービスを追加し、その名前を ProductsService.svc とするという前提で話を進めますので留意してください。

追加すると、IProductsService.cs, ProductsService.cs が App_Code フォルダに、アプリケーションルート直下に ProductsService.svc が生成されます。(Web アプリケーションプロジェクトでは生成されるファイルの場所と名前が違いますが基本は同じです)

次に、Visual Studio のデータソース構成ウィザードを使って、SQL Server のサンプルデータベース Northwind の Products テーブルをベースに、型付 DataSet + TableAdapter + TableAdapterManager を生成します。ここでは、名前を ProductsDataSet.xsd としています。

その手順は割愛しますが、一つだけ注意点を書くと、Web サイトプロジェクトの場合、.xsd ファイルは App_Code フォルダに作るのではなく、同一ソリューション内に別プロジェクトを作ってそこに作るようにした方がよさそうです。Temporary ASP.NET Files フォルダに生成される .Designer.cs ファイルのコードが読めないという不都合のほか、自分の環境ではなぜか TableAdapterManager が生成されないという問題がありましたので。

以上が完了したら、(a) SQL Server から型付 DataSet にデータを取得して Windows Forms アプリケーション渡すメソッド、(b) ユーザーが編集済みの型付 DataSet を受け取って SQL Server のレコードを更新するメソッドを ProductsService.cs に実装します。

具体例は以下のようになります。GetDataSet メソッドが上記 (a)、Update メソッドが上記 (b) に該当します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using NorthwindProducts;
using NorthwindProducts.ProductsDataSetTableAdapters;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class ProductsService : IProductsService
{
  public ProductsDataSet GetDataSet()
  {
    // 非同期実行を確認するため追加(実環境では不要)
    System.Threading.Thread.Sleep(3000);

    ProductsDataSet dataset = new ProductsDataSet();
    ProductsTableAdapter adapter = new ProductsTableAdapter();
    adapter.Fill(dataset.Products);
    return dataset;
  }

  public int Update(ProductsDataSet dataset)
  {
    // 非同期実行を確認するため追加(実環境では不要)
    System.Threading.Thread.Sleep(3000);

    TableAdapterManager manager = new TableAdapterManager();
    manager.ProductsTableAdapter = new ProductsTableAdapter();
    return manager.UpdateAll(dataset);
  }
}

上記コードの ProductsDataSet, ProductsTableAdapter, TableAdapterManager は、Visual Studio のデータソース構成ウィザードを使って自動生成させた .xsd ファイル下の .Designer.cs に含まれる型付 DataSet + TableAdapter + TableAdapterManager です。

自動生成された IProductsService.cs ファイルに含まれるインターフェイス IProductsService の定義も上記に合わせて書き換えます。具体的には以下の通りです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using NorthwindProducts;

[ServiceContract]
public interface IProductsService
{
    [OperationContract]
    ProductsDataSet GetDataSet();

    [OperationContract]
    int Update(ProductsDataSet dataset);
}

(2) Windows Formsアプリケーション

上記の Web サービスを Windows Forms アプリケーションでは以下のように利用できます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace UpdateUsingWcfService
{
  public partial class Form2 : Form
  {
    private ProductsServiceReference.
                         ProductsServiceClient client;
    private ProductsServiceReference.
                         ProductsDataSet productsDataSet;

    public Form2()
    {
      InitializeComponent();
      InitializeComponent2();
      this.client = new ProductsServiceReference.
                              ProductsServiceClient();
    }

    // 初期画面で Products テーブルのレコード一覧を表示
    private async void Form2_Load(object sender, EventArgs e)
    {
      // WCF サービスより型付 DataSet を取得

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.GetDataSetResponse>
      ProductsServiceReference.GetDataSetResponse response =
                           await this.client.GetDataSetAsync();

      // 目的の型付 DataSet は以下のようにして取得できる
      this.productsDataSet = response.GetDataSetResult;

      // 結果を DataGridView に表示
      this.productsBindingSource.DataSource = 
                                         this.productsDataSet;
    }

    //[データの保存]クリックでユーザーの編集結果を DB に保存
    private async void productsBindingNavigatorSaveItem_Click(
                                    object sender, EventArgs e)
    {
      this.Validate();
      this.productsBindingSource.EndEdit();

      // Web サービスに渡すのはユーザーが編集した行のみで可
      ProductsServiceReference.ProductsDataSet ds =
                 (ProductsServiceReference.ProductsDataSet)
                             this.productsDataSet.GetChanges();

      // 編集済みの型付 DataSet を Web サービスに渡して更新

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.UpdateResponse>
      ProductsServiceReference.UpdateResponse response = 
                             await this.client.UpdateAsync(ds);

      // 目的の戻り値(更新レコード数)は以下のようにして取得
      int n = response.UpdateResult;

      // 更新後の型付 DataSet を取得し DataGridView 書き換え

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.GetDataSetResponse>
      ProductsServiceReference.GetDataSetResponse response2 = 
                            await this.client.GetDataSetAsync();

      // 目的の型付 DataSet は以下のようにして取得できる
      this.productsDataSet = response2.GetDataSetResult;

      // 結果を DataGridView に表示
      this.productsBindingSource.DataSource = 
                                     this.productsDataSet;
    }

    // Form1.Designer.cs のコードにあるフォーム上の各コン
    // トロールの変数宣言をコピー。ProductsDataSet はサー
    // ビス参照の定義を使うのでその部分はコメントアウト。
    // TableAdapter と TableAdapterManager は不要(WCF サ
    // ービスを使う)なのでコメントアウト

    //private ProductsDataSet productsDataSet;
    private System.Windows.Forms.
                         BindingSource productsBindingSource;
    // ・・・以下省略・・・

    // Form1.Designer.cs のコードにある InitializeComponent()
    // メソッドのコードをコピー。メソッド名がダブらないように変
    // 更し、DataSet, TableAdapter, TableAdapterManager 関係の
    // コードをコメントアウト。Form.Text, Form.Name が "Form1"
    //  になっているので必要に応じて修正 
    private void InitializeComponent2()
    {
      this.components = new System.ComponentModel.Container();

      // BindingNavigator が使う画像が Form1.resx にしかないので、
      // ここは Form1 のままにしておく
      System.ComponentModel.ComponentResourceManager resources = 
         new System.ComponentModel.
                        ComponentResourceManager(typeof(Form1));
      // ・・・以下省略・・・

    }
  }
}

まず、Visual Studio のデータソース構成ウィザードを使って、上記 (1) WCF サービスを作った時と全く同様に、SQL Server のサンプルデータベース Northwind の Products テーブルをベースに、型付 DataSet + TableAdapter + TableAdapterManager を生成します。

生成できると「データソース」ウィンドウに Products テーブルが表示されているはずですので、それをデサイン画面で Form1 にドラッグ&ドロップすると DataGridView, BindingSource, BindingNavigatorなどのコードが自動生成され、直接 SQL Server とやり取りする Windows Forms アプリが完成するはずです。その段階で Form1 が期待通り動くことを確認してください。

確認出来たら、次に空の Form2 を追加し、それに上記 (1) WCF サービスを経由して SQL Server にアクセスして Form1 と同等の操作ができるコードを実装します。その結果が上記のコードです。具体的な手順は以下の通りです。

Visual Studio のサービス参照の追加ウィザードを利用して、上記 (1) で作成した Web サービスを参照し、サービスプロキシを自動生成させます。上記のコード例の ProductsWebServiceClient, ProductsDataSet, GetDataSetAsync, UpdateAsync 等は自動生成された Reference.cs ファイルに定義されています。

次に、Form1.Designer.cs にあるコントロールの定義と初期化のコードで必要なものを From2.cs にコピーします。上のサンプルで書いたように一式コピーして不要な部分をコメントアウトするのが間違いなくて良いと思います。

最後に、Form1.cs のイベントハンドラのコードを Form2.cs にコピーし、中身を上のサンプルのように書き換えます。

非同期版メソッド xxxAsync を利用する場合はその戻り値に注意してください。

同期版のメソッド GetDataSet、Update の場合、戻り値は (1) WCF サービスで定義した通り、それぞれ ProductsDataSet 型、int 型の値が直接返されます。

しかしながら、非同期版メソッド GetDataSetAsync、UpdateAsync の場合、戻り値はそれぞれ Task<ProductsServiceReference.GetDataSetResponse>、Task<ProductsServiceReference.UpdateResponse> となります。

即ち、Task<T> の T は ProductsDataSet 型、int 型ではなく、<operationName>Response という型のオブジェクトになります。そのクラス定義は、サービス参照を追加すると自動生成される Reference.cs というファイルに含まれています。

<operationName>Response オブジェクトから目的のオブジェクトを取得するのは、その中の public フィールド <operationName>Result にアクセスして可能です。

何故そういうことになるのかは調べ切れてません。また、ホントにその方法で良いのかと言われるとかなり不安があります。(汗) とりあえずそうすれば取得できるし非同期で動くということで上の記事は書きましたが、もう少し調べてみます。

Tags: , ,

.NET Framework

WCF サービスの非同期呼び出し

by WebSurfer 2018年2月28日 16:47

Visual Studio 2015 Community で WCF のサービス参照を追加して生成されるサービスプロキシには非同期版のメソッドも含まれ、それを利用すれば async / await キーワードを付与することで簡単に非同期呼び出しができるという話を書きます。

WCF のクライアント

何を今さらと言われるかもしれませんが、自分としては新発見で、これは備忘録として残しておかねばと思いましたので。(笑)

上の画像は Microsoft のチュートリアル「10 行でズバリ!! [C#] WCF サービスの作成と利用」をベースに作った WPF クライアントアプリです。string 型のデータを応答として返す WCF サービスを呼び出して、その応答を表示しています。

上のチュートリアルにある通り、クライアントアプリから WCF サービスを呼び出すのに利用するサービスプロキシを生成するため、サービス参照の追加を行います。

その際、「サービス参照の追加」ダイアログで[詳細設定(V)...]をクリックすると表示される「サービス参照設定」ダイアログを見ると、Visual Studio 2015 Community ではデフォルトで以下の画像の設定になっています。

サービス参照の追加

この設定画面で[非同期操作の生成を許可する(P)]にチェックが入っていて、[タスクベースの操作を生成する(T)]が選択されているのがポイントのようです。自信度はあまり高くないですが・・・(汗)

ダイアログ右上の ? マークをクリックすると表示されるヘルプを読んでも何のことだかよく分かりませんでしたが、Microsoft のドキュメント「同期操作と非同期操作」を読むと、その中の「タスク ベースの非同期パターン」が相当するのではないかと思いました。

(Visual Studio 2010 Professional では[非同期操作の生成(G)]というのがありますが、デフォルトではチェックは入っていません。未確認ですが、.NET 4.5 以降でしか使用できない async / await キーワードを利用して使うものではなさそうです)

サービス参照の追加が完了すると、以下の画像の通り、同期版の GetData メソッドに加えて非同期版の GetDataAsync メソッドも生成されているのがインテリセンスの表示から分かります。

インテリセンスの表示

クライアントアプリからは、async / await キーワードを使って非同期版メソッドを呼び出せば、時間がかかる WCF サービスから応答が返ってくるまで UI がブロックされるということはなくなります。

それを試すには以下のようにします。

WCF サービス

まず、WCF のメソッドに 3 秒の待ち時間を入れます。これにより、同期の場合は呼び出し側の WPF アプリは 3 秒ブロックされますが、非同期の場合はブロックされないことを確認できます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;

// このサービスをホストする ASP.NET アプリの web.config
// で aspNetCompatibilityEnabled="true" と設定されている
// 場合は以下の属性が必要
[AspNetCompatibilityRequirements(RequirementsMode = 
             AspNetCompatibilityRequirementsMode.Allowed)]
public class Service : IService
{
  public string GetData(int value)
  {
    // 非同期実行を確認するため追加
    System.Threading.Thread.Sleep(3000);

    return string.Format("You entered: {0}", value);
  }

  // CompositeType は IService.cs に自動生成されたクラス
  public CompositeType 
      GetDataUsingDataContract(CompositeType composite)
  {
    // 非同期実行を確認するため追加        
    System.Threading.Thread.Sleep(3000);
        
    if (composite == null)
    {
      throw new ArgumentNullException("composite");
    }

    if (composite.BoolValue)
    {
      composite.StringValue += "Suffix";
    }

    return composite;
  }
}

WPF クライアントアプリ

呼び出し側のクライアントアプリでは以下のようにします。一番上の画像で、[同期呼び出し]ボタンのクリックイベントのハンドラが button_Click で、[非同期呼び出し]の方が button1_Click です。

// 同期
private void button_Click(object sender, RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = client.GetData(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = client.GetDataUsingDataContract(composite);
  label1.Content = composite.StringValue;
}

// 非同期
// async void を async Task に変更すると、メソッドとデリゲート
// 型との間に互換性が無いというエラーになる
private async void button1_Click(object sender, 
                                         RoutedEventArgs e)
{
  var client = new TestService.ServiceClient();
  var composite = new TestService.CompositeType();
  label.Content = await client.GetDataAsync(12345);
  composite.BoolValue = checkBox.IsChecked == true;
  composite.StringValue = textBox.Text;
  composite = 
    await client.GetDataUsingDataContractAsync(composite);
  label1.Content = composite.StringValue;
}

[同期呼び出し]ボタンをクリックすると上の画像のアプリは WCF サービスから応答が返ってくるまで操作できませんが、[非同期呼び出し]ボタンクリックなら操作可能で応答も期待通り返ってくることが確認できました。こんな簡単なことでホントにいいのか・・・という不安はありますが。(汗)

あと、Microsoft の文書「非同期プログラミングのベストプラクティス」には「async void メソッドよりも async Task メソッドを利用する」ということが書いてありますが、それはできなかったです(上のコードのコメントに書いたようにエラーになります)。解決方法があるのかどうかわかりませんが、分かったら追記することにします。

Tags: , ,

.NET Framework

HttpClient で WCF サービスを呼出

by WebSurfer 2018年2月24日 23:49

先の記事「HttpWebRequest で WCF サービスを呼出」では、JSON 文字列をデータとしてやり取りする WCF サービスのメソッドを HttpWebRequest / HttpWebResponse を利用して呼び出して JSON 文字列のデータを取得し、それを���シリアル化して C# のオブジェクトに変換する方法を書きました。

HttpWebRequest / HttpWebResponse に代えて HttpClient を利用し、同じ WCF サービスのメソッドを呼び出してデータを取得するサンプルを書きます。 (呼び出し先の WCF サービスは「WCF と jQuery AJAX」を参照)

参考にしたのは Call a Web API From a .NET Client (C#) という記事ですので合わせて見ていただくと良いと思います。

まず、先の HttpWebRequest / HttpWebResponse を利用した記事と同様に、以下のクラス / データコントラクト定義を行います。

[DataContract]
public class RootObject
{
    // GetCarsByDoorsResult は WCF サービスメソッドに付与した
    // BodyStyle = WebMessageBodyStyle.WrappedRequest による
    // ラップの名前(ラップするのはセキュリティ対策)
    [DataMember]
    public List<Car> GetCarsByDoorsResult { get; set; }
}

[DataContract]
public class Car
{
    [DataMember]
    public string Make { get; set; }

    [DataMember]
    public string Model { get; set; }

    [DataMember]
    public int Year { get; set; }

    [DataMember]
    public int Doors { get; set; }

    [DataMember]
    public string Colour { get; set; }

    [DataMember]
    public float Price { get; set; }
}

参考にした Microsoft の記事ではデシリアライズに HttpContentExtensions.ReadAsAsync<T> メソッド (HttpContent) を使っていますが、今回の例のような入れ子になったオブジェクトはデシリアライズできないようです。

なので、先の記事と同様、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズすることにしました。

HttpClient.SendAsync メソッドを利用して、JSON 文字列を WCF サービスの GetCarsByDoors(int doors) メソッドに POST 送信します。以下のコードの例では 5 ドア車を要求しています。

応答の HttpContent が HttpClient.SendAsync メソッドの戻り値として返されますので、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズします。

詳しくは以下のサンプルコードとそれに付与したコメントを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading.Tasks;
using System.Net.Http;
using System.IO;

namespace ConsoleAppHttpClientWCF
{
  class Program
  {
    static void Main(string[] args)
    {
      // GetCarsAsync メソッドの引数で 5 ドアを要求
      RootObject rootObject =
          GetCarsAsync(5).GetAwaiter().GetResult();
      ShowCars(rootObject);
    }

    static async Task<RootObject> GetCarsAsync(int doors)
    {
      using (HttpClient httpClient = new HttpClient())
      {
        string url = "http://.../GetCarsByDoors";

        // POST 送信を指定
        HttpRequestMessage request =
          new HttpRequestMessage(HttpMethod.Post, url);

        // POST 送信する JSON 文字列
        string postData = 
            "{\"doors\":" + doors.ToString() + "}";

        // Content-Type: application/json; charset=utf-8 が
        // 要求ヘッダに必要。それを POST 送信する JSON 文字
        // 列と共にここで設定
        request.Content = new StringContent(postData, 
                                          Encoding.UTF8, 
                                          "application/json");

        var response = await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
        {
          // JSON シリアライザの初期化
          DataContractJsonSerializer ser =
            new DataContractJsonSerializer(typeof(RootObject));

          // 応答のコンテンツを逆シリアル化して C# の
          // オブジェクトを取得
          RootObject rootObject =
              (RootObject)ser.ReadObject(responseStream);

          return rootObject;
        }                
      }
    }

    // コンソールに取得結果を書き出すためのヘルパメソッド
    static void ShowCars(RootObject rootObject)
    {
      foreach (Car car in rootObject.GetCarsByDoorsResult)
      {
        Console.WriteLine("Make:{0}, Model:{1}, Doors:{2}",
                          car.Make, car.Model, car.Doors);
      }
    }
  }

  /*
  結果は:
  Make:Audi, Model:A4, Doors:5
  Make:Ford, Model:Focus, Doors:5
  Make:Renault, Model:Laguna, Doors:5
  Make:Toyota, Model:Previa, Doors:5
  */
}

DataContractJsonSerializer クラスを利用したシリアル化 / 逆シリアル化については、MSDN ライブラリの記事「方法 : JSON データをシリアル化および逆シリアル化する」が参考になると思います。

なお、本題とは直接関係ないことですが、You're using HttpClient wrong and it is destabilizing your software によると、HttpClient の初期化と Dispose を繰り返すようなことをすると、socket が浪費されるという問題があるそうです。これは覚えておいた方がよさそうです。

Tags: ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年10月  >>
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar