Kobarin's Development Blog

C#やASP.NETなどについての記録です。

HTTP Error 500.21 - error code:0x8007000d の解決方法

ローカル環境に新たにIISをインストールして実行したところ、表題の通りのエラーが発生し、解決できたのでメモ。

環境

症状

サイト全体において表題のエラーが発生。

HTTP Error 500.21
Error Code 0x8007000d
Message ASP.NETがインストールされていないか、完全にインストールされていません(再現できなかったので覚えているメッセージです。少し違うと思います)

原因

エラーメッセージは「ASP.NETがインストールされていない」旨の内容ですが、正確にはIIS側にASP.NETを登録(?)するプロセスがされていないことが原因のようです。Web Platform Installerを使って.net frameworkをインストールするとこの処理がされないような情報も以前見た気がしますが、定かではありません。

解決方法

HTTP Error 500.21 when trying to host an ASP.NET web app with IIS 7 のWinAnimesh氏のReplyの通りですが、詳しく手順を示します。

  1. コマンドプロンプトを管理者権限で実行
  2. 「C:\Windows\Microsoft.NET\Framework64\v4.0.30319」に移動
  3. 「aspnet_regiis -i」を実行
  4. 数十秒後、処理が完了します

LayoutTemplate内にコードブロック

LayoutTemplate内にコードブロックは配置できない

ListViewのLayoutTemplate内にコードブロックが配置できない事、および代替策をメモします。

例えば以下のように、何らかの一覧をulでバインドし、上部に件数を示したいケースを想定します(該当件数の箇所だけListView外に置けばいいだろう、という突っ込みはナシです)。

<asp:ListView id="lv" ...>
  <LayoutTemplate>
    該当件数: <%=_listCount %>
    <ul>
    <asp:PlaceHolder ID="itemPlaceholder ... />
    </ul>
  </LayoutTemplate>
  <ItemTemplate>
    <li><%# Eval("Name") %></li>
  </ItemTemplate>
</asp:ListView>

コードビハインドは以下の通り。

protected _listCount = 0;

protected Page_Load(...) {
  MyCompany mycom = new MyCompany();
  List<Employee> employees = mycom.GetEmployee();
  _listCount = employees.Count;
  lv.DataSource = employees;
  lv.DataBind();
}

これを実行すると、以下の様なエラーになります。

コントロールにコード ブロック (<% ... %>) が含まれているため、コントロールのコレクションを変更できません。

どうやらLayoutTemplate内でコードブロックは使えない事がわかります。

<%# ~ ->にすれば…

次に、「=」を「#」に変えてみました(以下、LayoutTemplate部のみ抜粋)。データバインドをしているので、何となくイケそうな気がしますが…

  <LayoutTemplate>
    該当件数: <%# _listCount %>
    <ul>
    <asp:PlaceHolder ID="itemPlaceholder ... />
    </ul>
  </LayoutTemplate>

結果は、エラーこそ表示されないものの、件数の部分が空欄になってしまいます。

該当件数: ←何も表示されない?!
・田中
・鈴木
・山田

以上の現象から、このListViewの挙動に関して、2つの原因が考えられます。

  1. ListViewのデータバインドはあくまでItemTemplateやGroupTemplateに対してのみであり、LayoutTemplate部は対象外
  2. コードビハインドの実行よりも、LayoutTemplate部のデータバインドが先に実行されてしまっている

ListView_LayoutCreatedイベントで解決

どちらが正しいか分かりませんが、今回もStackOverFlowの過去の書き込みで解決しました。

asp.net - Get public string in codebehind into LayoutTemplate of ListView - Stack Overflow

の中でReplyされている、以下の箇所です。

protected void lv_LayoutCreated(Object sender, EventArgs e)
{
  lv.Controls[0].DataBind();
}

