WebSurfer's Home

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

MVC で Chart を利用する方法

by WebSurfer 2018年5月23日 16:51

ASP.NET Web Forms アプリ用のサーバーコントロールである Chart を MVC アプリで利用する方法を書きます。

MVC アプリで Chart 利用

一般的に、Web Forms 用のサーバーコントロールは、MVC でも ViewState とポストバックを使わない範囲であれば利用できます。

Chart の場合、要求を受けてグラフの画像ファイルを生成するという操作には ViewState とポストバックは不要ですので、MVC でも利用できます。

問題は Chart が生成した画像ファイルを、どのようにブラウザが取得して表示できるかということです。

ASP.NET Web Forms アプリの場合、Chart が生成した画像ファイルはサーバーの c:\TempImageFiles フォルダに一時保存されます。そして、.aspx ページ上で Chart を配置した位置に img 要素がレンダリングされ、その src 属性に画像ファイルを取得するための HTTP ハンドラ設定されます。

ブラウザがページを受信すると、ブラウザは img 要素の src 属性に設定された HTTP ハンドラをサーバーに要求します。要求を受けたサーバーは HTTP ハンドラを使って一時保存された画像ファイルを取得してブラウザに送信し、ブラウザは受信した画像を img 要素の位置に表示するという仕組みになっています。詳しくは、先の記事「Chart」を見てください。

MVC アプリでも .aspx 形式の View を使う場合は Web Forms アプリと同等なことができると思いますが(未検証・未確認です)、最近の主流らしい Razor 形式の View の場合は img 要素のレンダリングが問題です。

ではどうするかと言うと、Web Forms アプリのような HTTP ハンドラは利用せず、Controller のアクションメソッドで画像をダウンロードするようにし、img 要素は自分で View に書いて、その src 属性にアクションメソッドの url を設定してやります。

アクションメソッドでは、動的に Chart を生成し、Chart.SaveImage メソッドを使って画像データを MemoryStream に保存し、Controller.File メソッド (Byte[], String) で画像のバイト列を応答ストリームに書き出すようにします。

先の記事「Chart」と同じ SQL Server のデータから、同じグラフ画像を生成してダウンロードさせるアクションメソッドのサンプルを以下に書いておきます。このアクションメソッドの url を img 要素の src 属性に設定した結果が上の画像です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using AdventureWorksLT;
using Mvc5App.Extensions;
using System.ComponentModel.DataAnnotations;

// Chart を利用するコードのため以下を追加する
using System.Web.UI.DataVisualization.Charting;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.IO;

