WebSurfer's Home

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

MVC5 で AjaxHelper が働かない

by WebSurfer 2018年5月28日 15:40

Visual Studio Community 2015(以下、VS2015 と書きます)のテンプレートで [MVC] を選択して生成した ASP.NET MVC5 アプリケーションでは、そのままでは AjaxHelper(Ajax.BeginForm, Ajax.ActionLink など)が働きません。その理由と解決策を書きます。

Ajax.ActionLink を利用したページ

上の画像は Ajax.ActionLink を使って a 要素の href 属性に部分ビューの url を設定したハイパーリンクをレンダリングし(青文字の部分)、ユーザーがハイパーリンクをクリックすると Ajax を利用して部分ビューを呼び出し、応答を UpdateTargetId に指定した div 要素内に書き出した結果です(Customer Details 以下の部分)。

上の画像のアプリは対策済みなので、Ajax を利用した部分ビューの呼び出しと、UpdateTargetId に指定した div 要素内へ応答の書き出しは成功しています。

未対策の場合はハイパーリンクは普通の a 要素の機能しかないので、クリックすると部分ビューは同期呼び出しされ、ブラウザの現在のウィンドウ全体が部分ビューに書き換えられて表示されます。

なぜそうなるかと言うと、必要な外部 .js ファイルがダウンロードされる設定がされてないからです。

そんな設定は自分でやるのが当たり前と言われるかもしれませんが、Visual Studio 2010 Professional(以下、VS2010 と書きます)のテンプレートで自動生成される MVC4 アプリのプロジェクトでは、AjaxHelper に必要な外部 .js ファイルがダウンロードされる設定が含まれているのです。

なので、VS2015 で MVC5 アプリのプロジェクトを作って、既存の MVC4 の AjaxHelper を使うコードを MVC5 に移植しただけでは期待した Ajax の機能は働きません。

何を隠そう、自分はその理由が分からずハマってしまいました。(汗) また無駄な時間を費やすことがないよう、以下に備忘録として原因と対策を書いておきます。

MVC4 以降は控えめな JavaScript を利用して、Ajax.BeginForm, Ajax.ActionLink などから生成される form 要素、a 要素の属性の設定に応じて Ajax 呼び出しと応答の表示を行うようになっています。

その控えめな JavaScript は jquery.unobtrusive-ajax.js または jquery.unobtrusive-ajax.min.js というファイルに含まれています。(ちなみに、MVC3 以前は「控えめ」ではない MicrosoftAjax.js と MicrosoftMvcAjax.js を使います)

例えば、Ajax.ActionLink を使った場合、上の画像の例の「Ms. Rosmarie Carroll」のハイパーリンクの html 要素は以下のようになります。

<a  data-ajax="true" 
    data-ajax-mode="replace" 
    data-ajax-update="#results" 
    href="/Customer/Details/6">
    Ms. Rosmarie Carroll
</a>

控えめな JavaScript は、上の a 要素の属性 data-ajax, data-ajax-mode, data-ajax-update の設定に従って Ajax 呼び出しと応答の処理を行います。

従って、やるべきことは控えめな JavaScript の外部ファイルがダウンロードされるように設定を行うことです。VS2010 で作る MVC4 アプリはそのあたりの設定が自動的に行われますが、VS2015 で作る MVC5 アプリは開発者が自分で設定しなければなりません。

前置きが長くなりましたが、その方法を以下に書きます。

まず、NuGet から Microsoft.jQuery.Unobstrusive.Ajax をインストールします。以下の画像を見てください。

NuGet からインストール

インストールが完了すると、Script フォルダに jquery.unobtrusive-ajax.js, jquery.unobtrusive-ajax.min.js がインストールされるはずですのでチェックしてください。

スクリプトファイル

App_Start フォルダの BundleConfig.cs ファイルを開いて上記 .js ファイルがバンドルされるように設定します。下の画像の赤枠部分を見てください。ここでは MVC4 と同様に、クライアント側での検証用のスクリプトファイルと一緒にダウンロードされるようにしています。

BundleConfig.cs の設定

_Layout.cshtml には @RenderSection("scripts", required: false) が含まれているので、_Layout.cshtml を使う View では以下のコードを追加すれば OK です。

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

_Layout.cshtml を使わない View の場合は @Scripts.Render("~/bundles/jqueryval") の位置が jQuery.js の後に来るように注意してください。

Tags:

MVC

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

About this blog

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

Calendar

<<  2018年7月  >>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar