2019年11月27日水曜日

ASP.NET CoreでSignalRを使ってファイルダウンロード

環境

  • ASP.NET Core 2.1
  • Microsoft.AspNetCore.SignalR 1.0.4
  • jQuery 3.3.1
  • Visual Studio 2017  15.9.10

やりたいこと

  •  サーバー上でPDFを作成しダウンロードする機能を作ったが処理に時間がかかる。このため、ただローディング画像を出すだけではなく処理状況の進捗を表示し終わったら自動でダウンロードするようにしたい。(※サイズの大きいファイルのダウンロード状況の進捗ではないです。)

とにかくソース

asp.net core controller (C#)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRFileDownload.Hubs
{
 public class DemoHub : Hub
 {
  public Task Download()
  {
   byte[] contents = null;

   /*
    * 処理に時間がかかる処理
    * Clients.Caller.SendAsync("MessageReceive","通知メッセージ");
    */
   var result = new FileContentResult(contents, "application/pdf") { FileDownloadName = "demo.pdf" };
   return Clients.Caller.SendAsync("Complete", result);
  }
 }
}

※上記に加えサーバー側ではStartup.csにMapHub設定追加
JavaScript

//SignalRコネクション作成
const connection = new signalR.HubConnectionBuilder()
 .withUrl("/demohub")
 .configureLogging(signalR.LogLevel.Information)
 .build();
//コネクションスタート
connection.start().catch(e => console.log(e));
//ダウンロード実行
function execute() {
 connection.invoke('Download').catch(e => console.log(e));
}
//メッセージ表示
connection.on('MessageReceive', (message) => {
 console.log(message);
});
//ファイルデータ受信処理(サーバー側処理完了時)
connection.on('Complete', (data) => {
 let filename = decodeURI(data.fileDownloadName);
 var mimeType = data.contentType;
 var downloadData = toBlob(data.fileContents, mimeType);
 let downloadUrl = (window.URL || window.webkitURL).createObjectURL(downloadData);
 let link = document.createElement('a');

 link.href = downloadUrl;
 link.download = filename;
 link.click();
 (window.URL || window.webkitURL).revokeObjectURL(downloadUrl);
});
// Base64とMIMEコンテンツタイプからBlobオブジェクトを作成する。(日本語対応)
function toBlob(base64, mime_ctype) {
 // 日本語の文字化けに対処するためBOMを作成する。
 var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
 var bin = atob(base64.replace(/^.*,/, ''));
 var buffer = new Uint8Array(bin.length);
 for (var i = 0; i < bin.length; i++) {
  buffer[i] = bin.charCodeAt(i);
 }
 try {
  var blob = new Blob([bom, buffer.buffer], {
   type: mime_ctype,
  });
 } catch (e) {
  return false;
 }
 return blob;
}


ポイント

  • サーバー側ではFileContentのオブジェクトを返す。
  • クライアント側では戻り値にfileDownloadName、contentType、fileContentsの各プロパティから必要なデータが取得できる。(fileContentsの中身はBase64でエンコードされている。)
  • 日本語に対応するためBlobオブジェクトを作成する際にBOMを付与してやる必要がある。
  • Blobオブジェクト作ってリンクタグ作ってソース上でクリックさせる。

雑感

この対応入れる前はajaxでファイルダウンロード処理してローディング画像表示ってやってたんですがそれでも我慢できないくらい時間のかかる処理だったためSignalR対応決意。思ったよりはまってしまったのですがControllerと同じくSignalRのHubでもFileContentを返せばいいだけでは、と気づいてからは早かったです。

参考にさせて頂いたサイト

 https://www.hos.co.jp/blog/20170213/




2018年6月4日月曜日

ASP.NET Identity Facebook認証失敗

VS2015+Owin3.0.1を使用してFacebookで単純な認証処理を実装しようとして無駄にはまってしまったので忘れないようにメモ

環境

  • Visual Studio 2015
  • .Net Framework 4.6.1
  • ASP.NET MVC 5
  • Microsoft.Owin 3.0.1.0

現象

  1. Facebook developer設定。
  2. ASP.NET 新規MVCサイト作成。
  3. Startup.Auth.csのFacebook認証箇所コメントアウトを外す。(app.UseFacebookAuthentication)
  4. appId,appSecretを設定(手順1の操作で取得したもの)
  5. 起動してログイン画面、"Facebook"を選択


認証は通っているように見えるが、下記問題が発生

  • AccountController ExternalLoginCallbackメソッド内のAuthenticationManager.GetExternalLoginInfoAsync戻り値がNullになる。
  • Facebookから戻ってきた後ログイン画面がまた表示される。またURLが”http://localhost:xxxxx/Account/Login?_=_”となる。(後ろに_=_が追加された状態)


対応方法

NugetでMicrosoft.Owin関係のバージョンを3.1以上に、Newtonsoft.Jsonのバージョンを10.0以上にあげる。

※Visual Studio 2015で作るとMicrosoft.Owinが3.0.1




2018年5月30日水曜日

ASP.NET Identity Twitter認証で「応答の状態コードは成功を示していません」エラー

訂正


2018/08/01 時点 Twitter Application Managementの設定が変更(APIの仕様変更)になったため、 2018/5/30に書いた当ページの方法では動作しなくなりました。


※Enable Callback Lockingの設定がなくなりました。

対処方法

 Twitter Application Managementの設定 「Callback URLs」に「http://localhost:xxxxx/signin-twitter」を設定。
※xxxxxは動作環境のポート番号

もともとの設定でCallback URLsに設定してた値のミスが原因でした。
Enable Callback Lockingの設定関係なく、正しくCallback URLs指定してれば発生しない問題です。orz



概要

ASP.Net Identityの基本認証を使用しTwitterアカウントでの認証を実装しようとしたところ、「応答の状態コードは成功を示していません: 403 (Forbidden)。」が発生し、軽くはまってしまったので、対象方法メモ。




前提条件

環境

  • Visual Studio 2015
  • .Net Framework 4.6.1 
  • ASP.NET MVC 5

状況

  1. 新規にWEBプロジェクトを作成(ASP.NET MVC指定。認証は個別のユーザーを指定)
  2. Twitter Application Management の設定を行う。
  3. ASP.Net Identity でTwitterの認証を有効にする。※
  4. WEBサイト起動し、ログイン→Twitterをクリックする。
  5. エラーが発生

※手順1~3の詳細は割愛。
※「検証プロシージャによると、リモート証明書は無効です」エラーが発生する場合は下記サイトの対応を実施
https://stackoverflow.com/questions/25011890/owin-twitter-login-the-remote-certificate-is-invalid-according-to-the-validati

対応方法


Twitter Application Management の設定で
Settings -> [Enable Callback Locking]のチェックを外す。


  • ※2018/05/30 デフォルトはチェックありでした。

その他

チェック外していいのか、って所がよくわかってません。
どんなパターンで問題になるんだろ。。


2017年4月25日火曜日

sphinx-autobuildをWindowsで使ってみる

Sphinxでドキュメント作る際に、ビルドの手間を軽減してくれる「sphinx-autobuild」を windowsで使用する際にはまったのでメモ

前提条件

Sphinxがインストールされていること。


環境

  • Windows 7
  • Python 2.7
  • Sphinx 1.3.1
  • sphinx-autobuild 0.6.0 ←今回環境を作るやつ
※Sphinxとはといったことについては割愛


インストール

pip install sphinx-autobuild

使い方

sphinx-autobuild <ソースディレクトリ> <ビルド成果物出力ディレクトリ>

※コマンド実行後http://127.0.0.1:8000アクセス


インストールと使い方は下記サイト参考にしました。
http://qiita.com/mikanbako/items/28a3fc5d1da42939f941


Sphinxプロジェクトのファイル名・パスに全角がある場合


ファイル名やパスに全角があると「UnicodeDecodeError」が発生し、ブラウザで表示しても500エラーが表示される。
原因はPython2.xの場合文字コードの初期値がasciiコードだが、WindowsのファイルパスはShift-JISのため。
この場合、sitecustomize.pyというテキストファイルを作成し、下記の2行を記載、Pythonインストールフォルダ内にある「site-packages」フォルダを探し、直下に保存する。(ファイル自体の文字コードはShift-JISで作成する。)

import sys
sys.setdefaultencoding('cp932')


※Sphinxのドキュメント自体はUTF-8で記述しなければいけないのが地味に問題をややこしくします。


2015年9月4日金曜日

DapperExtensionsを使ってみる2

前回に引き続きDapperExtensionsの小ネタです。
※我ながら遅筆にもほどがある。


今回は、テーブル名や、カラム名がエンティティクラスと異なる場合や、
主キーが複数の場合の対応です。特に主キーは1個にしといてよっと
いいたいところなんですが、会社で業務アプリをさわってる以上、
避けられない、っというか、ほぼ全部ですよってことで。


やり方

前回は複数形のテーブル名を単数形のクラス名に変換するためにすでに準備されている、PluralizedAutoClassMapperをDapperExtensions.DefaultMapperに 設定しましたが、今回はこの部分をカスタムで作ってやる必要があります。


前提条件


こんなテーブルがあったとします。(テーブル名は"syain")


こんなエンティティクラスにMappingしたいです。
public class Person
{
    public int BuilderId { get; set; }
    public string PersonId { get; set; }
    public string Name { get; set; }
}

  • gyousya_id(int)とsyain_id(nvarchar)で複合キー
  • テーブル名syainをPersonに
  • カラム名gyosya_idをBuilderIdに
  • カラム名syain_idをPersonIdに
  • カラム名namaeをNameに


とりあえずやってみる


1.CustomMapperの作成


DapperExtensions.Mapper名前空間にあるClassMapperを継承したCustomMapperを作成します。
01:using DapperExtensions.Mapper;
02:public sealed class CustomClassMapper< T > : ClassMapper< T > where T : class
03:{
04:    public CustomClassMapper()
05:    {
06:        if (typeof(T) == typeof(Person))
07:         {
08:             Table("syain");
09:             Map(x => (x as Person).BuilderId).Column("gyosya_id").Key(KeyType.Assigned);
10:             Map(x => (x as Person).PersonId).Column("syain_id").Key(KeyType.Assigned);
11:             Map(x => (x as Person).Name).Column("namae");
12:        }
13:         AutoMap();
14:    }
15:}

2.DefaultMapperの指定


先ほど作成したCustomMapperをDefaultMapperに指定します。
DapperExtensions.DapperExtensions.DefaultMapper = typeof(CustomClassMapper<>);
この処理はアプリケーション起動時に1回呼ぶだけでおっけーです(前回と一緒です)。 あとはInsert/Update/Deleteも前回と同じ方法で行うことが可能です。


解説・ポイント


  • 01行目:DapperExtensions.Mapper空間のClassMapperを継承したクラスを作成します。
  • 04行目:コンストラクタを作成します。この中にマッピング定義を記載します。
  • 06行目:マッピング対象のクラスを判別します。複数テーブルのマッピングを定義する場合は、ここで切り分けます。
  • 08行目:テーブル名のマッピング定義です。エンティティクラス名とテーブル名が同一の場合は不要です。
  • 09~10行目:主キーが複数の場合の定義です。
  • 11行目:カラム名のマッピング定義です。テーブル名同様、変更がない場合は不要です。
  • 13行目:これはデフォルトのマッピングを追加するメソッドになります。テーブルに「birthday」というカラムがあり、エンティティクラスにも「birthday」という同名のプロパティがある場合必要になります。(今回の例の場合不要ですが、やらないと全定義を記載する必要があるため参考までに記載してあります。)

ポイントとなるのは9~10行目です。今回はカラム名のマッピングとキー指定を同時に行っているため、Column("~").Key(KeyType.Assigned)といった記載になります。
カラム名のマッピングが不要の場合はColumn("~")の部分は不要です。


感想

テーブル数の数だけ、Mapping定義を追加していかなくてはならないので、最初がとても
めんどうです。が、1回設定してしまえば終わりなので(データベースの主キーや名前が変わる
ってことはまずないので)、そこさえ乗り切ってしまえば、さくさく使えるようになって便利です!


2015年5月19日火曜日

DapperExtensionsを使ってみる

Dapperの拡張ライブラリDapperExtensionsについてです。


Dapperとは

 
  すでに解説されてる方がたくさんいらっしゃるので、そちらをご覧ください。


DapperExtensionsとは

  
DapperはORMと冠しているものの、実際はクエリすべて手打ちです。(それがいいんですが)
なんですが、単純なSELECTやINSERT、UPDATE、DELETEはいちいち手打ちするのも面倒!
そんな時に、さくっとかけるようになるDapperの拡張ライブラリです。
 

DapperExtensionsの基本的な使い方


※以下コード部分はC#で記載します。

まず、DapperとDapperExtensionsをNugetなどでプロジェクトに追加します。

PM> Install-Package Dapper
PM> Install-Package DapperExtensions


例えばこんな構成のPersonsテーブルがあったとして、
※主キーはIdentity








こんな、エンティティクラスがあったとします。
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}