namespace Mvc5App.Controllers
{
  public class HomeController : Controller
  {        
    public ActionResult Chart()
    {
      SqlDataSource sds = new SqlDataSource()
      {
        ID = "SqlDataSource1",
        ConnectionString = @"接続文字列",
        SelectCommand = @"SELECT 
        Month, 
        SUM(CASE WHEN Name=N'三吉' THEN Sales ELSE 0 END) AS 三吉, 
        SUM(CASE WHEN Name=N'春日' THEN Sales ELSE 0 END) AS 春日, 
        SUM(CASE WHEN Name=N'東雲' THEN Sales ELSE 0 END) AS 東雲, 
        SUM(CASE WHEN Name=N'府中' THEN Sales ELSE 0 END) AS 府中, 
        SUM(CASE WHEN Name=N'広島' THEN Sales ELSE 0 END) AS 広島 
        FROM Shop GROUP BY Month"
      };

      Chart chart = new Chart()
      {
        ID = "Chart1",
        Width = 600,
        DataSource = sds.Select(DataSourceSelectArguments.Empty)
                
        // 注:以下のようにするとエラー
        // DataSourceID = "SqlDataSource1"
      };

      Legend legend = new Legend()
      {
        DockedToChartArea = "ChartArea1",
        IsDockedInsideChartArea = false,
        Name = "Legend1"
      };
      chart.Legends.Add(legend);

      Series series = new Series()
      {
        Name = "三吉",
        ChartType = SeriesChartType.StackedColumn,
        CustomProperties = "DrawingStyle=Cylinder",
        IsValueShownAsLabel = true,
        Label = "#PERCENT{P1}",
        Legend = "Legend1",
        XValueMember = "Month",
        YValueMembers = "三吉"
      };
      chart.Series.Add(series);

      series = new Series()
      {
        Name = "春日",
        ChartType = SeriesChartType.StackedColumn,
        CustomProperties = "DrawingStyle=Cylinder",
        IsValueShownAsLabel = true,
        Label = "#PERCENT{P1}",
        Legend = "Legend1",
        XValueMember = "Month",
        YValueMembers = "春日"
      };
      chart.Series.Add(series);

      series = new Series()
      {
        Name = "東雲",
        ChartType = SeriesChartType.StackedColumn,
        CustomProperties = "DrawingStyle=Cylinder",
        IsValueShownAsLabel = true,
        Label = "#PERCENT{P1}",
        Legend = "Legend1",
        XValueMember = "Month",
        YValueMembers = "東雲"
      };
      chart.Series.Add(series);

      series = new Series()
      {
        Name = "府中",
        ChartType = SeriesChartType.StackedColumn,
        CustomProperties = "DrawingStyle=Cylinder",
        IsValueShownAsLabel = true,
        Label = "#PERCENT{P1}",
        Legend = "Legend1",
        XValueMember = "Month",
        YValueMembers = "府中"
      };
      chart.Series.Add(series);

      series = new Series()
      {
        Name = "広島",
        ChartType = SeriesChartType.StackedColumn,
        CustomProperties = "DrawingStyle=Cylinder",
        IsValueShownAsLabel = true,
        Label = "#PERCENT{P1}",
        Legend = "Legend1",
        XValueMember = "Month",
        YValueMembers = "広島"
      };
      chart.Series.Add(series);

      ChartArea chartArea = new ChartArea()
      {
        Name = "ChartArea1",
        AxisY = new Axis() { Title = "売上高" },
        AxisX = new Axis() { Title = "売上月" }
      };
      chart.ChartAreas.Add(chartArea);

      using (var ms = new MemoryStream())
      {
        chart.SaveImage(ms, ChartImageFormat.Png);

        // キャッシュを許可するか否か、許可する場合は有効期限を
        // 指定しておくべき。
        // 以下のコードはキャッシュを許可しない場合の例。応答ヘ
        // ッダーは次のようになる。
        //    Cache-Control: no-cache
        //    Pragma: no-cache
        //    Expires: -1
        Response.Cache.SetCacheability(HttpCacheability.NoCache);
        Response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
        Response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

        return File(ms.ToArray(), "image/png");

        // File の第 3 引数を以下のように設定すると応答ヘッダに
        // Content-Disposition: attachment; filename=chart.png
        // が含まれるようになる。
        // return File(ms.ToArray(), "image/png", "chart.png");
      }
    }
  }
}

先の記事「Chart」で書きました Web Forms アプリでは、Visual Studio によって自動的に web.config に HTTP ハンドラの定義や appSettings の設定が追加されますが、それらは一切不要です。

ただし、System.Web.DataVisualization の参照への追加が必要です。using 句を追加するだけではダメです。

参照の追加

Tags:

MVC

Linq to Entities で型の初期化

by WebSurfer 2018年4月30日 16:31

以下のコードを実行すると foreach (var item in list2) のところで System.NotSupportedException がスローされ、

1 つの LINQ to Entities クエリ��含まれる構造的に互換性のない 2 つの初期化に、型 'ConsoleAppJoinByLinq2.JoinedList' が指定されています。1 つの型を同じクエリ内の 2 つの場所で初期化することはできますが、両方の場所で同じプロパティが同じ順序で設定されている必要があります。

・・・というエラーメッセージが表示されます。その理由と解決策を書きます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleAppJoinByLinq2
{
  public class JoinedList
  {
    public int ProductID { set; get; }
    public string ProductName { get; set; }
    public int CategoryID { get; set; }
    public string CategoryName { set; get; }
  }

  class Program
  {
    static void Main(string[] args)
    {
      NORTHWINDEntities db = new NORTHWINDEntities();

      var list1 = from c in db.Categories
                  where c.CategoryID == 1
                  select new JoinedList
                  {
                    CategoryID = c.CategoryID,
                    CategoryName = c.CategoryName
                  };

      var list2 = from c in list1
                  join p in db.Products
                  on c.CategoryID equals p.CategoryID
                  select new JoinedList
                  {
                    ProductID = p.ProductID,
                    ProductName = p.ProductName,
                    CategoryName = c.CategoryName
                  };

      foreach (var item in list2)
      {
        Console.WriteLine("{0}, {1}, {2}",
          item.ProductID, item.ProductName, item.CategoryName);
      }
    }
  }
}