どうやらControls[0]がLayoutTemplateを示すらしく、これは手動でバインドしてやらなると良いようです。
そもそもLayoutTemplateがバインドを自ら行わないから強制的にバインドさせてやる必要があるのか、それともバインドがコードビハインドより先に行われてしまう為に後から再度実行させてやる必要があるのか判然としませんが、とりあえず解決策でした。




オブジェクトの権限一覧、ストアドプロシージャの引数一覧

権限一覧はSQL Server Management Studioで確認できるわけですが、確認のためだけにSSMSを起動するのは重いし、管理者権限であれば操作ミスも起こりえます。

日頃Common SQL Environmentを使用しているのであれば、コマンドで実行できれば簡単だと考え探したところ、発見出来ました。

ASP.NETのPostback先URLに、Rewrite前のアドレスを設定したい

ASP.NETでURLの置き換えをする場合、通常Rewriteを使用します。
URLの置換え規則は正規表現で定義しますが、この辺りは他のサイト等でも多く書かれているので割愛します。
ここでは、ASP.NETのRewriteを使う際によく問題になる「Postback先URL」に触れたいと思います。


例えばクライアント側から以下のアドレスへの要求があったとします。

/site1/product/city-13101/price-980/ (BEFOREとします)

もちろんこれは仮想アドレスに過ぎず、実体は存在しません。Rewriteにより以下の実ページヘアクセスされます。

/site1/product.aspx?city=13101&price=980 (AFTERとします)

この場合、クライアントから見えているURLはBEFOREですが、ASP.NETとしてはAFTERとして認識されるため、Postback先URL(formタグのaction要素)も必然的にAFTERのアドレスが入ります。その結果、ここでSubmit等によりPostbackが発生すると、クライアントからAFTERのアドレスが丸見えになってしまうのです。


そこで、HTTP_REFERRERを使ってPostback先のURLを強引に書き換えてしまうのが、以下のコードです。

protected void Page_Load(object sender, EventArgs e)
{
  form1.Action = Request.RawUrl;
}

▼参考
ASP.NET postbacks creates problem in URL rewriting?


以前はMasterPageにformを設置した時期もありましたが、こういうのを見るとやはりページ単位にformを置くのが良いと感じますね。

IIS7.0で.net framework4.0で動作させる

BugTrackerを導入しましたが、.net framework 4.0で動作するようでした。
これまで全て2.0ベースで開発してきたため、4.0は初めてでしたが、VSの開始オプションを「4.0」にすれば良いだけだと軽く考えていました。が、実際にはIIS側の設定が必要でした。

症状から説明します。BugTrackerを導入後に実行したところ、

認識されない属性 'targetFramework' です。

というエラー画面になり、web.configの「」に問題がある事を指摘されました。
エラー画面に共通している、画面下に表示される実行時の.netのバージョンが2.0.xxxとなっており、4.0で実行されていないことが分かります。

ASP.NETサイトを .NET Framework 4環境へ移行すると「構成エラー:認識されない属性 'targetframework' です」エラーが発生する
にもある通り、アプリケーションプール(以降、APとします)側の変更なようで、手順は以下の通り。

  1. IIS Managerを開く
  2. 左サイドより「アプリケーションプール」
  3. .net4にするサイト名を右クリックして「基本設定」
  4. .net framework version」が2になっているので、4に変更

以上で完了したと思いましたが、私の場合この状態だとまだver2.0で実行されていました。
私の場合、サイトアプリケーションのAPが「Default Application Pool1」(だったかな)になっており、サイト名と同じAPになっていなかったためです。
必ずしも同じようになるわけではないと思いますが、もしAPが「Default〜」になってしまっていた場合の方法は以下の通り。

  1. アプリケーションを選択して「基本設定」
  2. アプリケーションプールを「Default〜」からサイト名に変更

以上。

HtmlAgilityPackの基本

HTMLからスクレイピングでデータを抜き出す際に使用しました。
現時点でわかった使用例を示します。

HTML code example