登録処理を記述するクラスにDapperと、DapperExtensions名前空間をusing します。
 using DapperExtensions;
 using DapperExtensions.Mapper;


今回はテーブル名がPersons、エンティティクラス名がPerson、なので複数形と単数形を変換するルールをプログラム開始時に指定します。
(この処理は1回だけ呼べばOKです。)
DapperExtensions.DapperExtensions.DefaultMapper = typeof(PluralizedAutoClassMapper<>);

Connectionオブジェクトを作成します。

 var connection = new SqlConnection("<接続文字列>");
 connection.Open();

あとは以下ような感じで登録・更新・削除できます。

新規登録(Insert)

connection.Insert(new Person { Name = "テスト太郎" });

更新(Update)

connection.Update(new Person { Id = 1, Name = "テスト太郎更新" });

削除(Delete)

connection.Delete(new Person { Id = 1 });


※connectionのDispose処理記載するの省略してます。

その他

※デフォルトではテーブルの主キーが1つで、IdやPersonIdといった名称の場合にのみ対応します。
複合キーだったりした場合は別途マッピングルールを指定する必要があります。(あとテーブル名とエンティティクラス名が異なる場合とか)
(最初それについて書くつもりだったんですが、だらだらと長くなりそうだったので次回)

雑記

会社で業務システムに携わってるとあれな設計のテーブルも多く(1テーブルカラム100以上とか)、 そういったテーブルに対していちいち単純なSQL文書くのもめんどくさいのでとても重宝してます。