上のコードでは、SQL Server のサンプルデータベース Northwind をベースに Visual Studio Communirt 2015 のウィザードを使って作った Entity Data Model (EDM) を使っています。以下の画像を見てください。

Entity Data Model

NORTHWINDEntities は EDM を作ると一緒に自動生成される DbContext クラスを継承したコンテキストクラスです。

Categories と Products は NORTHWINDEntities コンテキストクラスのプロパティで、データベースの当該テーブルを表すエンティティのコレクションを取得・設定するものです。

そして、肝心の話の何故エラーになるかの理由ですが、エラーメッセージの「両方の場所で同じプロパティが同じ順序で設定」という条件が満たされてない、即ち、list1 と list2 のクエリで JoinedList を初期化する際のプロパティの設定が異なるからです。

その前のエラーメッセージの条件「1 つの型を同じクエリ内の 2 つの場所で初期化」には該当しないように見えますが、list1 と list2 のクエリは両方 foreach のところで遅延評価されて、結局「同じクエリ内」ということになるようです。

解決策は、

  1. list1 のクエリに ToList() を適用する(遅延評価されないように)、または、
  2. JoinedList を初期化する際のプロパティの設定を、並び順序を含めて list1 / list2 のクエリで同じになるようにする

・・・です。そうすれば、以下の通り期待した結果が得られます。

実行結果

Tags:

ADO.NET

SQL Server 列の照合順序

by WebSurfer 2018年4月26日 19:27

自分の持っている本「一目でわかる Microsoft SQL Server 2008」によると、SQL Server の照合順序はサーバー、データベース、列、式に対して指定することができるそうです。(テーブルレベルでは指定できないようです)

そして、照合順序は変更できるのですが、既存のデータベースの照合順序を変更しても、そのテーブルの中の列の照合順序は元のままで、変更されないということは知ってました?

実は、自分は知らなかったです。おかげで 1 ~ 2 時間ハマってしまいました。(汗) また時間を無駄にすることがないように備忘録を残しておきます。

(1) データベースの照合順序

データベースの照合順序

データベースの照合順序は SQL Server Management Studio (SSMS) を使って表示・変更することができます。Transact-SQL を使っても可能です。詳しくは Microsoft の文書「照合順序情報の表示」を見てください。

上の画像は、SSMS で既存のデータベース TestDatabase を右クリックして[データベースのプロパティ]ダイアログを開き、[ページの選択]で[オプション]を選択したものです。[照合順序(C)]ボックスに現在の照合順序が表示されています。

この画面で、[照合順序(C)]ボックスのドロップダウンリストから他の照合順序を選んで、変更することができます。

現在はサーバーレベルでのデフォルトの照合順序を継承して Japanese_CI_AS になっていますが、試しにそれを Japanese_BIN2 に変更してみます。

(2) 列の照合順序

列の照合順序

上の画像は、上記 (1) で既存のデータベース TestDatabase の照合順序を Japanese_CI_AS から Japanese_BIN2 に変更した後の、テーブル dbo.T_TRADE の 列 USER_ID (varchar(10), NOT NULL) のプロパティを SSMS で見たものです。

列 USER_ID の照合順序は Japanese_CI_AS のまま変わってないのが分かるでしょうか。

(3) 列の照合順序の変更

列の照合順序の変更

列の照合順序の変更は Transact-SQL を使って可能です。詳しい方法は Microsoft の文書「列の照合順序の設定または変更」を見てください。

上の画像は、SSMS 上で Transact-SQL を使って、テーブル dbo.T_TRADE の 列 USER_ID の照合順序を Japanese_BIN2 に変更したところです。

(4) 変更後の列の照合順序

変更後の列の照合順序

上の画像の通り、上記 (3) の手順を実行した結果、テーブル dbo.T_TRADE の 列 USER_ID の照合順序が Japanese_BIN2 に変更されています。


以上、既存のデーターベースの照合順序を変更しても意味はなくて、上記の操作をして列の照合順序を変えないとダメという話でした。

先の記事「SQL Server の Order By での濁音の扱い」は order by 句による並び順の話でしたが、主キーの場合は Japanese_CI_AS では濁音有り無しを区別しないことにより制約違反になることがあるということで、注意が必要です。

Tags:

SQL Server

About this blog

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

Calendar

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

View posts in large calendar