<html>
<head></head>
<body>
  <h3>企業いろいろ</h3>
  <h4>コンビニ</h4>
  <ul>
    <li><a href="711.htm">セブン-イレブン</a></li>
    <li><a href="lawson.htm">ローソン</a></li>
    <li><a href="famima.htm">ファミリーマート</a></li>
  </ul>
  <h4>PC</h4>
  <ul>
    <li class="usa"><a href="hp.htm">HP</a></li>
    <li class="usa"><a href="dell.htm">Dell</a></li>
    <li><a href="epson.htm">Epson Direct</a></li>
  </ul>
  <h4>EC</h4>
  <ul>
    <li class="usa"><a href="amazon.htm">Amazon</a></li>
    <li><a href="rakuten.htm">楽天</a></li>
  </ul>
  <div class="migi">注意:順不同です</div>
  <div id="footer">copyright 2013 Kobarin</div>
</body>
</html>

前処理(対象によって変更して下さい)

string html = "";                  //HTMLコード
HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
doc.OptionAutoCloseOnEnd = false;  //最後に自動で閉じる(?)
doc.OptionCheckSyntax = false;     //文法チェック。
doc.OptionFixNestedTags = true;    //閉じタグが欠如している場合の処理
doc.LoadHtml(html);

取得コード

Exp1. id・classを指定しての要素を取得(単一取得)
HtmlAgilityPack.HtmlNode node1 = doc.DocumentNode.SelectSingleNode("div[@id='footer']");

Console.WriteLine(node1.InnerHtml);

結果

copyright 2013 Kobarin
Exp2. <li>タグの中身を全部取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//li");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

<a href="711.htm">セブン-イレブン</a>
<a href="lawson.htm">ローソン</a>
<a href="famima.htm">ファミリーマート</a>
<a href="hp.htm">HP</a>
<a href="dell.htm">Dell</a>
<a href="epson.htm">Epson Direct</a>
<a href="amazon.htm">Amazon</a>
<a href="rakuten.htm">楽天</a>
<a href="yahoo.htm">ヤフーショッピング</a>
Exp3. aタグの文字列部分を取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

セブン-イレブン
ローソン
ファミリーマート
HP
Dell
Epson Direct
Amazon
楽天
ヤフーショッピング
Exp4. 特定classのタグのみ取得
//<li class="usa">のタグを取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//li[@class='usa']");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

<a href="hp.htm">HP</a>
<a href="dell.htm">Dell</a>
<a href="amazon.htm">Amazon</a>
Exp5. 順番を指定して特定タグを取得
//body直下にある、2番目のdivタグ
HtmlAgilityPack.HtmlNode node = doc.DocumentNode.SelectSingleNode("/html[1]/body[1]/div[2]");

Console.WriteLine(node.InnerHtml);

結果

copyright 2013 Kobarin
Exp6. aタグのhref内のURLを全て取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a");

foreach (HtmlAgilityPack.HtmlNode node in nodes) {
  string url = node.GetAttributeValue("href", "");
  Console.WriteLine(url);
}

結果

711.htm
lawson.htm
famima.htm
hp.htm
dell.htm
epson.htm
amazon.htm
rakuten.htm
yahoo.htm
Exp7. 全ul内の1番目のli内のリンク文字列だけ取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//li[1]/a");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

セブン-イレブン
HP
Amazon
Exp8. 全ul内の最後のli内のリンク文字列を取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//li[last()]/a");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

ファミリーマート
Epson Direct
楽天
Exp9. 各ul内で2番目以降にあり、且つclassが"usa"のli内にあるリンク文字列を取得
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//li[position() >= 2 and contains(@class, 'usa')]/a");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

Dell
Exp10. a内のテキストに'D'を含む内容を取得(※カンマを使います)
HtmlAgilityPack.HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a[contains(.,'D')]");

foreach(HtmlAgilityPack.HtmlNode node in nodes){
  Console.WriteLine(node.InnerHtml);
}