エンティティクラスと条件指定して簡単な検索(SELECT文発行)なんかもできるんですが、これについてはDapperExtensions使わずに、素直にクエリ書いたほうが楽だと思います。条件の指定方法が独特だったり、少し複雑にしようと思うと対応できなかったりします。

2014年12月16日火曜日

NPOIで改ページを設定する

.Net Framework用エクセル編集ライブラリのNPOIで改ページをする方法です。

NPOIのバージョンは2.0.6.0、動作確認に使用したExcelは2002です。(ふるっ)

会社ではエクセル出力の要望が多く、NPOIは簡単なエクセルなら
すぐできてしまうのでとても便利です。
//エクセルブックとシートのオブジェクト作成
IWorkbook  book = new HSSFWorkbook();
ISheet sheet = book.CreateSheet("testsheet");
//適当なデータを設定
for (int i = 0; i < 6; i++)
{
 var row = sheet.CreateRow(i);
 for (int j = 0; j < 6; j++)
 {
  var cell = row.CreateCell(j);
  cell.SetCellValue(string.Format("({0},{1})", i + 1,j + 1));
 }
}
using (var fs = new FileStream(@"d:\test.xls", FileMode.OpenOrCreate, FileAccess.Write))
 book.Write(fs);

この時に印刷範囲の指定をしようとしてはまりました。
NPOIにはISheetインタフェースにSetRowBreak、SetColumnBreakというメソッドが
あるのでこれを指定するだけ、と思いましたが期待通りの動作はしてくれません。