結果

Dell
Epson Direct

注意点

HTMLコードそのものにエラーがあると正常に処理されません。
例えば私が試した所では、閉じタグが抜けていたり、<div><p></div></p>のように互い違いになっていると抽出も出来ないため、ソースコードの段階で補正をしてやる必要があります。

参考

HtmlAgilityPackの記述は原則的にXPathに準拠するらしく、自分の場合はXPath Syntaxを参考にさせて頂きました。
また、HtmlAgilityPack Simulatorも公開しましたので、記述テスト等にお使い下さい。

ASP.NETで、Google Calendarの情報を表示

ASP.NET上でGoogle Calendarのデータを呼び出し、GridViewに表示するまでの処理を示します。

1.Google Data APIの.NET libraryをダウンロードしてインストール

以下サイトから最新の「Google_Data_API_Setup_xxx.msi」をダウンロードします(当方の時点では2.1.0でした)。
.NET library for the Google Data API

2.Visual StudioでWebサイトを開き「参照設定」の「.NET」タブ内より、以下コンポーネントを選択。
3.ASPXファイルを開き、GridViewを記述。

4.aspx.csファイルを開き、以下の記述をする。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using Google.GData.Calendar;
using Google.GData.Client;
using Google.GData.Extensions;
using Google.GData.AccessControl;

public partial class mypage : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    CalendarService cservice = new CalendarService("companyName-applicationName-1");

    //認証。xxxxxにはGmailアカウント、XXXXXXXXにはGmailパスワード
    cservice.setUserCredentials("xxxxx@gmail.com", "XXXXXXXX");

    //CalenderのソースURL。GoogleCalendarの設定内でURL取得可能。※1
    const string gcalUrlFormat = "https://www.google.com/calendar/feeds/{0}/public/full";
    const string guserMy = "xxxxx@gmail.com";
    string gcalUrl = String.Format(gcalUrlFormat, guserMy);

    // 取得条件設定(開始時間が2011年3月19日の予定を降順で取得)
    EventQuery query = new EventQuery();
    query.Uri = new Uri(gcalUrl);
    query.StartTime = new DateTime(2012, 10, 23); //抽出条件:開始日
    query.EndTime = new DateTime(2012, 10, 24);   //抽出条件:最終日
    query.SortOrder = CalendarSortOrder.descending;

    //データを取得
    EventFeed feeds = cservice.Query(query);
    //※2
    IEnumerable entries
     = feeds.Entries.Cast();

    //GridViewのデータソースとなるDataTableの定義。ここでは開始日・最終日・タイトル・場所のみ抽出。
    DataTable dt = new DataTable();
    dt.Columns.Add("StartTime", Type.GetType("System.DateTime"));
    dt.Columns.Add("EndTime", Type.GetType("System.DateTime"));
    dt.Columns.Add("Title", Type.GetType("System.String"));
    dt.Columns.Add("Location", Type.GetType("System.String"));
    foreach (Google.GData.Calendar.EventEntry entry in entries)
    {
      DateTime startTime = entry.Times[0].StartTime;
      DateTime endTime = entry.Times[0].EndTime;
      string title = entry.Title.Text;
      string location = entry.Locations[0].ValueString;
      dt.Rows.Add(startTime, endTime, title, location);
    }
    GridView1.DataSource = dt;
    GridVeiw1.DataBind();
  }

注釈
  • ※1 URLのメルアド部分を書き換えれば、他ユーザや他グループの公開カレンダーも参照できる。publicをprivateに変更すると非公開カレンダーも参照可能だが、当然自アカのカレンダー限定。末尾のfullをbasicにすると情報量が限定されるため速度改善につながるかも。ただし他カレンダーの場合日付を取得できなかった。
  • ※2 EventEntryはSystem.Web.UIとカブってしまうため、フル宣言しています(他に方法あるのかな?)