//エクセルブックとシートのオブジェクト作成
IWorkbook  book = new HSSFWorkbook();
ISheet sheet = book.CreateSheet("testsheet");
//適当なデータを設定
for (int i = 0; i < 6; i++)
{
 var row = sheet.CreateRow(i);
 for (int j = 0; j < 6; j++)
 {
  var cell = row.CreateCell(j);
  cell.SetCellValue(string.Format("({0},{1})", i + 1,j + 1));
 }
}

sheet.SetRowBreak(2); //追加
sheet.SetColumnBreak(2); //追加

using (var fs = new FileStream(@"d:\test.xls", FileMode.OpenOrCreate, FileAccess.Write))
 book.Write(fs);
※エクセルの3列目と3行目に改ページを行う設定を追加



対処方法ですが、SheetオブジェクトのFitToPageプロパティをFalseに設定すれば
思ったとおりの動作をしてくれます。

//エクセルブックとシートのオブジェクト作成
IWorkbook  book = new HSSFWorkbook();
ISheet sheet = book.CreateSheet("testsheet");
//適当なデータを設定
for (int i = 0; i < 6; i++)
{
 var row = sheet.CreateRow(i);
 for (int j = 0; j < 6; j++)
 {
  var cell = row.CreateCell(j);
  cell.SetCellValue(string.Format("({0},{1})", i + 1,j + 1));
 }
}

sheet.SetRowBreak(2);
sheet.SetColumnBreak(2);
sheet.FitToPage = false; //追加
using (var fs = new FileStream(@"d:\test.xls", FileMode.OpenOrCreate, FileAccess.Write))
 book.Write(fs);



FitToPageはシートを1ページに収まるように自動で調整するプロパティでデフォルトでTrueになっているため、SetRowBreak,SetColumnBreakを設定してもこちらが優先されてしまうようです。